Skip to content

refactor(program): a comparison of expressions is one node before and after lowering - #631

Merged
FBumann merged 1 commit into
claude/blissful-heisenberg-z5rpwo-typesetfrom
claude/blissful-heisenberg-z5rpwo-arith
Sep 23, 2026
Merged

FBumann merged 1 commit into
claude/blissful-heisenberg-z5rpwo-typesetfrom
claude/blissful-heisenberg-z5rpwo-arith

Conversation

@FBumann

@FBumann FBumann commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

Prompt: Is ArithmeticComparison needed in lpspec? … Do both

Note

The following content was generated by AI.

program.ArithmeticComparison is deleted. It was ExpressionComparison with its sides still in the core syntax tree. ExpressionComparison[Side] is now one class: its sides are syntax-tree nodes before lowering and Expressions after. A consumer meets one comparison class. Stacked on #629.

Method, gate output, alternatives

What this changes

  • ExpressionComparison takes its side type as a type parameter. Resolution builds ExpressionComparison[ArithmeticNode]. Lowering rebuilds it with Expression sides.
  • ArithmeticComparison leaves program.__all__, Predicate and TypedPredicate.
  • The dim rules, the exclusivity check and the typesetter already matched both classes the same way. They now match one.
  • docs/reference/reading.md drops the sentence "one member of the Predicate union never reaches you".

Breaking for consumers

program.ArithmeticComparison no longer exists. There is no alias, per the alpha-stream rule. fluxopt/lpspec has three references, and each only refuses the class:

  • src/lpspec/linopy/where.py:125-127: delete the ArithmeticComparison arm.
  • src/lpspec/relational/engines/polars/predicates.py:232-234: delete the ArithmeticComparison arm.
  • tests/test_resolution_parity.py:91-94,152: delete NEVER_LOWERED. expected becomes set(get_args(program.Predicate)).

This session could not open that PR, because push access to lpspec was not granted.

Guards removed

These two AssertionErrors can no longer tell the two states apart. Nothing reaches either one today:

  • The typesetter's "a lowered comparison reached the typesetter".
  • Mask.names_read's "a resolved mask is asked what it reads".

ExpressionComparison is out of the golden test's UNRESOLVED set, because the walk now renders it.

Why a type parameter and not a private class

Mask.atoms and Mask.dims run on resolved masks: in the dim rules, exclusivity and validation. So program has to know the resolved comparison either way. One class with a side type is one concept. A private second class is two.

A type-parameter default ([Side = Expression]) needs Python 3.13, and the package supports 3.12. So the two union aliases that serve as isinstance targets carry a pyrefly: ignore[implicit-any-type-argument], with the reason given inline.

Gates

  • pixi run lint: green.
  • pixi run test: 1560 passed.
  • docs-build and compile-tex did not run: the session proxy blocks their downloads.

🤖 Generated with Claude Code

https://claude.ai/code/session_017XwKgY5wZXv1bKgkfCW2q1


Generated by Claude Code

@read-the-docs-community

read-the-docs-community Bot commented Sep 22, 2026 •

Copy link
Copy Markdown

… after lowering

ArithmeticComparison was ExpressionComparison with its sides still in the
core syntax tree. ExpressionComparison now takes its side type as a
parameter, and program.ArithmeticComparison is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017XwKgY5wZXv1bKgkfCW2q1
@FBumann
FBumann force-pushed the claude/blissful-heisenberg-z5rpwo-arith branch from b279760 to 9a5d38d Compare September 23, 2026 11:25
@FBumann
FBumann removed this pull request from stack #637 September 23, 2026 11:30
@FBumann
FBumann added this pull request to stack #644 September 23, 2026 11:32
@FBumann
FBumann merged commit f723602 into main Sep 23, 2026
5 checks passed
FBumann added a commit to fluxopt/specsolve that referenced this pull request Sep 23, 2026
…at() (#1718)

> **Prompt:** Update lpspec to the latest mathspec release (119)

> [!NOTE]
> The following content was generated by AI.

The pin moves from math-spec alpha.116 to alpha.119. Both lanes now
build the new `at(<predicate>, by=, over=, into=)` in a `where:`
(energy-models/mathspec#634), and they agree on the result. A
coordinate with no relation row reads false. 70 insertions, 34
deletions.

<details><summary>What moved</summary>

* **`PulledBackPredicate`**: the relational lane walks the coordinates
the operand admits through `walk_join`, as an expression's `at` does.
The linopy lane reads the evaluated mask through `operator_at`. Before
this change, both lanes hit `assert_never`.
* **`PiecewiseExpansionError` is gone upstream**
(energy-models/mathspec#638). A piecewise block now raises
`DimensionError`, so `lps.PiecewiseExpansionError` goes too, with no
alias. `test_api`, `test_architecture` and `test_piecewise` follow, and
so does `docs/reference/api.md`.
* **`ArithmeticComparison` is gone upstream**
(energy-models/mathspec#631). The two lanes' branches for it were never
reached, so they go. `NEVER_LOWERED` in `test_resolution_parity` goes
with them.
* **Tests**: `test_a_where_reads_a_relation` gains 2 cases and
`test_a_relation_where_agrees_with_the_oracle` gains 3: a total
relation, a partial one, a negation and a conjunction.
`COVERED_ELSEWHERE` names the oracle test for `PulledBackPredicate`.
Before the implementation, the coverage guard failed on it.
* **Upstream now refuses `sum()` over a scalar**, so the
carried-parameter probe in `test_strategy` reads `soc_initial` bare. It
still asserts the refusal names "carried".
* `uv.lock` is relocked. Only the math-spec entry changed.

</details>

<details><summary>Mutation table</summary>

Run by hand on the committed tree. Each file was restored through `git
checkout --` and `__pycache__` was dropped. The runs cover
`test_label_coords.py` and `test_resolution_parity.py`.

| Mutation | Result |
| --- | --- |
| linopy: a missing relation row reads true (`fillna(True)`) | caught, 1
failed |
| polars: a missing row reads true (`fill_null(True)`) | caught, 5
failed |
| polars: the operand's mask is ignored (`masked(..., None)`) | caught,
5 failed |

</details>

<details><summary>Gate</summary>

```
ruff check .            clean
ruff format --check .   327 files already formatted
pyrefly check           0 errors (20 suppressed)
pytest -q -n auto       3796 passed, 403 skipped, 1 xfailed, 11 failed
```

The run used `uv` with the `[linopy]` extra, not pixi. The xfail is the
known `osemosys_utopia` / #894 case. Of the 11 failures, one was the
`test_strategy` probe, which is fixed in the second commit. The other 10
are the gurobi and xpress parametrizations of `test_diagnostics` and
`TestThePositionalHandoff`. They raise `ModuleNotFoundError` because
neither package is installed here, and they fail the same way on
`origin/main`.

**Not run**: `docs-build`, `docs-test`, `test-floors`, `test-bench`, the
gurobi and xpress sinks. I did not regenerate the gallery pages. The
typesetter changed upstream (energy-models/mathspec#629), but the doc
tests passed.

</details>

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01LzN7uYEvHW9dDGCmtAzbkD

---
_Generated by [Claude
Code](https://claude.ai/code/session_01LzN7uYEvHW9dDGCmtAzbkD)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
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