Repository navigation
fix(engine): a quadratic row keeps its own pair when a row before it has no term - #2027
Conversation
…ows two causes removed Both are strict xfails against the build as it stands. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DSGmeGQfUxE4gyNQy94qG
…has no term A block that dropped a row with no term renumbered its rows and its matrix share but not its quadratic share, so a later row took an earlier row's pair and lost its own. The omitted count of that block now adds to the rows a restriction removed rather than replacing them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DSGmeGQfUxE4gyNQy94qG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DSGmeGQfUxE4gyNQy94qG
…mory, as a variable's do The block stays an in-memory frame through the build and moves once with _moved, so a read-back no longer re-runs the filter, the renumbering and the offset on every read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DSGmeGQfUxE4gyNQy94qG
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013DSGmeGQfUxE4gyNQy94qG
…e/gallant-wright-89731k
Merging this PR will improve performance by 16.23%
Performance Changes
Tip Curious why performance improved? Comment Comparing Footnotes
|
|
Note The following content was generated by AI. Folded into #2022 as a fast-forward of Generated by Claude Code |
…has no term (#2028) > **Prompt:** And what of this is a bug on main that should land there independently? > [!NOTE] > The following content was generated by AI. A quadratic constraint whose block drops a row with no term now keeps each quadratic pair on its own row. Before, a later row took an earlier row's pair and lost its own, and the wrong model solved silently. <details><summary>Cause, test, gates, what was not done</summary> **Cause.** `_drop_termless_rows` renumbered `rows` and the matrix share, but not the quadratic share. `_block` then filtered the quadratic share against the renumbered rows. So an entry kept its old row number and landed on the wrong row, or was dropped. **Test**, written first as a strict xfail and watched to fail on `af1dac7`: `test_a_quadratic_row_keeps_its_own_pair_when_a_row_before_it_is_not_built`. `k * p * p <= 10` over `g=[a,b,c]` with `k` only at `b, c`. On `main`, `qmatrix` is `[(1, 1, 1, 1.0)]` where `[(0, 1, 1, 1.0), (1, 2, 2, 4.0)]` is correct. The test also solves with Gurobi against the optimum worked out by hand. **Fix.** `_drop_termless_rows` takes the quadratic share and renumbers it with the same map. The filter in `_block` goes, because every row the quadratic share holds is in `kept`. `replace_strict` raises if that ever stops being true. **Gates**, in a uv venv on polars 2.0.0 (no pixi in this container): `ruff check .` and `ruff format --check .` at 0.16.9, `pyrefly check` on `src/` at 1.3.1 (0 errors), `pytest -n auto tests`: 5449 passed, 657 skipped, 4 xfailed, and 2 failed. The 2 failures are the `[xpress]` cases, which fail on `main` too because the xpress package is not installed here. `docs-test` was not run. **Not done.** - `_drop_termless_rows` has a second caller in `_build_constraint`, for a constraint with no linear and no quadratic term. That caller now passes `None` for the quadratic share. No test reaches that branch: the suite stays green with it replaced by a `raise`. The loader refuses a constraint with no variable, so the branch looks unreachable. #2022 removes it. - The omitted-row count of the same function is a separate bug, fixed in #2029. - #2022 rewrites this function. When it merges `main`, the resolution keeps its version and adds this renumbering; #2027 already has it there. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_013DSGmeGQfUxE4gyNQy94qG --------- Co-authored-by: Claude <noreply@anthropic.com>
… are reported together (#2029) > **Prompt:** And what of this is a bug on main that should land there independently? > [!NOTE] > The following content was generated by AI. `diagnostics().omissions` now counts every row a constraint did not build. Before, a constraint that lost rows to a restriction and also rows with no term reported only the second count. <details><summary>Cause, test, gates</summary> **Cause.** `_build_constraint` records the rows a restriction removed with `.get(name, 0) +`. `_drop_termless_rows` then wrote its own count with `=`, which replaced the first one. **Test**, written first as a strict xfail and watched to fail on `af1dac7`: `test_rows_a_restriction_removed_and_rows_with_no_terms_are_counted_together`. `p` is absent at `a`, so the restriction removes that row. `k` has no value at `b`, so that row has no term. On `main`, `rows_not_built` is 1, where 2 is correct. **Fix.** `+=` in `_drop_termless_rows`. The built model does not change; only the report does. **Gates**, in a uv venv on polars 2.0.0 (no pixi in this container): `ruff check .` and `ruff format --check .` at 0.16.9, `pyrefly check` on `src/` at 1.3.1 (0 errors), `pytest -n auto tests`: 5449 passed, 657 skipped, 4 xfailed, and 2 failed. The 2 failures are the `[xpress]` cases, which fail on `main` too because the xpress package is not installed here. `docs-test` was not run. **Not done.** #2022 rewrites this function. When it merges `main`, the resolution keeps its version with this `+=`; #2027 already has it there. </details> 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_013DSGmeGQfUxE4gyNQy94qG --- _Generated by [Claude Code](https://claude.ai/code/session_013DSGmeGQfUxE4gyNQy94qG)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Note
The following content was generated by AI.
Stacked on #2022, from a review of it. A block that drops a row with no term now renumbers its quadratic share too, and the omitted-row count adds up. Both bugs are on
mainas well.What changed, what was verified, what was not done
Commits
test— two strict xfails that fail on the perf(engine): a model with many variables and constraints builds about 15% faster #2022 head (611d936):test_a_quadratic_row_keeps_its_own_pair_when_a_row_before_it_is_not_built:k * p * p <= 10overg=[a,b,c]withkonly atb, c. Rowahas no term and is dropped. The quadratic share kept rows 1, 2 and was then filtered to the renumbered rows 0, 1, soqmatrixwas[(1, 1, 1, 1.0)]: rowcheldb's pair andbheld none.test_rows_a_restriction_removed_and_rows_with_no_terms_are_counted_together: one row removed by a restriction and one with no term reportedrows_not_built = 1, not 2._drop_termless_rowsused=where_labelleduses.get(name, 0) +.fix—_drop_termless_rowsrenumbers the quadratic share with the same map, and adds to the omitted count. The filter in_blockgoes, because every row the quadratic share holds is inkept. The markers come off, and what was wrong moves into the docstrings.chore(bench)—bench/profile_build.pyalso wrapspl.collect_all, and patcheslabels.framesrather thanlabels.frame, which the build no longer calls. Before this, the batched passes of perf(engine): a model with many variables and constraints builds about 15% faster #2022 were not timed at all. Onnetwork/xsthey now show ascollect_all of N: …entries under_build_constraints,_build_variablesandframes.chore(engine)— a constraint's labelled block stays an in-memory frame through the build and moves onto the model's rows with_moved, as a variable's does. Before, it was a lazy plan (filter,replace_strict,+ offset) that ran again on every dual or activity read.44423fb, which mergedmainand reworded perf(engine): a model with many variables and constraints builds about 15% faster #2022's changelog line. The merge touches no file undersrc/ortests/. After it, the touched test modules were re-run.Mutation table, by hand (the tree had to be committed first; restored with
git checkout --,__pycache__dropped before and after):_moved(frame, 'row', offset).lazy()→frame.lazy()+=back to=on the omitted countGates, in a uv venv on polars 2.0.0 (no pixi in this container):
ruff check .andruff format --check .at 0.16.9,pyrefly checkonsrc/at 1.3.1 (0 errors),pytest -n auto tests: 5446 passed, 657 skipped, 4 xfailed, and 2 failed. The 2 failures are the[xpress]cases, which fail the same way on611d936because the xpress package is not installed here.bench/test_harness.py: 396 passed. 1 failed (test_an_ab_refuses_to_measure_a_checkout_it_did_not_import) because specsolve is onPYTHONPATHhere and not installed.docs-testwas not run.Not done, from the same review:
_build_constraintsholds every constraint's raw term table at once, andrunnow stacks them all before one collapse. Not measured here; it needs a memory ladder atnetwork/lbefore a change.refuse_uncoveredguard still has no probe. The door refuses a null parameter value, and a missing coordinate is caught first byrefuse_short_constants, so I found no input that reaches it. It may be dead code, which is a decision for the author.declared_height,refuse_null_constantsandrefuse_short_constantsstill run once per constraint. Batching them needs its own measurement.profile_buildattributesattachtosetup: the module attribute it patches is not the one the caller imported. This is also the case onmain.Departures from Part 2: none.
🤖 Generated with Claude Code
https://claude.ai/code/session_013DSGmeGQfUxE4gyNQy94qG