Repository navigation
perf(engine): the bound attach reads the ordinal off the Enum, not a dictionary - #568
Conversation
…dictionary #511 landed the positional attach and pays for the ordinal with a Python dict built from the dimension table plus a `replace_strict` per row per dim. Since #541 that lookup is redundant for most dimensions: `encode_dimensions` makes every string dimension an `Enum` over its labels *in ordinal order*, so the physical code already is the ordinal. profiled/l build 1.240 s -> 0.901 s The two PRs were written a day apart and neither could see the other. This is the part of the idea #511 could not have: `to_physical()` where it had a dictionary, for the two of `profiled`'s three dimensions that are strings. A non-string dimension still pays the dictionary, which is built from the small side — one entry per label, not per row. No behaviour change and no new gate: the ordinal is the same integer by either route, which is what #511's oracle and shuffled-input tests already pin.
Merging this PR will regress 2 benchmarks
|
| Mode | Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|---|
| ❌ | Memory | test_emit[profiled-s-lpspec-lp] |
24 MB | 27 MB | -11.32% |
| ❌ | Memory | test_emit[sector-s-lpspec-lp] |
4.5 MB | 5 MB | -10.03% |
| ⚡ | Memory | test_emit[sector-s-lpspec-highs] |
4.9 MB | 4.1 MB | +20.14% |
| ⚡ | Memory | test_emit[transport-s-lpspec-lp] |
20.7 MB | 18.4 MB | +12.5% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing perf/bound-ordinal-from-enum (191fe2e) with main (8efd207)2
Footnotes
-
38 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
-
No successful run was found on
main(15c9776) during the generation of this report, so 8efd207 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩
Two hunks, both in `compiler.py`, and main's side wins both on substance. The attach (#511, #568) is adopted as-is — it is the better answer to the question this branch's `bounds` was already asking, and it composes: when it fires there is no join to shuffle anything, so `_build_variable`'s verify-then-sort verifies and does nothing. Its `maintain_order='left'` argument is dropped only because `parameter_join` on this branch already keeps order unconditionally, carrying the same +12 ms / ~110 ms argument in its own docstring; a wrapper to restate it would be a third copy. `_scattered` comes over including the coverage check main added — a parameter that passed the density gate but does not cover the product now raises rather than reading whatever the allocator left. `_join_mul` keeps this branch's return, `keyed` having gone with the static machinery, and the #345 fact it carried inline is already in that function's docstring here. profiled/l with the attach: 1.037 s on this branch against 0.901 s on main — the same shape of gap the `m` rungs show, and the reason #565 exists.
`cancel-in-progress` was unconditional, and on `main` that quietly punches holes in the series the metered job exists to produce. Four consecutive commits lost their walltime on 2026-08-11 — #511, #567, #568, #569, merged four minutes apart, each cancelling the one before. Only #565's own run survived, which is two engine changes older than the tip. That is not a cosmetic gap. CodSpeed stores one number per commit and compares a pull request against its own merge base, so a cancelled run is a base the next PR cannot be read against: the first one opened after that burst had nothing to compare to but a commit predating the bound attach, which would have flattered it on exactly the case the attach speeds up. On a branch the behaviour is right and stays: the newest push is the only one anybody wants a number for. The cost is stated in the file rather than hidden. A burst of N merges now takes N metered macro runs where it took one — the price of a complete series, which is the whole reason this job is fenced to merges.
`cancel-in-progress` was unconditional, and on `main` that punches holes in the series the metered job exists to produce. Four consecutive commits lost their walltime on 2026-08-11 — #511, #567, #568, #569, merged four minutes apart, each cancelling the one before. Only #565's own run survived, two engine changes older than the tip. That is not cosmetic. CodSpeed keeps one number per commit and reads a pull request against its own merge base, so a cancelled run is a base the next PR cannot be read against: the first one opened after that burst would have been compared to a commit predating the bound attach, flattering it on exactly the case the attach speeds up. Keyed on the **ref**, not the event. `main` is the only ref whose runs are a series; every other one — a pull request, or a `workflow_dispatch` CodSpeed fires at some branch to backfill — wants its newest push to supersede the run in flight, which is the behaviour that was already right. An event-name test would have caught `push` and let dispatch stop cancelling as a side effect. The cost is that a burst of N merges takes N metered runs where it took one.
`cancel-in-progress` was unconditional, and on `main` that punches holes in the series the metered job exists to produce. Four consecutive commits lost their walltime on 2026-08-11 — #511, #567, #568, #569, merged four minutes apart, each cancelling the one before. Only #565's own run survived, two engine changes older than the tip. That is not cosmetic. CodSpeed keeps one number per commit and reads a pull request against its own merge base, so a cancelled run is a base the next PR cannot be read against: the first one opened after that burst would have been compared to a commit predating the bound attach, flattering it on exactly the case the attach speeds up. Keyed on the **ref**, not the event. `main` is the only ref whose runs are a series; every other one — a pull request, or a `workflow_dispatch` CodSpeed fires at some branch to backfill — wants its newest push to supersede the run in flight, which is the behaviour that was already right. An event-name test would have caught `push` and let dispatch stop cancelling as a side effect. The cost is that a burst of N merges takes N metered runs where it took one.
…word, and every link into math-spec's docs resolves (#1740) > **Prompt:** classes of a loaded model ? What is "model" use for in our package? Is it consistent? > [!NOTE] > The following content was generated by AI. Later prompts that set scope: "Do it", then "Check if the specsolve PR has correct links! mathspec changed its docs a bit!" The glossary now defines a model as math-spec does: the problem a spec states, with no data. `specsolve.Model` is that model with data attached. All 85 links into math-spec's docs resolve on its current site. Before this PR, 20 of the 48 distinct URLs were broken. <details><summary>Method, gate output, alternatives</summary> **Glossary** (`docs/reference/glossary.md`): - The opening line now reads: "A **spec** is the model you write: the math, with no data. A **`Model`** is that model with your data attached." - The **Model** entry says the word is math-spec's, and `specsolve.Model` is that model with data attached. - The matching change in math-spec is energy-models/mathspec#697. **Links.** math-spec's `main` now holds the merged docs restructure, and #568 moved its site to zensical. I built that site locally from `main` with zensical and checked every `math-spec.readthedocs.io` URL in this repository against its pages and element ids. - **Ladder pages, 16 links:** they linked `examples/pypsa/#rung-N`. The rungs are headed `rung-N--<name>`, and rungs 10 and 12–15 have a page of their own. The names differ from this ladder's (`multi-link`, not `multilink`), so they cannot be derived. `tools/ladder.py` now holds a `CORPUS_RUNGS` table from rung to page and anchor, and the 16 pages are regenerated. The regeneration changes one line per page. - **Hand-written links:** | Old target | New target | |---|---| | `about/limits/#solver-capability` (4) | `about/what-counts-as-language/#what-each-tool-decides-for-itself` | | `reference/language/expressions/#named-expressions` (3) | `reference/language/named/#expressions` | | `reference/language/dimensions/#relations` (2), `#how-the-map-is-supplied` | `reference/language/relations/`, `#the-data-contract` | | `reference/language/absence/#a-row-with-no-variable-terms-is-not-built` | `#rows-with-no-variable-terms` | | `reference/language/piecewise/#lp-the-one-that-declares-nothing` | `#method` | | `reference/language/reading/…` (3) | `reference/reading/…` | One link's text quoted the old heading, "Capability is not the ceiling". It now reads "what each tool decides for itself". **Gates** (pixi is not installable in this session; a Python 3.12 venv with `pip install -e .` stood in): - `python -m tools.ladder --check`: exit 0. - `pytest tests/test_pypsa_ladder_page.py tests/test_docs_math.py tests/test_docs_site.py tests/test_doc_examples.py`: 200 passed, 73 skipped (the docs group is skipped in the default environment). - `ruff check` and `ruff format --check` (0.16.1) on `tools/ladder.py`: clean. - Link check: 85 links, 0 broken. - Not run: `pixi run check`, the full suite, the docs build. **Not done:** - `specsolve.Model` is not renamed. - The `CORPUS_RUNGS` table breaks again if math-spec renames a rung heading. A test that asks math-spec's built site would catch that, but this PR does not add one. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01A89tReSh8rTBG9ocy7688y --------- Co-authored-by: Claude <noreply@anthropic.com>
Nothing to revert — this is the rest of #511. 15 lines.
#511 landed the positional attach and pays for the ordinal with a Python dict built from the dimension table, plus a
replace_strictper row per dim. Since #541 that lookup is redundant for most dimensions:encode_dimensionsmakes every string dimension anEnumover its labels in ordinal order, so the physical code already is the ordinal.Measured warm, median of 5, on
mainat 8efd207 against this branch.The two PRs were written a day apart and neither could see the other — #511 predates #541 landing. This is the part of the idea it could not have:
to_physical()where it had a dictionary, for the two ofprofiled's three dimensions that are strings. A non-string dimension (snapshot, here) still pays the dictionary, which is built from the small side — one entry per label, not per row.Why there is nothing else to change
No behaviour change and no new gate. The ordinal is the same integer by either route, so #511's gate still says exactly what it said, and its three tests — the oracle, the shuffled input, both refusals — pin this unchanged.
942 passed,ruffclean,pyrefly0 errors.Context
Together with #511 this is the full effect the idea was worth:
profiled/lbuild 1.325 s joined → 0.901 s attached, a 0.68x. #511 alone got about a fifth of that, because the dictionary it needed was most of what the attach saved.Supersedes #566, which proposed the same thing as a wholesale replacement of #511 before #511 merged. That framing is obsolete now that the mechanism is on
main; this is the same measured win as a 15-line delta instead.🤖 Generated with Claude Code