Repository navigation
refactor(engine): ask the data, not the declarations - #520
Merged
Merged
Conversation
The relational lane carried three pieces of machinery whose only job was to
avoid work: a `keyed`/`label_dims`/`mapping` triple on every term fragment so
the assembly could skip its terminal aggregate, three routes through the
labeller depending on how much of the coordinate product a mask left, and an
inner-vs-left join choice for the names a where is certain of. Each was correct
and each was a second thing a reader had to hold.
They are gone. The matrix and the objective always aggregate — the one place
duplicates were ever meant to collapse — labels are always the sorted product
numbered from the next free index, and a mask always left-joins. Order is now
established where it is needed (`sort('var_label')`, `sort('row')`) rather than
inherited from a cross-join fold and verified after the fact.
The LP writer's constraint section follows: the interleaving integer-key sort
becomes a group into a list per row and an explode, with the two sorts that
make the bytes reproducible stated on either side of the join.
Same models, same labels, same bytes.
Three smaller pieces of the same kind of machinery. `ModelTables` now promises its sinks a matrix sorted by `(row, col)`, which the engine establishes once; the solver hand-off and the LP writer read that instead of each re-sorting a frame that already had the order. `check_one_row_per_coordinate` asked a cheap boolean first and only paid for the group-by on the path about to raise, and the single-valued check rode in the caller's aggregate through a scratch-column protocol between two modules. Both now just ask the question they mean. Also: `plan.children` never descended into `At`, so `divisor_parameters` could not name a divisor under one; `Builtin.value_kwargs` was a field no helper declared; `_helper_sum` had three branches doing one thing.
Every constraint's share of the matrix now arrives from one concat of term pieces, so it always carries `coeff` — the column-presence test in front of the null check was a leftover from the branch that could return early.
The comment #517 rewrote sat above `_constraint_blocks`'s presort of the matrix. This branch deletes that function: the constraint section groups terms per row instead of interleaving them by sorting one integer key, and the `(row, col)` order it needs is now `ModelTables`' contract rather than a local sort. So the question #517 left open — whether the presort pays for itself — has no code left to attach to here.
Merging this PR will degrade performance by 7.27%
Warning Please fix the performance issues or acknowledge them on CodSpeed. Performance Changes
Tip Investigate this regression by commenting Comparing Footnotes
|
The objective now refuses an undefined divisor the way constraints do, on both lanes — its aggregate was the one place a null coefficient could still be read as a zero. The duplicate-row check's count column is named '#rows', which no dim can be, where 'n' crashed every build of a model with a dim of that name. The polars floor rises to 1.36, where `explode(empty_as_null=)` exists — the LP writer relies on it. The constraint-section docstring now cites the within-group order guarantee #109 rests on, and a test docstring still naming the deleted Labeller says labels.frame. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
FBumann
marked this pull request as draft
August 9, 2026 20:02
The divisor-null count rides the aggregating group_by instead of forcing the unaggregated stack eager first: each constraint share and the objective are one lazy streaming pipeline again, and the check reads a count the aggregate already carried. And the assembly-time sort is gone — each share leaves sorted and owns the next run of rows, so ascending shares concatenate into an ascending whole, and the sort was copying the model's largest frame at the peak of the build to reorder nothing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 9, 2026
* perf(engine): validation asks its cheap questions in one pass Data validation probes a parameter once — any duplicate coordinate, any unknown label — and pays the passes that name offenders only on a path about to raise; a healthy build of a large model no longer runs the group_by that was the single most expensive step of its build. And an explicit dimension index is collected once, with the single-valued check and the coordinate grouping both reading that one collect, instead of each re-scanning a source that may be a file (#273). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(lp): the writer's peak is bounded again (#530) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…rged PR The stack merged out of order — #529's squash (carrying #530) reached perf/one-pass-assembly after #528 had already merged that branch here — so the validation and writer recoveries were stranded on a branch with no open PR. This merge brings them down; the assembly half is already here as #528's squash, which is content-identical. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…inery Two collisions, both spelling against deletion. The Model rename wins where both sides touched a line (MathSchema is gone everywhere else), and the branch's tests win where main's still probed _needs_aggregate and may_share_a_column — machinery this branch removes, its question now answered by the aggregate always running. The _parameter_names and _variable_names wrappers main added have no caller left on this branch's check_divisors_cover, so they go too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 10, 2026
The explanatory inline comment is retired: what narrated code is gone, what carried a fact — a measured number, an issue reference, a library subtlety — now lives in the nearest docstring, and the one expression complex enough to deserve a name (the null-counting matrix aggregate, written twice) is the _summed_shares helper. Kept inline: pragmas with their reasons, #: attribute docs on constants, and section banners. CLAUDE.md states the rule so it holds for new code too. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… never repair it (#534) * style: inline comments become docstrings, and one helper The explanatory inline comment is retired: what narrated code is gone, what carried a fact — a measured number, an issue reference, a library subtlety — now lives in the nearest docstring, and the one expression complex enough to deserve a name (the null-counting matrix aggregate, written twice) is the _summed_shares helper. Kept inline: pragmas with their reasons, #: attribute docs on constants, and section banners. CLAUDE.md states the rule so it holds for new code too. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * perf(engine): ask the data, not the declarations — and produce order, never repair it Interleaved A/B against main (l rungs) had the branch at 2.1-4.7x on build. Four recoveries, none of them the deleted machinery: - the coordinate product folds in reverse again (#433): the streaming engine walks a cross join right-major, so the product arrives in label order and everything downstream can read position instead of sorting. - labelling is one path: row-major position by arithmetic, a linear is_sorted verify, a single-key sort only when a join actually lost the order. The unconditional multi-key sort was 0.26 s of a 0.73 s build. - order-bearing joins say so: maintain_order='left' on bounds and mask joins (+12 ms at 10M rows, against ~110 ms to sort afterwards), 'left_right' on a lone term's placement. - the matrix aggregate runs only when a linear adjacency probe finds a repeated cell, and the writer stamps set_sorted('row') — the order ModelTables already promises — so the streaming engine groups adjacent runs instead of hashing: 0.14 s against 0.89 s at 10M nonzeros. After: transport/l 1.10x, storage/l ~1.3x, dispatch/l 2.2x (its vacuous where pays one single-key sort main's factored-prefix machinery avoided; that trade is the point of #520 and is documented, not resurrected). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Conflicts were #533 (a Model validates itself) against this branch's docstring sweep and the aggregate-avoidance deletion: - language/piecewise.py: keep #533's known_variables threading and model_validate-with-context; keep this branch's docstrings. - linopy/__init__.py: take main — extend's explicit validate_expressions call is gone, coords variable keeps main's existing_coords name. - engines/polars/compiler.py: merge both docstring halves (this branch's phrasing, main's measured numbers). - engines/polars/executor.py: main only trimmed docstrings of _needs_aggregate/_in_row_order/_has_repeated_entry; this branch deletes the functions, so the deletion stands. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three findings from the #533 review, addressed here since the merge brought that code onto this branch: - extend() now threads its known_variables into expand_piecewise, so an extension whose piecewise links borrow a model variable validates against the namespace it will run in instead of being refused with "'p' not found". Regression test proves the failure without the fix. - A validated Model keeps the expansion its own validation built, keyed by the namespace it was expanded against; expand_piecewise returns it rather than building — and fully re-validating — a second Model. Every piecewise consumer (build, extend, lowering, typeset) drops from two validation passes to one. - The SPEC piecewise example is checked again: doctest skip becomes wrap=piecewise, so a typo'd key in the example fails CI instead of shipping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A four-angle cleanup pass (reuse, simplification, efficiency, altitude) over this branch's diff: - TermFragment.variable had lost its last reader when may_share_a_column went; the field, its threading through four constructors, and the test asserting it go too. - _group_fragment and _at_fragment had converged to the same four lines read in opposite directions; one _remap_fragment states the join once. - parameter_join's maintain_order knob had one setting at every call site; the join now always keeps order and says why. - The VariableDefined branch spells its null-reads-as-false through _falsy_if_null like every other mask site. - _build_variable projects (var_label, lb, ub) before collecting, so the dim strings and bound aliases stay in the lazy pipeline instead of materialising to be dropped — and its verify-then-sort goes through labels.in_position_order rather than restating it. - The matrix's sorted flag is stamped where the order is established (executor assembly), not by one sink; the LP writer's per-chunk full-matrix filter becomes ModelTables.matrix_block, the binary-search slice row_blocks already used, so both chunked readers share one answer to which entries a row range owns. - The piecewise expansion cache is owned entirely by expand_piecewise — write, read, and key in one module, misses repopulate — with Model keeping only the slot. - _build_constraint's docstring described the intermediate always- aggregate design; it now states the shipped probe-then-collapse. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…e key per line Codspeed on the branch still showed -30..-60% against main: memory on small models, wall on the lp sink. Interleaved A/B (median of 5 paired subprocess runs) reproduced both and profiling put them in two places. Labelling a variable whose mask cannot see its leading dims walked the full coordinate product through the mask join: +177 MB transient and 17 ms for sector/m's one masked variable, where main's factored path paid for the suffix. That split returns, as one guarded branch of the one labels.frame path: a mask that reads none of the leading dims removes the same coordinates under every one of their values, so the survivors are a rectangle — rank the suffix set, cross-join the head product on the left (the streaming engine emits the right side fastest, which is label order), and the label is arithmetic again. Restrictions and masks that read the first dim still count the slow way. The writer's group-into-list-and-explode paid per list element: 3x main on sector/m emit. The one-key-per-line union sort returns — header, placeholder, terms, footer each pick a slot in a chunk-relative integer key and one single-column sort interleaves them — now fed by ModelTables.matrix_block, so the terms arrive as a pre-sorted slice rather than a per-chunk scan. A/B after (branch/main wall, peak): sector-m-lp 1.01/1.08 was 1.81/1.31; sector-s-lp 1.09/1.02; nodal-m-lp 0.98/1.03; fleet-m-lp 1.09/0.98; storage-m-lp 1.09/1.00; sector-m-highs 1.06/1.09. dispatch-m-highs stays 1.24 wall: its mask reads both dims, so no prefix is free and the counted path pays its sort — the documented residual of deleting the static machinery, now confined to that class. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One conflict: model.py's pydantic import block — this branch adds PrivateAttr (the expansion-cache slot), #538 on main adds ValidationError. Both stay. #542's extend fix and #541's Enum dims auto-merged; the branch's expansion memoisation, which main's #542 does not carry, is kept. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…vives The mask path carried a caveat: joins keep the frame's order, except a value join over the lazy cross product, which is why a mask reading the first dim forced labelling's verify into a real sort (dispatch 1.24x). The caveat dies instead of being documented harder. A predicate is a function of only the dims it reads, so it is evaluated once over their product and the full product is semi-joined against the truth set — the mask's parameter columns never touch the full product, and a semi-join leaves left order alone, so 'every frame is produced in order; sorts are verified fallbacks' now holds without exceptions. Pointwise predicates (dimension comparisons, constants) stay a plain filter, detected by _predicate handing the carrier back unchanged. predicate_dims moves from labels.py to the compiler: the semi-join and the factored prefix now read one answer to 'which dims does a where read'. Interleaved A/B vs main after: dispatch-m-highs 1.19x wall / 0.97x peak (was 1.24/1.02), dispatch-m-lp 1.13/1.00, sector-m-lp 0.99/0.92, sector-s-lp 1.05/0.99, fleet-m-lp 1.06/0.92 — peaks now at or below main everywhere measured; dispatch's residual is the per-declaration probe passes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
One conflict, in _translate_fragment: #546's nested-shift presence fix (presence keeps its own dims; a shift along a dim it is silent about widens it first) lands over this branch's pre-fix one-liner. The fix wins; its inline commentary moves into _translate_fragment's docstring per this branch's convention. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s waiting for The job's own comment deferred l until a series showed whether m resolves what we want to see. #520 answered: fixed per-declaration overhead reads as -5..-10% at m and amortizes to within ±4% at l, where peak memory — the product claim — moves the other way (0.88-0.99 of main). The two rungs disagree, which is what the metered rung exists to catch. Timeout widened to fit the second rung. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…inty returns Two reversals, one reachable only behind the other, measured together by a six-arm interleaved A/B (median of paired ratios vs main): sector/m 1.075 -> 1.009, the only delta in the corpus above the harness's ~5% noise floor. Credit to a parallel review session for the attribution and the patches; docstrings and their numbers are this commit's. A mask that reads every frame dim has a truth set as wide as the product itself, so the semi-join built the coordinate product twice to save no width — sector's balance mask is exactly that shape and paid 6.6% of the m pipeline for it. It returns to the plain filter, which keeps order the same way. Behind that, the one-strategy uniformity stops paying: on the direct path the rows a left join keeps are rows the filter then drops, so a name the mask is certain of is inner-joined and a certain variable semi-joined again (_certain_parameters, back with its Or/Not asymmetry stated). Behind a semi-join's key product the two strategies measured the same, which is why uniformity looked free when it landed. parameter_join takes how/maintain_order again; only the bounds ask for order (cols is positional), every other consumer verifies where it reads. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Only a mask or a restriction leaves gaps in the row-major positions. With neither, every coordinate survives and start + position is the label, so the unmasked path stops materialising a position column only to renumber it with a row index and drop it — one arithmetic expression, as before the paths merged, but inside the single labels.frame. Phase-measured, not wall-measured: ~3 ms per unmasked declaration at the m rungs (fleet carries 19 and no where at all), 1.5-2% of build at the l rungs — consistent in sign across cases but below the end-to-end noise floor of the local harness. Landed for the shape as much as the number: the renumber pass existing only where a row was dropped is the honest form of the one-rule labeller. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The one conflict was the Builtin docstring: main's wording still counted a value_kwargs field both sides have deleted, this branch's names the one kind that exists. plan.children merged to the identical one-word fix. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This was referenced Aug 11, 2026
6dc3c24 and 725cd0e are performance-only branches: delete either and every one of the 941 tests still passes, every model still solves to the same answer, and sector quietly gives back the 6.6% the guard recovered. The fallbacks are semantically identical by construction — a full-width mask filters where it would have semi-joined, a certain name is left-joined where it would have inner-joined — so nothing that reads an answer can hold them. Verified as pins rather than as decoration: both new assertions fail at 43a7109 (the branch before the recovery) and pass at 90d87b3. test_compiler.py already exists for exactly this ("the only place query *shape* is asserted", and every property there "can regress while the whole suite still passes"), so its invariant list gains the two entries rather than a new file appearing beside it. test_a_parameter_predicate_needs_a_left_join is renamed: the predicate it compiles is one the mask is certain of, so 6dc3c24 made it an inner join and the name had stopped being true. Its left-join half moves to the same predicate under an Or, where certainty does not survive. Not pinned, for want of an observable: 3a0619e's objective probe. keep_order threads maintain_order into _join_mul, which polars does not print in a query plan, so deleting the linear branch and reverting to n_unique on every build is invisible to a shape assertion as well as to an answer. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 11, 2026
FBumann
added a commit
that referenced
this pull request
Aug 11, 2026
#574 asked whether the assembly probes could be skipped by propagating the sortedness labelling already establishes. They cannot: polars clears the sorted flag on both `concat` and `join(maintain_order='left')`, which are the two operations between labelling and assembly, and re-asserting it after a join is the move `set_sorted` cannot check and the verify exists to avoid. Measuring that premise found the real cost, which is not the comparison but the chunking. A streaming collect returns its morsels as chunks -- 104 for dispatch/l's 10M-entry share, 136 for transport/l's 12.6M -- and `shift(1)` pays at every boundary. `is_sorted` does not: 1.18 ms chunked against 1.14 ms flat. In situ the probe cost 28 ms and 35 ms where a synthetic contiguous frame of the same shape said 5 ms, and that 6x gap is the whole finding. Rechunk, then probe: 12 ms and 19 ms including the rechunk. A single combined key probed with `is_sorted` was also tried and is worse (21 ms and 26 ms) -- building the key gathers across the same chunks. The copy is not new work. #550 already requires the matrix to be contiguous or every block's `to_numpy` is a gather, so the assembly rechunks regardless; this moves that copy earlier, and where one declaration owns the whole matrix the later rechunk becomes a no-op. Interleaved A/B against the branch head, 7 rounds, min of 3, two independent runs: dispatch/l wall 0.960 / 0.927 peak 0.944 / 0.942 (-142 MB) transport/l wall 0.960 / 0.978 peak 0.969 / 0.993 fleet/l wall 0.963 / 0.960 peak 0.993 / 0.996 sector/m wall 1.007 peak 1.000 fleet is the shape that does not benefit in peak, as expected: 19 declarations mean 19 small rechunks and a `_stack` that re-fragments to 19 chunks, so its later rechunk still copies. The objective's stack takes the same `concat().collect()` and is deliberately left alone: rechunking it too measured no consistent wall gain and cost dispatch 2% of peak (0.963 against 0.942). Like the two branches #573 pins, this one is invisible to the suite -- a rechunk changes no answer -- so the docstring carries what a future reader needs to not delete the rechunk. The A/B ratios stay here and in the PR: they compare against a branch head, which stops naming anything once #520 merges. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
#574 asked whether the assembly probes could be skipped by propagating the sortedness labelling already establishes. They cannot: polars clears the sorted flag on both `concat` and `join(maintain_order='left')`, which are the two operations between labelling and assembly, and re-asserting it after a join is the move `set_sorted` cannot check and the verify exists to avoid. Measuring that premise found the real cost, which is not the comparison but the chunking. A streaming collect returns its morsels as chunks -- 104 for dispatch/l's 10M-entry share, 136 for transport/l's 12.6M -- and `shift(1)` pays at every boundary. `is_sorted` does not: 1.18 ms chunked against 1.14 ms flat. In situ the probe cost 28 ms and 35 ms where a synthetic contiguous frame of the same shape said 5 ms, and that 6x gap is the whole finding. Rechunk, then probe: 12 ms and 19 ms including the rechunk. A single combined key probed with `is_sorted` was also tried and is worse (21 ms and 26 ms) -- building the key gathers across the same chunks. The copy is not new work. #550 already requires the matrix to be contiguous or every block's `to_numpy` is a gather, so the assembly rechunks regardless; this moves that copy earlier, and where one declaration owns the whole matrix the later rechunk becomes a no-op. Interleaved A/B against the branch head, 7 rounds, min of 3, two independent runs: dispatch/l wall 0.960 / 0.927 peak 0.944 / 0.942 (-142 MB) transport/l wall 0.960 / 0.978 peak 0.969 / 0.993 fleet/l wall 0.963 / 0.960 peak 0.993 / 0.996 sector/m wall 1.007 peak 1.000 fleet is the shape that does not benefit in peak, as expected: 19 declarations mean 19 small rechunks and a `_stack` that re-fragments to 19 chunks, so its later rechunk still copies. The objective's stack takes the same `concat().collect()` and is deliberately left alone: rechunking it too measured no consistent wall gain and cost dispatch 2% of peak (0.963 against 0.942). Like the two branches #573 pins, this one is invisible to the suite -- a rechunk changes no answer -- so the docstring carries what a future reader needs to not delete the rechunk. The A/B ratios stay here and in the PR: they compare against a branch head, which stops naming anything once #520 merges. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
… twins (#575) No behavior change — every query leaves expression-identical — only shape: - keep_order stops being threaded: product and quotient never read self, so they move into expression()'s walk as closures and capture the flag where it is stated. No signature carries it anymore. - labels.frame's two numbering branches become one skeleton with a single 'dropped' decision, so 'renumber only when a row was dropped' is now the code rather than a docstring claim about two code paths. - The full-width-mask guard turns positive: the semi-join states when it applies and the filter is the fall-through, instead of three negated disjuncts guarding an early return. - The objective probe's nested branches flatten to one 'repeated' answer. - The certainty story is told once, in _predicate, where the decision is made; parameter_join points at it instead of retelling it. Net -22 lines on the PR's diff against main. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
FBumann
added a commit
that referenced
this pull request
Aug 11, 2026
…red (#577) A release commit is a strict duplicate. release-please writes CHANGELOG.md and .release-please-manifest.json and touches no source -- the git tag is the version -- so the metered macro runner reproduces the previous commit's number at full price. Over the last 40 merges that is 17 of them, 43%, every one a release. The rule is what cannot change what the benchmark executes, which is narrower than "not code" and deliberately excludes entries that look safe: pyproject.toml and uv.lock decide which polars is installed and a floor moving under us is what this series exists to catch, bench/ is the workload, and this file is the instrument. Checked against the last 40 merges: nothing touching src/, bench/, pyproject.toml or uv.lock is skipped. The concurrency note calls a missing number a hole the next pull request falls into, and it is right in general. It does not apply to these paths: the hole sits at a commit whose executable tree is identical to its parent, so the fallback compares against the same code it would have. The cost is that a pull request merged after one is footnoted `unexpected-base`, which reads like a fault -- it misled this session for one round trip on #520 -- so the comment says so where somebody will find it. Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 11, 2026
Merged
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.
Read the performance section before the rest. This PR opened as a simplification, the simplification was measured, and most of it was reversed. What is left is a smaller and different change than the title suggests, and it still costs emit time at the
mrungs.25 files, +1179/−1385.
What it actually changes
The engine reads the data instead of reasoning about the declarations.
TermFragmentused to carrykeyed,label_dimsandmapping— a log of every operator that had moved its labels — so_needs_aggregateandmay_share_a_columncould decide statically whether a constraint's terminalSUM(coeff) GROUP BY row, colhad anything to collapse, down to "is there a line whose two ends are one bus?". That is gone._matrix_shareasks the assembled share two linear probes instead — is it ordered, does a cell repeat — and sorts or aggregates only on a yes. The objective asksn_unique() == height. Same answers, ~250 lines less of the subtlest reasoning in the tree, and nothing that has to be kept in step with how a fragment was reshaped.Labellerbecomeslabels.frame, a function with no state, and the label order is verified rather than re-established:in_position_orderis one linearis_sortedagainst a single witness column, and all three orderings in the lane go through it. The factored-prefix path survives as a guarded branch of that one function rather than a second method — a mask that cannot see the leading dims leaves the survivors a rectangle, and materialising the full product instead cost +177 MB onsector/m.Masks restrict by semi-join. A joining predicate is evaluated over the product of only the dims it reads, and the full product is semi-joined against the truth set — so the mask's parameter columns never touch the full product, and left order survives, which is what keeps labelling's verify a verify. A pointwise predicate stays a plain filter.
Smaller, self-contained:
_remap_fragmentwritten once forsum(group_by=)and its adjointat; the shift's presence rule namedtravelled_presence;ModelTables._spansso thebudget=Nonerule is spelled once;Model._in_our_treeso the two validating doors cannot answer differently;data_validationasking the question it means instead of a cheap probe then a naming pass.Three fixes, not refactors:
plan.childrennever descended intoAt, sodivisor_parameterscould not name a divisor under one;Builtin.value_kwargswas a field no helper declared;relational/__init__listed anengine.pythat does not exist.One dependency claim withdrawn:
polars>=1.36was raised forexplode(empty_as_null=)in a rendering that got reverted, and is back at1.30.Performance — measured at both rungs, and it does not amortize
Do not merge this on the current numbers without deciding to accept them.
CodSpeed, against a base that now contains #511 and #568,
test_emit(which times build and emit together):mdispatch-m-highs−16.9%ldispatch-l-highs−13.0%plus two memory regressions at
s:transport-s-lp−13.6%,sector-s-lp−8.2%.The amortization argument is refuted.
0f3614cadded thelrung on the claim that fixed per-declaration overhead "reads as −5..−10% atmand amortizes to within ±4% atl", measured locally. With a reallbaseline in the series that is not what happens:llands at −6.8% to −13%, and onsectorandtransportit is worse thanm, not better. Whatever the cost is, it scales with the model rather than sitting on top of it.That local measurement was mine and it was wrong — a single machine, one arm at a time, against a tree that has since taken three engine changes. The metered series is the instrument that settles this and it disagrees.
A third measurement agrees with CodSpeed rather than with the local one:
profiled/lbuild alone is 1.037 s on this branch against 0.901 s onmain, warm, median of five.What that means for this PR
The change buys readability — the static avoid-work machinery is gone, replaced by probes that ask the data — and it costs roughly a tenth of the pipeline across the whole ladder, at every rung, on both sinks. That is the trade, stated without the amortization that was supposed to soften it.
The suspects, in the order I would look:
_matrix_share's two linear probes per constraint share; labelling's_row_majorplus thein_position_orderverify per declaration; and the semi-join mask path.fleetcarries 19 declarations per coordinate and is −10.5%,dispatchcarries 3 and is −16.9%, so "fixed per declaration" does not fit the shape either — which is exactly why it needs profiling rather than another argument.I have not found it yet. Until it is found, the honest options are to accept ~10%, or to hold the PR.
What this PR is no longer
The original description claimed the aggregate always runs, that three labelling paths collapsed to one, and that every mask name is left-joined. All three were true when written and none is true now — measurement reversed them, and the recovery reached the same capabilities by other means. The commit history keeps that arc; this description does not.
The
style: inline comments become docstringssweep that was the bulk of the churn is out, landed separately as #560.Verification
938 passed, 5 skipped;
ruff check,ruff format --check,pyreflystrict (0 errors),mkdocs build --strict.test_a_constraint_row_left_with_no_variablesis no longer xfail — #561 resolved what the two lanes were disagreeing about.🤖 Generated with Claude Code