Skip to content

feat(language): a where counts the coordinates a predicate admits, and reads one at a neighbour - #1706

Closed
FBumann wants to merge 1 commit into
claude/lpspec-589-592-update-qa51o2from
claude/lpspec-589-592-update-qa51o2-count
Closed

FBumann wants to merge 1 commit into
claude/lpspec-589-592-update-qa51o2from
claude/lpspec-589-592-update-qa51o2-count

Conversation

@FBumann

@FBumann FBumann commented Sep 21, 2026

Copy link
Copy Markdown
Collaborator

Prompt: "update lpspec to 589 and another stacked pr to 592"

Note

The following content was generated by AI.

The pin follows math-spec #592. count(<predicate>, over=) answers a number and shift(<predicate>, along=, offset=) answers a predicate, on both lanes — the two atoms #582 needs a curve to state as language.

Stacked on #1705 (math-spec #589, assumptions: at the door).

What this changes
where: "count(offer > 0, over=snapshot) >= 3"
where: "count(offer > 0 AND NOT shift(offer > 0, along=snapshot, offset=1), over=snapshot) == 1"

The relational lane reads each of the two predicates over its own coordinate product, through the same masked the outer mask came through, and joins one column back onto the carrier:

  • a count is pl.len() over the coordinates the predicate admits, grouped by what is left when over is reduced away, joined left. A coordinate the predicate admits nowhere has no row to join, so the column is null and fill_null(0) makes it the zero it is — a count is a number of coordinates, and "none" is an answer rather than a gap that drops the row.
  • a shift carries the operand's truth set forward by offset positions in the axis' own ordinal order and joins a boolean column. A position the move runs off the end of has no row to carry, joins null, and reads false through falsy_if_null — which is what a missing row already means in a mask, and why the predicate form states no edge=.

The eager lane is two lines each: the predicate summed over over and compared, and DataArray.shift(fill_value=False). Absence is already false in an evaluated mask on that lane, so both fall out of what it does with a bool array.

Nothing new is refused here: a keyword the language does not admit (by=, within=, edge='wrap'), a fractional bound, or a parameter on the right is refused at load, before a plan exists.

Why the math-spec pin is a merge

#589 and #592 are siblings on #566 in math-spec, so no commit there carries both — and this branch's base consumes #589. The pin is therefore energy-models/math-spec@2a9d0d5, claude/lpspec-589-592-update-qa51o2 there: #592's branch merged into #589's, which is the merge commit #592's own description says whichever lands second will need. Its conflicts are the typesetter walk and the goldens; #589 folds six comparison members into one AlignedComparison printed through sides(), so #592's re-stated branches for four of them go and its two new ones stay. That merge's suite is 1426 passed, 16 skipped.

When both land upstream the pin becomes an ordinary tag again.

Gates

pixi is refused by this environment's egress proxy, so the gates ran from a uv environment on Python 3.12. Same departure as #1705, for the same reason, and the same single checkout rather than a worktree per topic.

gate result
pytest -q -n auto 4047 passed, 320 skipped, 1 xfailed (base: 4026 passed)
ruff check . clean
ruff format --check . 330 files already formatted
pyrefly check 0 errors
docs-build, test-bench, sweep not run — no docs toolchain here, and nothing touches bench/
Mutation table

Five mutations, none of them a deletion tools/mutate.py can express — three changed constants and two changed reductions — so they were taken by hand with the tool's three precautions: a committed tree, git checkout -- to restore, and __pycache__ dropped on both sides of each. The tree was clean after the run, and that was checked rather than assumed.

mutation result run
the count's zero for a coordinate with no admitted row (predicates.py, fill_null(0) dropped) caught 1 failed, 41 passed
the shift's direction along the axis (predicates.py, + offset → - offset) caught 1 failed, 41 passed
the vacated position's false in the eager lane (where.py, fill_value=False dropped) caught 3 failed, 39 passed
the count reducing only the dim it counts along (where.py, sum(dim=over) → sum()) caught 6 failed, 36 passed
the count's own product, over plus what is left (predicates.py, the remaining dims dropped) caught 8 failed, 34 passed

Each ran tests/test_where_count.py tests/test_resolution_parity.py.

Coverage, and what was deliberately left out
  • tests/test_where_count.py asserts the coordinates built, not their count: a predicate that inverted its sense would keep the complement, which is the same size on a symmetric case. Six claims — a count per remaining coordinate, a coordinate admitted nowhere counting zero, a vacated shift reading false, a count over a translated predicate counting run starts, a count over a dim the masked declaration does not span, and the four relations a count may be compared by.
  • ACCEPTED in the parity sweep gains three shapes, so both lanes build the same model from a count over either dimension and from a shifted predicate, and test_every_resolved_predicate_is_parity_tested names a test for both new members.
  • docs/about/linopy.md gains the two rows for what the eager lane reads them as.
  • Not done, on purpose: no piecewise: block emits an assumptions: entry using either atom — that is math-spec A modeller should not have to shrink the model to solve it #582's, on top of refactor: one answer per question #589. Mask joining the golden node census is upstream's. And a count comparison keeps its own typesetter branch rather than joining AlignedComparison in the pinned merge, so a lone count in an assumptions: entry prints unaligned; the right home for that is refactor: cut the prose, keep the facts #592 or refactor: one answer per question #589 upstream, not a consumer.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TWu2WSipjg5T3khNLfTXDK


Generated by Claude Code

…d reads one at a neighbour

The pin follows math-spec #592, merged onto #589 so the stack keeps the
assumptions this branch's base consumes: `count(<predicate>, over=)` answers a
number and `shift(<predicate>, along=, offset=)` answers a predicate, on both
lanes.

The relational lane reads the counted predicate over its own product and joins
the total back, so a coordinate it admits nowhere counts zero rather than
losing its row; a shifted predicate carries its truth set forward along the
axis, and a position the move runs off has no row to join, which is the false
the language gives a vacated coordinate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TWu2WSipjg5T3khNLfTXDK
@read-the-docs-community

Copy link
Copy Markdown

@codspeed

codspeed Bot commented Sep 21, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 24 untouched benchmarks
⏩ 58 skipped benchmarks1


Comparing claude/lpspec-589-592-update-qa51o2-count (959ce51) with claude/lpspec-589-592-update-qa51o2 (345b8c6)

Open in CodSpeed

Footnotes

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

@FBumann FBumann closed this Sep 22, 2026
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