Skip to content

feat(language): a where predicate is named once under masks:, and another file reads it under given: masks: - #821

Open
FBumann wants to merge 15 commits into
mainfrom
claude/trusting-allen-xp0tgb
Open

FBumann wants to merge 15 commits into
mainfrom
claude/trusting-allen-xp0tgb

Conversation

@FBumann

@FBumann FBumann commented Oct 1, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: Lets now do the masks issue, both as a regular block and a given block. … Lets add this as a feature, and leverage it in the pypsa examples etc! And discuss the typesetting too!

Follow-up: 821: fix it. And why do masks even declare masks explicitly?

Note

The following content was generated by AI.

What this changes

A masks: entry names a where predicate once, and a where:, when: or holds: reads it by name. Another file reads it under given: masks:. The PyPSA example names 16 masks (#825). Closes #803.

A mask declares no dims:. Its frame is the dims its predicate reads. A mask has no cases:, so its predicate always gives the frame, and a declared one could only repeat it or be wider. A wider one printed an index no quantifier binds.

masks:
  stands:
    where: build_year <= period_year AND period_year < build_year + lifetime
    description: the generator stands in this period
variables:
  p: { dims: [period, generator], where: stands, bounds: { lower: 0, upper: p_nom } }
constraints:
  ramp_up:
    dims: [period, generator]
    where: stands AND shift(stands, along=period, offset=1)
    expression: p - shift(p, along=period, offset=1) <= 0.5 * p_nom
# ramping.yaml: #803's case, which loads alone now
given:
  variables: { p: { dims: [period, generator] } }
  masks: { stands: { dims: [period, generator] } }
constraints:
  ramp_up: { dims: [period, generator], where: stands AND shift(stands, along=period, offset=1), expression: … }

Why

#803 counts 51 where: strings that write "a generator stands in this period" out across four PyPSA files. Within one file, #795's workaround is a 1/0 cases: expression read as active == 1. Across files, a where may not read a given expression at all, since one may hold a variable.

Typesetting, for discussion

These are the choices this PR makes, with the alternatives I did not take.

Choice This PR Alternative
Symbol at a use upright, like data: $\mathrm{stands}_{e,g}$ italic like a named expression. A mask reads only data, and the convention note says that upright is what the data supplies.
Definition line $\mathrm{stands}_{e,g} \iff \ldots \quad \forall\, e, g$ :⟺ ("is defined as"), or = with a \{0,1\} reading. = would read a predicate as a number, which is the confusion the refusal names.
Section its own Masks heading, after Definitions inside Definitions. Separate headings keep = and ⟺ lines apart.
Legend a Masks group after Definitions. A given mask goes under Given: "a mask another file defines" a combined group
inline_expressions masks always print by symbol inline a mask's predicate too. A predicate at the end of a quantifier gets long fast, and #803's point is the name.
Read through shift the shifted index lands on the mask's subscript: $\mathrm{stands}_{e-1,g}$

Rendered (Markdown), from the example above:

$$p_{e,g} - p_{e - 1,g} \le 0.5 \cdot \mathrm{p}^{\mathrm{nom}}_{g} \qquad \forall\, e \in \mathcal{E},\ g \in \mathcal{G} \,:\, \mathrm{stands}_{e,g} \wedge \mathrm{stands}_{e - 1,g}$$ $$\mathrm{stands}_{e,g} \iff \mathrm{build\_year}_{g} \le \mathrm{period\_year}_{e} \wedge \mathrm{period\_year}_{e} < \mathrm{build\_year}_{g} + \mathrm{lifetime}_{g} \qquad \forall\, e \in \mathcal{E},\ g \in \mathcal{G}$$
Design, refusals, guards, gate output, what is not done

Triage (against limits.md, as #803 argued): a declaration section. It is not a macro, because a macro template is arithmetic with one owner. It is not a primitive, because it adds no operator. It is not a named expression: a predicate is not a number, and a bare parameter in a where already means "has a row and is finite". limits.md does not move.

Program.

  • NamedMask(name, body) is a new Predicate member. It is a pass-through, as Named is for expressions. where_children() steps into the body, so .atoms, .names_read, .dims, the frame checks, separability and the case-overlap proof read through it unchanged. The overlap proof needed one branch: _evaluate steps into the body.
  • Program.masks holds a MaskDeclaration(where, dims, description) per entry. Its dims are the dims the predicate reads, in the order dimensions: declares them. GivenTargets.masks holds the given ones.
  • A where reads a given mask as ParameterDefined(name, dims), the boolean data it is to that file. This mirrors a given expression read as Variable. reading.md says so.

Resolution.

  • Namespace.mask() resolves an entry once, on the same _loading stack as named expressions, so a cycle across masks names its chain.
  • A mask may read another mask, a named expression that reads data only, and a variable's existence.
  • The self-existence check runs again on the substituted tree, since a mask's predicate is resolved under the mask's own name.

Refused at load (test_what_a_mask_may_not_be_is_refused_at_load, 11 cases):

Also refused: a variable that asks through a mask whether it exists, two fragments that define one mask, and dims: on a mask. The closed schema's own error refuses the key: masks.young: unknown key 'dims' in a mask declaration. Valid keys: description, where.

Named expression 'e': 'stands' is a mask, which is true or false where it is read, and not a number. Write it bare in the where — stands, or NOT stands — rather than comparing it or computing with it.

Merge. A given mask is boolean data to the file that reads it, and merge folds it into the definition. GIVEN_KINDS and READ_KINDS take masks. _fits reads the definer's frame off its program. No mask block writes dims:, so the frame is the dims the definer's predicate reads. A reader states exactly that frame, as a reader of a parameter states its introducer's. A wider or narrower one is refused at merge, and the message names both frames.

Also changed.

Guards. Each check was deleted and the suite run:

Check deleted Failing test
cycle refusal …refused_at_load[a-cycle], [a-mask-reading-itself]
folds to a literal [always-true], [always-false]
dims on MaskBlock (the old tree) test_a_mask_declares_no_frame: DID NOT RAISE
mask in arithmetic [a-mask-in-arithmetic], [a-given-mask-in-arithmetic]
mask compared [a-mask-compared]
self-existence through a mask test_a_variable_that_asks_through_a_mask_whether_it_exists_is_refused
_fits mask frame as <= (the old tree) test_a_reader_that_states_a_frame_wider_than_the_mask_s_is_refused: DID NOT RAISE
masks in READ_KINDS test_a_reading_the_definer_does_not_answer_is_refused[a-mask-read-as-a-parameter]
the given-mask advice note test_check_notes_the_mask_a_fragment_reads

Coverage moved.

Docs.

  • named.md: a ## masks section. The page title is now "Named expressions, masks and macros", in the nav too.
  • declarations.md: ### given: masks, and a pointer from given: expressions.
  • expressions.md: the bare-name row and the namespace.
  • file.md and the language index: thirteen keys.
  • reading.md: NamedMask, and a given mask as ParameterDefined.
  • typeset.md: the Masks heading, and typeset_declaration prints six kinds, masks and curves included.
  • howto/compose.md: the fold row.
  • Regenerated: the schema, the golden output and the notation page.
  • Sentence measures: named.md masks n 14, median 14, over25 2 (both quoted messages); declarations.md given: masks n 8, median 13, over25 0.

Gates, on the head that removes dims: from masks:

  • pixi run lint: clean.
  • pixi run test: 2697 passed.
  • docs-build, strict: blocked in this session. The proxy refuses docs.python.org/3/objects.inv, and the unresolved pathlib.Path autoref that follows is the only warning. Left to CI.
  • compile-tex: not run. The tectonic bundle does not download here. Left to CI.

Not done.

  • adds_to: for masks. A predicate has no +.
  • Inlining masks.
  • shift(m, along=d) still needs d in the predicate's dims, as it does for a predicate written out.

🤖 Generated with Claude Code

https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK


Generated by Claude Code

claude added 10 commits October 1, 2026 11:41
…ven expression, not as a variable

The namespace files a given expression with the variables, because it is
read as a column. A where that read one was refused as if it read a
variable. The three refusals (the left name, the right name, a side of a
comparison of expressions) now name the given expression and say why a
mask may not read it: it may hold a variable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK
The Upcoming section becomes 0.3.0, grouped into composition, language,
typesetting and advice, and documentation. The notes name the two breaks
against 0.2.0 (#788, #751). The #742 line is left out: #763 replaced
`empty: true` before any release carried it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK
…s working

#788 and #751 change only documentation and a docstring. mathspec neither
attaches data nor computes duals, so neither breaks a file or an import.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK
…e on its own

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK
…ther file reads it under given: masks:

A masks: entry names a where predicate. A bare mask name in a where:,
a when: or a holds:, under NOT and inside count, shift and at, stands
for its predicate. A use is a NamedMask node with the predicate under
it, so .atoms, .names_read, .dims and the case-overlap proof read
through it, and the typesetter prints the symbol and defines the mask
once under Masks, with iff.

given: masks: reads another file's mask. A where reads it as boolean
data over its frame, a ParameterDefined, as a where may not read a given
expression. merge folds the reading into the definer; the reader's dims
bound the definer's frame.

Refused at load: a cycle, a predicate that folds to a literal, a
predicate wider than the declared dims, a mask in arithmetic or in a
comparison, and a variable that asks through a mask whether it exists.

Docs, named.md masks section (n 14, median 14, over25 2, both quoted
messages); declarations.md given: masks (n 8, median 13, over25 0).

Closes #803.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK
@FBumann
FBumann requested a review from brynpickering as a code owner October 1, 2026 14:52
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PBmqeqBrqpQC6MVoSrguHK
@FBumann FBumann added design: accepted Decided — the build queue area: where What a where predicate may say labels Oct 1, 2026
@read-the-docs-community

read-the-docs-community Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

…s, and its topic files read them under given: masks: (#825)

* docs(pypsa): the pypsa spec names its repeated row conditions as masks, and its topic files read them under given: masks:

Sixteen masks name the conditions examples/pypsa.yaml wrote out at every
row: per committable class C, C_committed (26 sites), C_com_ext (8),
C_maint_ext (5) and C_ramps_from_previous (6); StorageUnit_fix and _ext
(6 each); Line_lossy and Transformer_lossy (4 each). 165 where strings
change. Each where, with every mask expanded, equals the one it
replaces: 352 declarations and every cased expression were compared
conjunct by conjunct.

tools/pypsa_split.py places a mask in the topic its name names and
writes given: masks: for every other topic that reads it. All 24
fragments load alone and merge to the one file's canonical form. The
symbol table spells the masks as on^{qualifier} and prev, and the
gallery's table cut includes masks.

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

* docs: the changelog line links #825

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
@FBumann
FBumann added this pull request to stack #831 October 2, 2026 06:52
@FBumann FBumann added the v0.3.0 label Oct 2, 2026
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011a8N2JQdmmd1WhVCKN1a2A
FBumann pushed a commit that referenced this pull request Oct 2, 2026
Brings in origin/main through the base branch of #821.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011a8N2JQdmmd1WhVCKN1a2A
A masks: entry no longer takes dims:. Its frame is the dims its predicate
reads, in the order dimensions: declares them, so a use can no longer
print an index no quantifier binds. A given: masks: reader states exactly
that frame, as a parameter reader states its introducer's.

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: where What a where predicate may say design: accepted Decided — the build queue v0.3.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

a where: test cannot be named once and read in another file, so a component's mask is written out at every use

2 participants