Skip to content

feat(program): the windowing verdict says which way a row reads and what only data or the driver can resolve - #370

Closed
FBumann wants to merge 44 commits into
claude/separable-alongfrom
feat/separability-a-driver-can-act-on
Closed

FBumann wants to merge 44 commits into
claude/separable-alongfrom
feat/separability-a-driver-can-act-on

Conversation

@FBumann

@FBumann FBumann commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: "Can you just do a stacked PR fixing those?" — the three points lpspec's driver raised against #248.

Note

The following content was generated by AI.

Three changes to Separability, read from the consumer that will call it (lpspec.strategy.EachWindow). The shape and placement of #248 are unchanged.

  1. halo becomes behind and ahead. A translation reads behind for a positive offset (t - offset) and ahead for a negative one; a trailing window reads behind by width - 1. A rolling-horizon driver supplies the two differently — lookahead is rows it solves and does not keep, history is rows it carries from the window before — and abs(reach) could not say which it was in.
  2. Reasons are typed by what a driver can do about them, as three mappings rather than one. coupled (a sum in a constraint, a grouping, a wrap, a set, an at()) no window satisfies. undecided names the parameter or lookup whose values set the reach — a named offset or width, a partition — and a driver holding the data computes it; the same shape as coupled refused PyPSA's link delay, which rolls routinely. restarts names a position() a window restarts, which a rolling horizon seeding its opening state means. windowable is false while anything is coupled or undecided; a restart does not count against it.
  3. The claim is narrowed to what the walk certifies: every row builds complete inside some window. It cannot say the windowed answer is the whole-horizon one — a store carried over one row windows cleanly and a rolling solve of it is still a different answer — and the docstrings and reading.md said it could.

Two more, from the follow-up questions ("can we say what would make it windowable?", "isn't separability also what a scenario sweep asks?"):

  1. Each coupled entry names the change that would lift it, after a dash: a horizon total becomes a rolling sum_back(within=n), a wrap an opening-state seed at position(t) == 0, a grouping is windowed along the dimension it groups into, a set needs a window holding it whole. Named and not applied — every one changes what the model means, and math-spec's Advice already takes that line. at() moves from coupled to undecided, naming the lookup whose values set its reach.
  2. independent answers the other driver: whether each coordinate builds on its own — windowable, reading nothing behind or ahead, counting no position. A scenario sweep asks it before solving one coordinate per slice, and it is what licenses solving slices in any order or at once. A restart counts against it, unlike for windowable: with one coordinate per slice a position() holds everywhere, which changes what the mask means.
Verified

pixi run lint (lefthook: prettier, ruff), pixi run typecheck (0 errors), pixi run test: 931 passed. reading.md gains one executed line (the remedy) and the page test's claim count follows. Not run: mkdocs build --strict, compile-tex.

Coverage moved: the six coupling cases split into two structural (coupled), three data-decided (undecided, asserting the name), and one restart (asserting it stays windowable); a negative-offset case, the at() case, four independent cases and the remedy wording are new.

🤖 Generated with Claude Code

https://claude.ai/code/session_01FPBsP3UM6esCBREVmjMEhZ

FBumann and others added 30 commits August 31, 2026 20:08
…re that release does not have (#305)

`effect_limit` and priced effects are not in pypsa 1.3.0 — no Effect component,
no effect_limit in global_constraints, no effects module (checked against the
1.3.0 tag). The row tracked a later PyPSA feature in a table pinned to 1.3.0,
where it read as an open milestone gap. It is out of scope by version, not by
decision, so it leaves the table rather than becoming an `out` row.


Claude-Session: https://claude.ai/code/session_016jk5LAMCoiD39q4Xz4AMVw

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
… data-prep row used to answer for it (#272)

Claude-Session: https://claude.ai/code/session_01YbYPoWmvwpkRmYeFvgW5jm

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
…e, not each consumer's to re-derive (#313)

* feat(program): the conjuncts of a where mask are the program's to give, not each consumer's to re-derive

A program's resolved `where` is a WhereNode, and the AND spine the grammar
left-folds nests, so a consumer wanting the predicates one at a time had to
flatten it and decide where the flatten stops. `conjuncts()` gives them: it
flattens the AND spine and stops at an OR or a NOT, so the split has one home
and two consumers cannot disagree on what a conjunct is.

This is the language-side half of #312 — the model giving the conjuncts. The
sweep that asks which conjunct the corpus never makes false needs lpspec's
per-rung record and belongs there, beside the block-level parity.coverage() it
extends.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SV5dfu6ZGBULW21x8GcN2g

* test(parser): rename a conjuncts case id the typo check read as a misspelling

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SV5dfu6ZGBULW21x8GcN2g

* style: reword the conjuncts docstrings the typo check read as a misspelling

'ANDs together' in program.conjuncts and reading.md is 'joins with AND' now —
typos reads 'ANDs' as 'and', and the word is rewordable, so it is reworded
rather than added to the extend-words list kept for words that are not.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jk5LAMCoiD39q4Xz4AMVw

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
…being one, and claims neither linearity nor a storage format (#315)

Three claims a consumer reads and acts on. `Program` carries no data, so it is
a description rather than the thing; the objective has taken degree 2 since the
quadratic stack, so `linear` had been false for a while; and `over named tidy
tables` named a storage format the language does not decide — one program feeds
a backend keeping frames and one keeping dense arrays alike.


Claude-Session: https://claude.ai/code/session_01TZbCoSM1Ah7YWg6K4Ps5sU

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
…rry their dims, and the where grammar is package-private (#327)

Claude-Session: https://claude.ai/code/session_01D59sEp5d7CecJAwrzACbRh

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
…ss (#329)

Every expression node is frozen like the where and program vocabularies
already were; a call's args are a tuple and its kwargs a copied read-only
view, excluded from the hash because a mapping has none.

Refs #326.


Claude-Session: https://claude.ai/code/session_01Q9qaMS43V6yaNApJPmuWtM

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
… the grammar's complaint (#332)

The predictable mistakes — a strict or chained comparison, !=, a lone =,
^ for power — are diagnosed from the token at the parse failure, with the
grammar's own message kept beneath.

Refs #326.


Claude-Session: https://claude.ai/code/session_01Q9qaMS43V6yaNApJPmuWtM

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
…sed, and the refusal says what actually breaks (#336)

The rule landed in #327 with no home on the reference page a file's author
reads, and its wording framed the cases as ordered.

Cases are kept apart by proof rather than ranked, so an always-true arm is
not one that shadows the arms under it — it is one no other arm can be
proved apart from, leaving `otherwise:` nothing. The refusal says that, out
of a named helper whose docstring carries the reason.

`expressions.md` gains the rule beside the other two the cases are held
to, quoting the refusal verbatim, and says why a declaration's `where:` is
not held to it. The where-string table's literal row gains the double
negation the fold also cancels.


Claude-Session: https://claude.ai/code/session_01BJfy8K5XfxLmX5Tq5YW71b

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
…rrier it actually has (#334)

Three sentences the Mask move left pointing at things that are no longer
there:

- `exclusivity.py` cited `dimensions.py` and the typesetter for its
  unresolved-node guard; both walks are gone, and the live one is
  `program._atoms`, which `Mask` exhausts at construction.
- `where_of` claimed a Mask is the only shape a resolved where travels
  in past resolution; a case arm's `when` travels as a bare node through
  the AST, and lowering is what wraps it.
- `_arm_mask` returns that node, not a Mask, so it is `_arm_when`.


Claude-Session: https://claude.ai/code/session_01BJfy8K5XfxLmX5Tq5YW71b

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…that walk (#338)

`Mask.__post_init__` already walks the whole tree — building the atom tuple
is how it refuses an unresolved leaf — and then threw the tuple away, so
`atoms`, `dims` and `names_read` each walked it again on every access.

`atoms` becomes a `cached_property`, filled by the walk construction takes
anyway, and `dims` and `names_read` read it instead of re-walking the root.
One home for "the mask's leaves", and the construction cost is unchanged.

The fold has to run before that walk: `x AND False` folds to `False` and
drops `x`'s leaf, so a tuple held from before it would answer with a leaf
the root no longer carries. `test_a_held_leaf_walk_is_taken_after_the_fold_absorbed_a_branch`
fails when the two lines are swapped.


Claude-Session: https://claude.ai/code/session_01BJfy8K5XfxLmX5Tq5YW71b

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…ping one in a Mask is a type error (#341)

`parse_where` cast its result to `WhereNode` and its docstring explained
that the annotation was the type the tree has only *after* resolution. Two
more docstrings then leaned on that lie to justify a runtime guard.

The return type is now `WhereNode | UnresolvedWhereNode`, which resolution
already accepted. One site stores such a tree in a field typed as resolved
— `_parse_cased`, whose docstring says why — and carries the pragma.

The guard in `Mask.__post_init__` stays and its reason narrows: not that
the annotation over-claims, but that a consumer runs none of our checks
over its own code. `program._atoms`, `Mask` and the test that pins the
refusal say that instead.


Claude-Session: https://claude.ai/code/session_01BJfy8K5XfxLmX5Tq5YW71b

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
…refused, rather than loading unreferenceable (#340)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
…sion fails CI instead of scrolling past (#325)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…nchecked Any cannot reach a caller (#330)

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n replaying the whole history (#344)

Claude-Session: https://claude.ai/code/session_01Dnv7pdeso3eJNpnikcvVs6

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…me the values they can be, rather than being a string (#345)

Claude-Session: https://claude.ai/code/session_01Hdxai54hc4LwxFQBqMM8jF

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
… C connective habits (#346)

& and | and ~ and ! (doubled or not) and a lone = are diagnosed from the
token at the parse failure, beside the grammar's own complaint — the where
twin of #332.

Refs #326.


Claude-Session: https://claude.ai/code/session_01Q9qaMS43V6yaNApJPmuWtM

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
…values they can be, rather than being a string (#354)

Claude-Session: https://claude.ai/code/session_01Hdxai54hc4LwxFQBqMM8jF

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…es, rather than any string a walk happens to ask for (#352)

Claude-Session: https://claude.ai/code/session_01Hdxai54hc4LwxFQBqMM8jF

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
@read-the-docs-community

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

Copy link
Copy Markdown

FBumann and others added 8 commits September 2, 2026 10:03
…says what a caller needs rather than why (#363)

Duplicated rules folded to one home: the comparison-operator set (five
spellings), the node-shape walk over the core AST, the parse-failure frame
shared by both grammars, the atom walk over a where predicate (Mask), the
kwarg-kind dispatch of an operator call, the emitted piecewise names, the
open-bound and typeset row/document assembly. Hand-rolled AST recursions now
go through program.walk and program.children. Guards that validation already
enforces one pass earlier are asserts. Unreachable branches, one-caller
abstractions and the retired index() message are gone. Module and public
docstrings carry the contract; rationale, history and rejected alternatives
are cut.

Visible to a consumer: Format.suffix, EdgeNode.policy, Expression.__sub__ and
__neg__, degree.is_quadratic are removed; to_spec refuses a list with
SchemaError rather than TypeError; overlapping, Symbols, chosen_expressions,
expand, parse_and_expand and check_binary take what their caller already
holds; several load-time messages are reworded.


Claude-Session: https://claude.ai/code/session_01LArQyNe7Kjpryu7YrvvMgq

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…degree question names the tree it walks (#364)

* refactor: a built-in's one positional argument is stated once, and a degree question names the tree it walks

Builtin.positional was 1 for every operator; the count is now the one line
call_shape_error acts on. degree.carries_variable and program.carries_variable
answer the same question over two trees, so the three modules reading the
degree one call it module-qualified and its docstring names its namesake.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LArQyNe7Kjpryu7YrvvMgq

* docs(model): a Spec that exists has passed the whole language, whichever way it was built

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LArQyNe7Kjpryu7YrvvMgq

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ather than state left on the walk (#366)

Claude-Session: https://claude.ai/code/session_01LArQyNe7Kjpryu7YrvvMgq

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…me and mask once (#367)

Claude-Session: https://claude.ai/code/session_01LArQyNe7Kjpryu7YrvvMgq

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
… dim rule is one function (#368)

Claude-Session: https://claude.ai/code/session_01LArQyNe7Kjpryu7YrvvMgq

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…sal names the rule that raised it (#369)

Claude-Session: https://claude.ai/code/session_01LArQyNe7Kjpryu7YrvvMgq

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…on carries its claim, and a docstring argues for one assertion (#372)

Claude-Session: https://claude.ai/code/session_01LArQyNe7Kjpryu7YrvvMgq

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: energy-models-release-please[bot] <319464627+energy-models-release-please[bot]@users.noreply.github.com>
@FBumann
FBumann force-pushed the feat/separability-a-driver-can-act-on branch 2 times, most recently from 8397991 to 8c857fe Compare September 2, 2026 08:12
…a time, and what each coordinate needs from its neighbours

One dataclass and one attribute beside the Footprint the program already
answers: program.separability[dim] says how far a row reads behind and
ahead along the axis, which declarations tie it together and the modelling
change that would lift each, which reaches only data can decide and the
parameter or lookup that decides them, and which masks count a position a
window would restart. windowable and independent are the two verdicts a
rolling-horizon driver and a scenario sweep ask before cutting a slice.

Carries #248 rebased onto alpha.67, where a mask is a Mask and its leaves
come off Mask.atoms rather than a walk of its own.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FPBsP3UM6esCBREVmjMEhZ
@FBumann

FBumann commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

Superseded: the branch is rebased onto main (alpha.67) carrying #248's commit, since #248 predates the Mask move and GitHub refuses to retarget a stacked PR. New PR follows.

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.

1 participant