Repository navigation
docs(benchmarks): the benchmark charts share one pair of axes across every model, and a box dragged on any chart zooms all of them - #2040
Merged
Conversation
…every model, and the size range can stop at five times each power of ten Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTByC5UJJWvcFj3mvmv83v
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTByC5UJJWvcFj3mvmv83v
Merging this PR will not alter performance
Comparing Footnotes
|
…ery chart at once A second slider beside the size range sets the shared value axis, with stops at one and five times each power of ten. Both sliders share one helper. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTByC5UJJWvcFj3mvmv83v
…er of ten, and its ends are the axis ends Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTByC5UJJWvcFj3mvmv83v
…art, in place of the two range sliders The box sets the size range and the value range together, or one of them where it is dragged along one side, and the table follows its size range. A double-click or the reset button zooms out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTByC5UJJWvcFj3mvmv83v
…set button always sits above the charts Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NTByC5UJJWvcFj3mvmv83v
FBumann
enabled auto-merge (squash)
October 14, 2026 13:53
FBumann
added a commit
that referenced
this pull request
Oct 14, 2026
…es as the columns and rows that reach the solver, and tables what each model declares (#2042) > **Prompt:** And should we make the model overview at the top a table with variables, constraints, dims, sparsity,... columns? > **Prompt:** Should we also add information about complex groupings? Thats what kills linopy memory... > **Prompt:** columns and rows across the page. But it needs to be clearly defined and understandable that its the count of variabels that reach the solver > **Prompt:** THis is quite some text to read before one sees results. Lets make it more compact. … It seems like this could be much more punchy too > [!NOTE] > The following content was generated by AI. The benchmarks page opens on two sentences and its charts, and its reading guide is half as long. Sizes are counted as **columns and rows**, the variables and constraints that reach the solver. Below the reading guide, a generated table says what each model is for, what it declares, and what one snapshot sends to the solver. Page order now: title, two sentences, charts, "Reading the charts", "The models" (definition, table, memory finding), "A script that solves one model", "Not measured yet", "Method". | | dispatch | network | nodal | sector | | --- | --- | --- | --- | --- | | for | bounds and one balance: the shape an array library suits best | a small PyPSA model: lines, stores, solar and wind, and pooled caps | dispatch at nodes, where a technology exists only where it is installed | each technology delivers one of five carriers, such as heat or hydrogen | | variables | p[generator] | p[bus, tech], f[line], charge[store], discharge[store], soc[store] | p[node, tech] | p[node, tech] | | constraints | power_balance | balance[bus], soc_balance[store], pool_cap[pool] | balance[node] | balance[node, carrier] | | grouped sums | — | line → bus, store → bus, bus × tech → pool | — | — | | dimensions | generator 100 | bus 50, tech 12, line 100, store 50 | node 50, tech 12 | node 50, tech 12, carrier 5 | | density | 100% | 25% | 25% | 25% | | snapshots | 2–100k | 2–25k | 2–20k | 2–20k | | columns per snapshot | 100 | 400 | 150 | 150 | | rows per snapshot | 1 | 150 | 50 | 125 | | nonzeros per snapshot | 100 | 800 | 150 | 150 | > From 100k columns up, linopy's peak memory stays within 1.3× of specsolve's on HiGHS, except on `network`, the one model that sums through a grouping. There it is 2.8× at 1M columns, 1.76 GB against 0.63 GB, and the memory budget stops linopy at that size (#2031). <details><summary>What changed</summary> `docs/about/benchmarks.md`: - The intro is two sentences, and the charts follow it. - "Reading the charts" says each rule in one or two short sentences, with a bold lead-in that carries the claim. It says the x axis counts columns, the variables that reach the solver. The round count and the projection's two edges move to "Method". The guide went from 424 to 229 words and from 25 to 17 sentences; the median sentence went from 14 to 11 words, and the sentences over 25 words from 5 to 0. - The chart's axis and tooltips say "columns", through the chart spec's `label`; the data field stays `variables`. - "The models" holds a one-sentence definition of columns and rows, the generated table, and the memory finding. The four description paragraphs are gone, replaced by the table's "for" row. - The `nodal` wording and the projection note say columns. `bench/cases.py`: `Case.about`, the one line on what each published model is for. It is set on `dispatch`, `nodal`, `network` and `sector`. `bench/report.py`: - `models()` writes the table into a new `models` fence, with one column per model and one row per property. The "for" row is `Case.about`. The declarations come from the case's `spec.yaml` (`declared()`), shown without `snapshot`, since every one runs over it. The grouped sums come from each distinct `sum(x, by=…, over=…, into=…)` (`grouped_sums()`). The dimensions and the density come from the case's ladder. The per-snapshot counts are `BASELINE`'s (specsolve's) largest measurement divided by its snapshots, which is the model as built. `_span` prints to three figures. - `fresh()` heads its columns "N columns" rather than "N variables". `bench/test_harness.py`: how a size prints, which declarations and grouped sums `dispatch` and `network` give, the table's columns and rows from the committed results (every model has a "for" line), and the fresh table's new heading. **What stays as it is.** The `bench.report` markdown tables that are not on the page (`results`, `sweeps`) and the long CSV (`bench.tidy`) still say `variables`; they are tooling output, not this page. </details> <details><summary>The memory sentence: numbers, and what they do not show</summary> linopy ÷ specsolve peak RSS, HiGHS, from #2031's committed results, at every size of 100k columns or more: | model | ratios | | --- | --- | | dispatch | 1.07× (100k), 1.00× (1M), 1.19× (10M) | | nodal | 1.14× (300k), 1.26× (3M) | | sector | 0.93× (300k), 1.17× (3M) | | network | 1.62× (100k), **2.78×** (1M: 1.76 vs 0.63 GB) | `network` is the only published model whose linopy port sums through `groupby`: lines and stores into buses, and plants into pools. `sector` sums along a dimension with a coefficient and does not group. On Gurobi the gap also grows on `dispatch` and `nodal` (2.3–2.6× at their largest size in the run behind #2039), so the sentence keeps to HiGHS. What the data does not separate is the line and store sums from the pool sums. #2024's results, before the pools (#2025), had linopy at 1.6× on `network` at 1M (1.01 vs 0.65 GB). But #2023 also landed in between, switching linopy's line and store sums from a dense incidence matrix to `groupby`. So the sentence says "a grouping", not "the pools". A `network` case without pool caps would separate the two. </details> <details><summary>Verified</summary> On `51aa16b`, with `main` (#2040) merged in: `pixi run check` (lint, format, typecheck; 5601 passed, 566 skipped, 4 xfailed), `pixi run -e docs docs-build` (strict, no issues), `tests/test_docs_site.py` and `tests/test_doc_examples.py` (123 passed, 4 skipped). `pixi run test-bench` on `ea38637` (475 passed, 15 skipped); the last commit changes only the page. `pixi run -e bench report` on the committed results writes the `models` fence and the fresh table's new heading, and nothing else. Not checked in a browser: the chart's axis now reads "columns". The label comes straight from the chart spec. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01NTByC5UJJWvcFj3mvmv83v --------- Co-authored-by: Claude <noreply@anthropic.com>
FBumann
pushed a commit
that referenced
this pull request
Oct 14, 2026
… model sizes, five times each tenfold step (#2039) The [published benchmark run](https://github.com/fluxopt/specsolve/actions/runs/38520052042) on `fbc735a`, which is `main` at #2032 plus #2038's workflow fix. The comment on #2032 compares these numbers with the committed ones; the docs preview of this pull request shows the page. > **Prompt:** I want the benchmark to get main up to date first > [!NOTE] > The following content was generated by AI. This publishes #2032's new sizes, measured on the box, to `main`. It also carries #2038's two commits, because the run was dispatched from that branch: on `main`, `published-benchmark.yml` does not parse. Merging this makes #2038 redundant. **Merge this with a merge commit, not a squash.** `bench/reproduce.py` now pins `fbc735a5b`, the commit that was measured. A merge commit puts `fbc735a` into `main`'s history. After a squash it would be reachable only through `claude/specsolve-2032-review-7hrcv0`. <details><summary>What is in the diff, and what was checked</summary> - `bench/results/*.json`, `docs/about/benchmarks.json`, `docs/about/benchmarks.md`: from the run's artifact, as the workflow commits them. With `main` merged in (#2040, #2042), `pixi run -e bench report` on these results writes the page's `models` and `fresh` tables without a change. - The linopy memory sentence under the model table (#2042) quotes this run: `network` 2.7× at 1M columns, 1.75 GB against 0.65 GB. On HiGHS from 100k columns up, every other model stays at or under 1.26×, at every size including the new 5× ones. - `bench/reproduce.py`, `bench/reproduce.py.lock`: specsolve moves from `af1dac7d3` to `fbc735a5b`, through `uv lock --script bench/reproduce.py`, as #2031 did. No other package moved. Without this, `test_the_lock_installs_what_the_published_numbers_were_taken_on` fails. - `.github/workflows/published-benchmark.yml` and `tests/test_architecture.py`: #2038's fix and its test, unchanged. - `CHANGELOG.md`: this PR's line, as #2005, #2024 and #2031 added theirs. Run on a cloud container at `afff79b`: `pixi run check` (lint, format, typecheck; 5613 passed, 566 skipped, 4 xfailed), `pixi run test-bench` (475 passed, 15 skipped), `pixi run -e docs docs-build` (strict, no issues), `tests/test_docs_site.py` and `tests/test_doc_examples.py` (123 passed, 4 skipped). Read from the run's artifacts. All 8 cases finished. No cell was killed by the memory watchdog, and no library stops at a smaller size than in #2031. pyomo on network reaches `s5`, one size further. linopy and gurobipy-loop on network still stop at `m`, by the 16 GB and 30 s budgets. The matrix floor's row leaves the "launch to exit" table, because #2034 made `test_fresh` skip both matrix arms. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01NTByC5UJJWvcFj3mvmv83v --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
The following content was generated by AI.
Every panel of the benchmark chart has the same x and y axes. Dragging a box on any panel zooms all four to the box's size range and value range, and the table to its sizes. The size slider is gone, and a Reset zoom button sits above the charts.
What changed
docs/javascripts/benchmarks.js:draw()collects every visible panel's points in view, andsharedScalesbuilds one x and one y axis from all of them. Unzoomed, they look as onmainbut span every panel.state.zoomholds the last box in data units: a size rangexand a value rangey, each null where the box left that axis alone. A drag on any panel draws the box in all four at once. The panels share axes and size, so the same pixels are the same range in each. Tooltips are hidden during the drag so they do not cover it. On release the box's corners are inverted through the panel's resolved scales, which a control that draws nothing (reader) keeps. A side under 8 px leaves that axis as it was, so a horizontal drag zooms only sizes and a vertical drag only values. A zoomed axis ends exactly at the box. Its log ticks are 1–2–5 inside the box, or d3's own log ticks where a narrow range leaves fewer than three. The table keeps only the sizes inside the box.mousedown, so neither a drag nor a double-click selects text on the page. The charts are alsouser-select: nonewith the-webkit-prefix, which Safari still needs.xs5(50k) as 100k.logTicksalways kept the top tick, so it could sit next to the tick before it: "50 s" printed over "20 s". That was already the case onmainin the network and sector panels, and shared axes would put it in every panel. The domain now runs on to the next tick at the same step.docs/about/benchmarks.md: the slider is removed. A bar above the charts holds the hint "drag on a chart to zoom every chart" and the reset button. The reading guide says the panels share their axes and how the box works.docs/stylesheets/benchmarks.css: the slider rules are removed. The box, the reset bar, the crosshair cursor, and hiding tooltips during a drag are added.touch-action: pan-ykeeps a vertical swipe scrolling the page on a phone, so there a drag zooms along x.Why not the library's own gestures. TanStack Charts 1.0 has
brushXandzoomX, both along x only. Its docs say a different gesture should be built by the application, withd3-brushor directly, and that the y-domain policy belongs to the application too. This box is about 50 lines and adds no dependency, whered3-brushwould add two.Verified, and not done
pixi run checkon208b84c: lint, format and typecheck clean; 5601 passed, 566 skipped, 4 xfailed.tests/test_docs_site.py: 10 passed.pixi run -e docs docs-build: strict, no issues.The page was then loaded in Chromium with #2039's
benchmarks.jsondropped into the built site only. This session's egress policy blockscdn.jsdelivr.net, so the scratch copies of the page loaded the same pinned packages (@tanstack/charts@1.0.0,d3-scale@4.0.2,d3-shape@3.2.0) from npm instead. The tree's import map is unchanged. Real mouse drags, each step checking that all four panels carry identical tick labels:The browser selection stays empty during and after a drag inside a plot, past the panel's edge, and from the axis labels, and after a double-click. A text selection during zooming was reported in a browser this session does not have; Chromium did not reproduce it before or after the fix.
Two console errors appear on both pages,
main's included: another blocked host, and aTypeErrorreadingoutput. They are not from this change.Not done, by decision: the zoom has no keyboard path. The size slider had one, and the library's
zoomXwould have one for x only. On a phone a drag zooms along x only, becausepan-ykeeps the page scrollable.🤖 Generated with Claude Code
https://claude.ai/code/session_01NTByC5UJJWvcFj3mvmv83v