Repository navigation
perf(sinks): a solution is a vector, not a vector beside its own index - #439
Conversation
#414 made the read-back positional — a declaration owns a contiguous run of labels, so its share of a solver vector is a slice. The frame the solvers hand back never caught up: `_labelled` still builds `arange(count)` alongside the values, as a key for the join #414 deleted. Nothing reads it. That is 8 bytes a column, held for as long as the result is: 80 MB at `dispatch/l`, 96 MB at `fleet/l`, 0.96 GB at the `2xl` rung — and the same again in rows for the duals. It is the argument #433 used to take `col` off `cols` ("the column would be the frame's own row number"), on the other side of the solve. So both solvers return a `pl.Series` of the values, and `Result` holds one. `_read_back` slices it directly instead of reaching through a column name. No benchmark: nothing here is faster, an allocation is simply not made, and its size is `count * 8` rather than something to measure. `test_a_solver_hands_back_a_vector_and_not_an_index` pins it across both solvers, because — as with #433 — the numbers are identical either way and nothing about the answer can see the difference. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 52 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughSolver primal and dual outputs now use positional Polars ChangesSolver series handoff
Estimated code review effort: 3 (Moderate) | ~20 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/lpspec/relational/engines/polars/executor.py`:
- Line 388: Validate solver primal and dual vector lengths immediately after the
solver returns and before constructing Result, requiring primal length to equal
self._n_cols and dual length to equal self._n_rows. Raise LpspecError with both
returned and expected lengths when either vector is malformed, preventing
values.slice from hiding trailing omissions. Add regression coverage for short
solver vectors.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 7a453d9b-db18-4a61-ba6c-ebf4ddae8107
📒 Files selected for processing (6)
src/lpspec/relational/engines/polars/executor.pysrc/lpspec/relational/result.pysrc/lpspec/relational/sinks/solvers/__init__.pysrc/lpspec/relational/sinks/solvers/gurobi.pysrc/lpspec/relational/sinks/solvers/highs.pytests/test_relational.py
Reading a solution back is positional, so a vector of the wrong length is an answer about a *different* model rather than a short answer about this one. Refused where the solver hands it over rather than where it is read. The objective comes back from the solver directly, so a `Result` built on a broken vector reports a plausible number and fails only if someone asks for a coordinate — and then from inside polars, about a frame the caller never asked for. A vector that is too *long* was not refused at all: every declaration's slice still lands. `gurobi._duals` already did this for its own half; this is the same check where both solvers pass through, and it is one comparison against a count. Raised by CodeRabbit on #439. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Reading a solution back is positional, so a vector of the wrong length is an answer about a *different* model rather than a short answer about this one. Refused where the solver hands it over rather than where it is read. The objective comes back from the solver directly, so a `Result` built on a broken vector reports a plausible number and fails only if someone asks for a coordinate — and then from inside polars, about a frame the caller never asked for. A vector that is too *long* was not refused at all: every declaration's slice still lands. `gurobi._duals` already did this for its own half; this is the same check where both solvers pass through, and it is one comparison against a count. Raised by CodeRabbit on #439. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Verified against the current code before changing anything. The correctness claim does not hold. That was #414's reason for attaching the slice as a column rather than The rest of it is right, for two reasons the finding did not give, so I have
Regression coverage is |
0664786 to
3a0fd97
Compare
#438 fixed a comparability bug this branch's numbers were taken across: linopy's `to_highspy()` was being timed *with* variable naming, which neither of our solver sinks does — 82% of its HiGHS hand-off. Every `highs` ratio here was therefore measured against a linopy doing work we do not do, and flatters us. #439 moved the engine again on top of that. So the tables and all three results files go back to main's, and the whole measurement is re-run from a clean tree at a commit this branch can name.
…-off #439 changed the solver's answer from a `DataFrame` beside its own index to a bare `pl.Series`, read positionally, and added `_spanning` to refuse a vector that does not span the model. Both land on `Engine` here rather than in the polars executor, because `solve` does. The check belongs to the hand-off and both engines hand off through this one, so the duckdb engine gets it without a line of its own — the third time that split has paid out on a feature written after it (gurobi, writer-by-suffix, now this). `Result._executor` stays `Engine` and its value fields become `pl.Series`: the branch's type and main's shape, which is the same resolution the last three merges needed. 780 passed on polars, 779 passed / 5 skipped on duckdb, ruff + pyrefly (0 errors) + mkdocs --strict clean.
Follow-up to #414, using #433's argument on the other side of the solve.
What
#414 made the read-back positional — a declaration owns a contiguous run of
labels, so its share of a solver vector is a slice:
The frame the solvers hand back never caught up.
_labelledstill builds anarangealongside the values, as the key for the join #414 deleted:Nothing reads it. Its docstring still explains it as "the join column".
That is 8 bytes a column, held for as long as the
Resultis — and the sameagain in rows for the duals:
This is exactly what #433 removed from
cols— "the column would be theframe's own row number" — on the build side. Same argument, other side.
How
Both solvers return a
pl.Seriesof the values;Resultholds one;_read_backslices it directly. TheSolvetype alias moves with them, whichis what caught the two return annotations I'd missed — pyrefly, not me.
No benchmark
Nothing here is faster: an allocation is simply not made, and its size is
count * 8, which is arithmetic rather than something to measure. The wall-timeeffect is whatever
np.arangeof that size costs, which is not worth claiming.Test
test_a_solver_hands_back_a_vector_and_not_an_index, over both solvers: thevalue is a
Seriesnamedvalue, spanning the model. As with #433, thenumbers are identical either way — nothing about the answer can see this
regressing, so it has to be asserted on the shape.
756 tests pass, including the Gurobi ones.
🤖 Generated with Claude Code
Summary by CodeRabbit
Improvements
Bug Fixes
Tests