feat(language): an expression the math never reads may be nonlinear, a reported quantity - #287
feat(language): an expression the math never reads may be nonlinear, a reported quantity#287FabianHofmann wants to merge 8 commits into
Conversation
Documentation build overview
38 files changed ·
|
Note The following content was generated by AI. Read from the consumer seat — lpspec would implement this in both of its lanes, and these are the places where the PR as it stands would send two consumers to different answers, or cost a reader a correction. 1 and 2 are the language's, and only this repo can answer them; 3–5 are corrections. 1. Division by a solved zero is undefined
2. Whether a reduction still skips absent slotsThe new section in
The first clause says a null propagates through the arithmetic; the second says the sum is taken over the rows that were built. The math reconciles them elsewhere — a reduction skips absent slots rather than propagating them — but a postsolve body is arithmetic and reductions, and an implementer reading only "no value at that coordinate" will carry the null through Worth the sentence because an absence rule that no model exercises passes CI on both sides of a disagreement. 3. The list of lifted rules is short by two
Lifting both is right — they exist because a quotient compiles to multiplication by one reciprocal factor, and a postsolve body compiles to nothing that ingests it. But all three places enumerate only the degree-2 cap, the one-sum-factor rule and the variable-divisor and variable-exponent bans. For a consumer these two are the expensive ones, not an afterthought: they are what stops a postsolve body from being evaluated as a list of additive fragments, and force an operand to be materialised before the 4.
|
Note The following content was generated by AI. Recommendation: drop the The evidence that settled it is on the consumer side: at lpspec's API boundary the two designs are indistinguishable — the first thing a consumer does with the two mappings is union them.
What this deletes from this PR: the section key, the What the section bought and unification gives up: the file declares the lifecycle instead of the reader deriving it, and an unreferenced typo ( The API comparison in code, and methodEvaluating a statistic after a solve (lpspec's linopy lane; the relational lane has the same shape at # postsolve-section design
body = program.named_expressions.get(name) or program.postsolve.get(name)
value = _eval(body, EvaluationContext(built, sources))
# unified design
body = program.named_expressions[name]
value = _eval(body, EvaluationContext(built, sources))The union line is the whole difference, and the flat namespace guarantees it is always safe — which is another way of saying the two mappings were one mapping. The if isinstance(node, program.Dual):
return built.dual[node.constraint] # relational lane: join the dual vector over the constraint's foreachA worked example exercising everything: congestion rent as The one API where the section would help does not exist yet — a presolve accessor returning a linear form: # sectioned: membership is the guarantee
return _eval(program.named_expressions[name], ctx)
# unified: one grade check, message owned by the language
if program.grade(name) != 'presolve':
raise LanguageError(program.presolve_refusal(name))
return _eval(program.named_expressions[name], ctx)Method: read |
|
@FabianHofmann Sorry for the churn. I think its very important to get this right before continueing here. My goal is to make both the expression parser and where parser fully internal. And only expose the resolved nodes etc, like we do with all other operations! |
5915622 to
db1517a
Compare
Note The following content was generated by AI. Thanks — the review predated the rework to grade-by-body, and the unified design plus the 1. Division by a solved zero — resolved. The value is absent, the null a masked row leaves, not 2. Reduction vs propagation — resolved, as the sentence you asked for. 3. Lifted rules short by two — resolved. 5. 4. Verified: |
…a post-solve quantity An entry's expanded body decides its grade: math-grade when affine and dual()-free, post-solve-grade otherwise (a variable divisor or exponent, a product above degree 2, or a dual() call). The degree rules fire where the math reads the inlined body, not at declaration. dual(constraint) is a constraint's shadow price, legal only in an expressions: entry.
bcffa91 to
1409110
Compare
Note The following content was generated by AI. Addressed — the branch was rebased onto current Concretely, on this direction:
Verified: |
…ided at the math's degree-2 ceiling Rename post-solve -> reported across code, docs, the typeset "Reported quantities" section, and tests. Grade at ceiling=2 so a degree-2 entry the objective reads is math grade, not reported, restoring the invariant that the math never reads a reported entry.
Note The following content was generated by AI. F2 is fixed in Also in Correction to the earlier five-points reply (#issuecomment-5505336628). It stated the reported entries are carried as Verified: |
…ex and quantifier A reported entry read the frame from foreach, empty for a non-cased entry, so a body over free dims lost its left-side subscript and the quantifier. Also folds in a test for the uncalled-macro dual() guard and threads the caller's Namespace through reported_expressions rather than rebuilding it three times per render.
…on are covered, and a dead ceiling default is gone The private _check_expression carried a ceiling parameter that only its degree-checking callers read, and both passed 2; drop it and inline the value. Add typeset tests for the --no-reported off-path and for dual() taking the constraint symbol when a variable shares the name.
|
@FBumann let me know if you are happy with the design now. I think this is a clean way to separate math and interpreter responisibilities |
FBumann
left a comment
There was a problem hiding this comment.
Could we maybe add some sort of flag to the class Expression and do the logic from that? And store some info about the expression (uses_dual, degree etc) directly on it?
This would also allow the downstream engine to acton on that!
| *, | ||
| comparison: bool, | ||
| ceiling: int, | ||
| ceiling: int = 1, |
There was a problem hiding this comment.
Lets do ceiling: int | None, with None meaning no check!
| coordinate like any other parameter arithmetic. | ||
| """``base ** exponent``. | ||
|
|
||
| In the math a solver ingests, both sides are variable-free: the language |
There was a problem hiding this comment.
I would like to not have these If, then things to be explained here, neither to need to explain them here.
We should have some sort of flag on the Node instead. But not sure.
|
Sth like |
…, rather than grading its shape `Program.named_expressions` carries an `ExpressionDeclaration` whose `in_math` is true where the objective or a constraint inlines the entry, decided by expansion and held nowhere else. The shape predicate `is_reported_grade` goes with it: degree is checked where the math reads, and the typeset "Reported quantities" section lists the entries nothing reads. `dual(c)` resolves to its own `DualNode` leaf rather than a call carrying a `ConstraintNode`, so every walk has one arm for it and no guard against the argument reaching it alone. `_check_expression` takes `ceiling: int | None` in place of a second flag. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnCcEf1NShkTYBdbN4wrc7
…a plain set named for what it holds Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnCcEf1NShkTYBdbN4wrc7
…the leaves Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TnCcEf1NShkTYBdbN4wrc7
…th edges _check_expression now passes ceiling to degree.check_expression instead of hard-coding 2, so the int|None carries a real degree rather than a disguised bool; the constraint and objective sites state ceiling=2, and the dead default is gone. Restore the type on the lowered-expressions dict. Add lowering tests for the in_math chain, a macro formal sharing an entry's name, and a dual entry.
Note
The following content was generated by AI.
One
expressions:section, nopostsolve:. An entry is reported when the math never reads it — nothing in the objective or a constraint inlines its body, directly or through another entry, a macro, or a piecewise link. A reported entry may be nonlinear and prints in a flag-gated "Reported quantities" section assymbol = body. Folds in thedual(constraint)builtin from #290, which can now close.Usage, not shape. Whether an entry is reported is decided by use, not by the shape of its body.
expansion.read_by_the_math(schema)expands the objective and every constraint and collects each named entry inlined on the way — through another entry, a macro's call-by-value substitution, or a piecewise link's emitted constraint. Lowering stores the answer on eachExpressionDeclaration.in_math, and the typesetter asks the same function for its "Reported quantities" section, so the program and the printed page cannot disagree about which entries the math reads. An entry is degree-checked only where the math reads its inlined body — every constraint, the objective, eachwhere, bound and piecewise link — at that position's ceiling, so a nonlinear entry named by a constraint is refused there, not banned at declaration."Reported", not "post-solve". "Reported" is what is true of
lcoe,dual(...), and a data-only case like(1 + rate) ** periodalike: a quantity read back, never fed to a solve. A reported quantity need not need a solve, so "post-solve" named a stage it does not have.The dual.
dual(constraint)is a constraint's shadow price, a number only a solve produces. Resolution types it to a singleDualNodeleaf, and placement is one guard —degree.calls_dual, run on the resolved tree at every math-building position, refusing a dual there with a message that names the rewrite. Theexpressions:declaration is the one position that does not run it — that is where a dual belongs. Its only exposed form is the resolvedprogram.Dual; the parsed node is exported nowhere.Verified:
pixi run cigreen atf8eca1c— lint, full test suite,mkdocs build --strict, 28 TeX documents compiled. Schema unchanged (neither the reported flag nordualadds a key); schema and golden regenerated with no drift.Why the flag is usage, not a shape grade
An earlier revision decided "reported" by grading the body's shape — a variable divisor or exponent, a product above degree 2, or a
dual()call — at the math's degree-2 ceiling. The grade answered "could the math read this body", which is not the section's claim:reported.md's invariant is that the math never reads a reported entry. So an affine entry nothing references graded math-grade and printed nowhere, though nothing reads it.in_mathanswers the question the section actually asks, and the predicate is one a consumer can reach (a flag on the lowered declaration) rather than one taking a private parser node no second reader could call. The consequence a reader sees: an affine entry nothing references now prints under Reported quantities, and a symbol table may rename it.Rebase — onto the internalized parsers
The work sits on
main's parser internalization (#342 + #351): the dual edits_expression_parser.py(private), resolution ismain's_Resolver, the dim rule is a branch in_dims_calldispatch, and the typeset dual leaf and section builder usemain's walk API.dualresolving to aDualNodeleaf letsdimensions,lowering, the typesetter,carries_variableand the template-name check each drop a "a bare constraint reference cannot reach here" guard and thedualspecial case in call dispatch, keeping one arm.Tests and departures
test_a_nonlinear_entry_is_refused_where_the_math_reads_it(constraint, objective), a piecewise-link case atceiling=1, and a bound/where naming no expression.TestDualcovers the placement refusals, including a dual smuggled through a macro or an entry, now on the resolved tree.in_mathacross reference shapes intest_lowering.py: a constraint, the objective, through another entry, through a macro, unreferenced, and referenced only by a reported entry; plus the chain (intermediate and leaf both in the math), a macro formal sharing an entry's name (the argument decides), and an entry reading a dual (reported, aDualleaf).calls_dualunit cases intest_degree.py.--no-reported) and only when earned; adual(c)takes the constraint symbol, upright, even where a variable shares the name (add adualfunction to access duals from constraints in a solved problem #74).in_mathusage flag that replaced it — rather than introducingin_mathin one commit. Kept because squash-merge lands only the net diff, and the review that drove the switch is worth the trail.