Skip to content

feat(api): a named expression is read back at any degree and may read a constraint's dual - #1529

Merged
FBumann merged 1 commit into
mainfrom
claude/math-spec-pr-395-review-29jhxj
Sep 8, 2026
Merged

FBumann merged 1 commit into
mainfrom
claude/math-spec-pr-395-review-29jhxj

Conversation

@FBumann

@FBumann FBumann commented Sep 8, 2026

Copy link
Copy Markdown
Collaborator

Prompt: lets update lpspec to the latest mathspec versions. ONe version at a time, if we need to change things! … And add the dual + non linear expression stuff!

Note

The following content was generated by AI.

math-spec alpha.75 (#395, #398, #399) holds an expressions: entry the math never reads to no degree and lets it call dual(c). Both lanes now read such an entry at the solution rather than build it: every leaf is a number after a solve, so a product of two variables, a variable under a power or a division by one is arithmetic over values, and dual(c) is the constraint's row duals.

  • Relational lane. The compiler a read builds is handed the solve (Solution): a variable compiles to its primal and dual(c) to the constraint's rows beside its share of the dual vector, both as const fragments carrying the presence the term would. A divisor or a power's side that adds is added up per coordinate first, null where no piece has a value, so a hole is still reported rather than divided by. The reader then sums const fragments only; the old term-join path is gone.
  • Eager lane. The same walker with solved=True reads .solution and .dual and does xarray arithmetic, so it no longer builds a linopy expression to take .solution off.
  • A dual on a model declaring integrality is refused on both lanes, per entry, with the sentence Result.dual gives; every other entry still reads. On the linopy lane this is decided by the declaration, because HiGHS hands a MIP back with a zero dual on every row and linopy stores it — a number that means nothing, not an absence.
  • Program.named_expressions now carries ExpressionDeclaration; the three readers go through .expression. The gallery walkthrough golden gains dual in the built-in list.

The Dual row joins the plan table and the linopy translation table, and the guide's degree bullet says where an entry is checked. One boundary-test row moved: an expression-only degree-2 entry is inside the language now, so the row asserts the rule that remains — a dual smuggled into the objective through an entry is refused where the math reads it.

Verified, and the mutation table

In a uv environment at Python 3.12 (pixi is not on the box), on 70b8c80:

pytest -q -n auto     3600 passed, 251 skipped, 1 xfailed   (main at alpha.74: 3590 passed; the ten are new)
ruff 0.16.1 check     All checks passed
ruff format --check   302 files already formatted
pyrefly 1.2.0         0 errors, package and linopy reference scripts

The bare bump produced 24 failures and 9 errors: every named_expressions[name] reader falling through assert_never on the new declaration type, the two census tests asking both lanes to dispatch on Dual, the two doc-table tests, and the one boundary row the language change invalidated.

tools/mutate.py, full suite per row, on a13c377:

mutation result
a dual read on a solve that left none is refused (compiler.py:592-594) caught
a divisor that adds keeps its hole (compiler.py:640-641) caught
a product carries the presence of both factors (fragments.py:380) caught
the linopy lane refuses a dual on a model declaring integrality (builder.py:328-332) caught

Tree clean after the run. One mutation the tool cannot express, taken by hand with the same precautions (git checkout -- restore, __pycache__ dropped): dropping presences=a.presences + b.presences from join_pow fails test_a_product_is_absent_where_either_factor_is[a-power] — caught. The two assert solution is not None arms on the Dual node are invariants of a checked plan (the language keeps a dual out of the math), not guards, and are not in the table.

Not run: pixi run check itself, test-bench, the sweeps. uv.lock regenerated with uv 0.12.10 for the three-line diff.

What can be provided, and how that is decided

The language decides what an entry may contain; the engine can answer any of it because after a solve every leaf is a number — a parameter, a constant, a variable's primal, a constraint's dual. The one thing a solve may not leave is duals, so that is the one refusal, per entry. Mechanically: test_architecture asks both lanes to dispatch on every node of program.ExpressionNode, so a node math-spec adds fails the census with the pin rather than with the first model that uses it; test_docs_site asks the plan table for a row per node; and test_the_two_lanes_agree_on_a_named_expression now carries a degree-two entry and a dual entry.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SsHvKFYPt5AnN5caUPfFra


Generated by Claude Code

… a constraint's dual

math-spec alpha.75 holds an expressions: entry the math never reads to no
degree and lets it call dual(c). Both lanes now read such an entry at the
solution: the relational compiler, handed the solve, turns a variable into
its primal and dual(c) into the constraint's row duals as const fragments,
so a product of two variables, a variable under a power and a division by
one are arithmetic over values; the linopy lane evaluates the same plan on
.solution and .dual arrays. A divisor or a power's side that adds is added
up per coordinate first, null where no piece has a value, so a hole is
still reported. A dual on a model declaring integrality is refused on both
lanes with the sentence Result.dual gives, and every other entry still
reads. Program.named_expressions now carries ExpressionDeclaration, read
through .expression.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SsHvKFYPt5AnN5caUPfFra
@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 lpspec | 🛠️ Build #34448400 | 📁 Comparing a13c377 against latest (70b8c80)

  🔍 Preview build  

3 files changed
± guide/index.html
± about/architecture/index.html
± about/linopy/index.html

@codspeed

codspeed Bot commented Sep 8, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 16 untouched benchmarks
⏩ 42 skipped benchmarks1


Comparing claude/math-spec-pr-395-review-29jhxj (a13c377) with main (70b8c80)

Open in CodSpeed

Footnotes

  1. 42 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. ↩

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants