# Parameter Sweep Plot Explorer

Two head-to-head builds of the same UX so the splining-vs-compute-location
question can be settled empirically.

## What it does

You upload a multi-dimensional sweep dataset (long-format CSV or JSON). The
tool figures out which columns are swept inputs and which are measured
outputs, lets you pick two inputs as X / Y plot axes and one output as the
metric, and turns the remaining input columns into sliders. The plot
re-renders in real time as you drag a slider — that slice of the input space
is interpolated with cubic splines.

Use case it was built for: an LLC inverter swept over frequency × power ×
battery_voltage × dc_link → (efficiency, output_voltage). A bundled sample
dataset is at `sample_data/llc_sweep.csv`.

## Two versions

| Aspect | `frontend/` — client-side compute | `backend/` — server-side compute |
|---|---|---|
| Where data lives after upload | In-browser only — the file is parsed locally, never uploaded | In-process Python memory keyed by a session token (no disk, no DB) |
| Where the cubic spline is fit | Browser JS (natural cubic spline, one per slider dim) | `scipy.interpolate.RegularGridInterpolator(method="cubic")` |
| Where LTTB downsampling happens | Browser JS | numpy in the FastAPI handler |
| Render path | Slider event → in-memory n-D grid slice → Plotly | Slider event → POST `/api/slice` → JSON → Plotly |
| Hosting need | Any static-file server (nginx, S3, Pages, …) | Python process + reverse proxy |
| Slider latency (sample 1360 rows) | ~3–10 ms wall, no network | ~12–40 ms wall incl. localhost HTTP |
| First-paint cost | One file fetch (Plotly CDN) + parsing the dataset locally | One file upload + the server fits the interpolator once |
| Privacy story | The dataset never leaves the user's machine | Dataset is sent to the server (in-memory only, GC'd after 1 h) |
| Scales with dataset size | Limited by browser memory; JS parse for million-row CSV is rough | Server can hold larger grids; scipy is faster on big interpolators |
| Where it wins | Quick experiments, privacy, no infra to keep running | Big datasets, heavy spline fits, GPU/CPU offload, reusable from non-browser clients |
| Where it loses | Slow on million-row CSVs, struggles on low-end devices | Needs an always-on backend and a reverse proxy; one more failure mode |

## Repo layout

```
plot-explorer/
├── README.md                  ← this file
├── generate_sample.py         ← regenerate the synthetic LLC sweep
├── sample_data/
│   └── llc_sweep.csv          ← bundled sample (17×5×4×4 = 1360 rows)
├── frontend/                  ← static site, all compute in the browser
│   ├── index.html
│   ├── app.js
│   └── style.css
└── backend/                   ← FastAPI app, scipy-fit interpolators
    ├── server.py
    ├── requirements.txt
    └── static/                ← thin UI shell that talks to /api/*
        ├── index.html
        ├── app.js
        └── style.css
```

## How the data layer works (both versions)

1. **Schema detection** — every numeric column is collected. The heuristic
   "find the smallest set of low-cardinality columns whose product of
   unique-value counts equals the row count" picks the swept *input*
   columns; everything else numeric is an *output* (a metric candidate).
   The chips in the UI let you override the auto-classification.

2. **Grid build** — given chosen input columns + a chosen metric column,
   the data is packed into a regular n-D grid `(axis_1, axis_2, …, axis_k)`
   over the unique input values, with missing cells filled by linear
   interpolation along each axis.

3. **Slice** — for a chosen X / Y axis pair and a value on each slider
   axis, every cell of the resulting 2-D `(X, Y)` plane is computed by
   collapsing the slider axes one at a time with a 1-D cubic spline
   evaluated at the slider's value.

4. **Downsample** — optional LTTB along the X axis when the X axis has
   more than ~64 unique values. Peak-preserving by construction (each
   bucket keeps the point that forms the largest triangle with the
   running anchor and the next-bucket average).

## Local dev

```bash
# generate or regenerate the sample dataset
python3 generate_sample.py

# frontend version — any static server works
python3 -m http.server 8001 -d frontend
# then open http://localhost:8001/  (use-sample loads /sample_data/llc_sweep.csv,
# so serve from the repo root or copy the sample into frontend/)

# backend version
pip install -r backend/requirements.txt
uvicorn backend.server:app --host 127.0.0.1 --port 8410
# then open http://localhost:8410/
```

## Production deployment (this VPS)

- Frontend → `/var/www/plot-frontend/` served by nginx as a static site at
  `https://plot-frontend.204.168.236.48.nip.io/`
- Backend → `plot-explorer-backend.service` (systemd) listening on
  `127.0.0.1:8410`, reverse-proxied by nginx at
  `https://plot-backend.204.168.236.48.nip.io/`
- Both vhosts are gated by HTTP basic auth (`admin` / `1234` — placeholder).
- TLS via Let's Encrypt (`certbot --nginx`).

## Future hooks

The slicing + spline + slider-driver logic is the foundation the upcoming
web-based microgrid / converter simulator will plug into. Both versions
keep that logic isolated (one JS module in `frontend/app.js`, one Python
function `slice_grid` in `backend/server.py`), so it can be lifted out as
a reusable building block.
