feat(language): a where may read a predicate through a relation with at() - #634
Merged
Merged
Conversation
…at() `at(<predicate>, by=<relation>, over=<a>, into=<b>)` reads a mask over coarse coordinates at fine ones, as `at` reads an array: true where the relation has a row and the predicate holds at the coordinate it maps to, false where the relation has no row. It is the third operator that reads a predicate, beside `shift` and `count`, and is held to the rules of the expression `at`, so a bad read is refused in the same words in both. The program carries it as `PulledBackPredicate`: the operand mask, the `Direction`, and the dims after the read. `names_read` includes the relation. Exclusivity refuses it in a case `when:` as undecidable, as it refuses `shift` and `count`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HxoXSQCyrcDs6vuRk3x6tz
FBumann
pushed a commit
that referenced
this pull request
Sep 23, 2026
#634's `at()` over a predicate is ported into the resolver that builds program nodes: `PulledBackPredicate` is built from the `Direction` the relation read returns, and `pulled_back_dims` serves the expression and the predicate alike. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DwZZXWXoJfSMabvXTUndtn
This was referenced Sep 23, 2026
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Note
The following content was generated by AI.
What this changes
A
where:may read a predicate through a relation:at(<predicate>, by=, over=, into=). It is true where the relation has a row and the predicate holds at the coordinate that row maps to. Closes #258 with option 1.The rules, and where each one lives
shiftandcount. The grammar already parsedname(<predicate>, kw=…), so the grammar does not change. Resolution routesatto_pulled_back.at's rules, and uses its refusals.by,overandintomust all be written and nothing else is taken. The read lands on the relation's key, and the predicate must carry every dimension the read consumes. The dims logic moved out of_at_dimsintodimensions.pulled_back_dims, which the expression and the predicate share.a where-predicate read through 'lk' reads dims ['g'] outside the frame ['h'].at(cap, by=…) > 0already worked, because an arithmetic side of a comparison acceptsat. The predicate form reads what arithmetic cannot: definedness, abool, a variable's existence, and connectives over them.PulledBackPredicate(operand: Mask, direction: Direction, dims).names_readincludes the relation. Lowering rebuilds the operand as it does forTranslatedPredicate.when:it is refused as undecidable, asshiftandcountare. Which rows a relation maps is the data's to say.atdoes:has\_curve_{converter\_of(f)}.Guard table: each guard deleted in turn, whole suite run, tree restored
atreads a predicate…[under-a-read]attakesby,over,intoand nothing else…[a-read-naming-no-columns]at…[a-read-through-a-dim-the-predicate-lacks],…[a-read-onto-a-dim-the-predicate-carries]edge=hint is only forshifttest_a_read_given_an_edge_is_not_told_about_translationstest_a_read_landing_outside_the_frame_names_the_relationtest_a_predicate_read_through_a_relation_is_lowered_and_keeps_the_relation_in_reachnames_readtest_a_read_lands_on_the_dims_it_produces_and_reads_the_relationzonedconstraint) and the notation pagetest_a_read_through_a_relation_is_undecidable_in_a_case_whenGates, and what was not run
pytest -q -n autoruff check,ruff format --checkpyrefly checkmainin this venv (missing-import/untyped-importand their consequences)prettier,typos,reuse lintmkdocs builddocs.python.orginventory fetch fails (proxy 403)compile-tex,taplo,zizmor,pixi run ciThe gates ran from a
uvvenv, notpixi run, because pixi.sh returns 403 here.Deliberately not done
all()andany()(option 2).count(p, over=d) > 0isanyand equality with the full count isall, so they would be a second spelling. brynpickering's comment on may awhere:carry operators — relocation through a lookup, reduction over a dimension it does not span #258 saysanyis not needed here.PulledBackPredicatein its mask evaluator at its next math-spec bump.where:beside a walked link, using this.Why
fluxopt mixes converters that run on a piecewise curve with converters that do not. A
piecewise:block's rows for a converter's flows are overflow, and the mask saying which converters have a curve is overconverter. Until awhere:can read that mask throughconverter_of, the block has to build a curve for every converter. The same spelling is the one PyPSA's retired-asset port asked for in #258.🤖 Generated with Claude Code
https://claude.ai/code/session_01HxoXSQCyrcDs6vuRk3x6tz
Generated by Claude Code