An example client of specsolve archives: a static React app, with charts by TanStack Charts, that reads the archives the specsolve showcase publishes, and on one page solves the showcase's model again in the browser.
Live: https://fluxopt.github.io/specsolve-app/
specsolve-showcase specsolve-app
showcase-grid ──▶ grid/<point>/ ──▶ Pages: archive/grid/ ──HTTP──▶ hyparquet ──▶ React + TanStack Charts
(specsolve) (parquet) (stacked per path)
The showcase's own site is one client of those archives. This app is another, built with a different stack, to make the point the showcase makes: the contract is the archive directory, not a library. No page that reads an answer imports specsolve, and nothing here is shared with the showcase except the URL.
The showcase's deploy publishes every directory of archives at
https://fluxopt.github.io/specsolve-showcase/archive/<directory>/. Beside the
archives it writes each path stacked across them, because a static host cannot
answer a glob: archive/grid/answer/primal/total.parquet is
read_parquet('grid/*/answer/primal/total.parquet'). Every row names its
archive in specsolve_run.
Four pages, each reading a handful of stacked files with hyparquet and joining them in TypeScript:
- What if (
src/grid.ts) reads seven files of the what-if grid, about 23 KB. Every chart updates in place and animates from the old point to the new one when a slider moves; selecting a heatmap cell moves the sliders. - Dispatch (
src/dispatch.ts) reads five files of the four scenarios: hourly output, the balance constraint's dual, the load, the day weights and the emission rates. It plays the pathway period by period, morphing the stacked output from one fleet to the next, and one shared cursor ties the hour axis of the output and price charts to the price-duration curve beside them. - Risk (
src/hedge.ts) reads ten files of the hedge, about 28 KB: one investment period planned against 60 futures at once, at ten weights on the worst tenth of them, beside the plan made for the average future and the plans made with perfect foresight. It draws what each plan builds, the trade between expected cost and the cost in the worst futures, what every future costs, and the weight the plan puts on each, read off the duals of the tail rows. Each plan's hours come from that plan's own archive, about 70 KB, read when the plan is first shown, because the stacked file of every plan's hours holds over a million rows. The tail is computed from each future's cost rather than read off the model'scvar, which is arbitrary at a weight of zero. - Explore (
src/explore.ts) knows nothing about the model. It reads the stackedcatalog.parquetof either directory, lists every quantity, and opens any of them in a TanStack Table to filter, sort and group, with the URL of the file it read and the one line that reads it from DuckDB or polars.
Solve (src/Solve.tsx) asks a scenario's question again.
It reads the scenario's own inputs out of its published archive (spec.yaml,
axis.json and every sources/<name>.parquet its catalog.parquet lists),
shows every parameter in a table whose values you can type over or scale a
filtered set of rows at once (src/DataEditor.tsx),
shows the spec in a code editor (src/SpecEditor.tsx)
that completes, explains and checks it against mathspec's own schema
(src/specSchema.ts) as you type and colours each name
in an expression by what the file declares it as
(src/specHighlight.ts), completing and explaining
those names from the same table (src/specNames.ts), with
the model as
math beside it (src/MathPreview.tsx), and
solves the pathway with specsolve and HiGHS in a Web Worker
(src/solver.worker.ts). The worker loads
Pyodide from its CDN at the version package.json pins,
installs specsolve from PyPI, and runs src/solve.py. The page
puts what comes back beside the published answer.
Pyodide's polars is 1.33.1, the newest polars that builds for the browser, and
it reads and writes no parquet. specsolve 0.9 runs on that polars. So the page
reads the sources with hyparquet and passes them to Python as columns, the
worker passes the archive's axis.json to solve_over as it is, carry
included, and the answer comes back as columns.
Solve and download the archive saves the run as a specsolve archive, a zip
that sps.load_archive and DuckDB read. specsolve writes it through pyarrow
where polars has no parquet, so the worker loads pyarrow the first time it is
asked for one, about 15 MB more.
The math is mathspec's to_markdown of the spec, run in the worker and
rendered with KaTeX. It needs Python, so it starts after a solve or when asked,
never on a visit alone, and then follows each edit.
The first solve downloads about 38 MB of Python and packages. After two solves the tab holds about 780 MB, and about 1.1 GB once pyarrow is loaded, so a desktop browser is fine and a phone may reload the page.
src/mathspec.schema.json is mathspec's schema/mathspec.schema.json at
v0.2.1, the mathspec the worker installs. A specsolve that requires a newer
mathspec takes the schema from that tag.
Requires Node 22. Against the published archives:
npm ci
npm run devAgainst archives you solved yourself, from a checkout of the showcase beside this one:
cd ../specsolve-showcase
uv run showcase-grid --runs grid
uv run showcase-hedge --runs hedge
uv run python -m tools.publish_archive ../specsolve-app/public/archive grid hedge
cd ../specsolve-app
VITE_ARCHIVE=./archive npm run devnpm run build type-checks and builds to dist/. A push to main publishes
it to GitHub Pages.