Skip to content

feat(language)!: a missing row is refused only where the model reads it, and a file names its value instead of neutral - #881

Merged
FBumann merged 4 commits into
feat/refused-where-readfrom
feat/drop-neutral
Oct 15, 2026
Merged

FBumann merged 4 commits into
feat/refused-where-readfrom
feat/drop-neutral

Conversation

@FBumann

@FBumann FBumann commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: Lets do it — And we need to document the now more less ambigouos behaviour and cotract!

Prompt: And merge 880 and 881 into one PR

Note

The following content was generated by AI.

missing: refused now refuses a row only where the model reads the value, and a where reads nothing. neutral is gone: a file writes the value, such as 0 or false. A masked variable may stand as any finite number. absence.md states the contract position by position.

This PR replaces #880.

Contract: where a parameter is read

absence.md has a new section, Where a parameter is read. The model reads a parameter at these coordinates: a row it builds, a summand that exists, a bound of a variable that exists, an offset=/window= amount, the objective, a reported expression, and the breakpoints of a curve. A row that an absent term removes reads nothing.

A where reads nothing. A bare name tests whether the row is there, and a comparison at a missing row is false unless missing: names a value. With no short-circuit, a mask that reads would make the guard where: cost AND cost > 0 refuse the rows it is there to skip.

Two load refusals relied on "a row at every coordinate", so they are removed:

  • a bare int/str name in a where under refused;
  • a values table of a curve under points: declared refused. The curve reads no row past its mask.
Readings: refused, absent, or a value

neutral read a missing row as 0 or false. It was named for "contributes nothing", which holds only for an additive term. As a factor it dropped the whole term. As an exponent it gave 1. As an offset= it read the coordinate itself. Its three refusals (divisor, bound, constant side) are now what refused does.

  • A parameter takes refused, absent or a value. 'neutral' gets the closed-vocabulary error, which names the valid readings.
  • absence.md has a table with one row per position (coefficient or constant, summand, divisor, bound, offset=/window= amount, where comparison, bare name) and one column per reading. Under absent, a missing amount makes the shift/sum_back absent there.
  • A parameter declared missing: 0 that stands alone as a divisor is refused at load.
  • A variable takes absent or any finite number. It needs a where:, and the number must fit the variable: 0/1 for binary, an integer for integer, and inside any literal bound. A parameter bound is data, so it is not checked. A curve gate is binary, so it takes 0 (no curve) or 1 (ungated, which is also what absent gives).

One reading changes: a where comparison. Under neutral, a comparison at a missing row was false. A value compares as itself, so cost < 1 becomes true at a missing row under missing: 0. Every comparison on a migrated parameter in examples/ is > 0, which is false both ways. To keep the old reading, write where: cost AND cost < 1.

Migration. examples/, tests/expand/ and the golden fixture use 0 for float/int and false for bool. The divisor refusal caught the PyPSA module sizes: Generator_p_nom_mod divides in *_maint_modstatus_*. Every arithmetic read of the seven *_nom_mod parameters sits inside a *_nom_mod > 0 mask, so they keep the default refused. Regenerated: the schema, the golden output, the gallery and notation pages.

Verification

Mutation table. Each guard was deleted by hand, the suite was run, the file was restored through git checkout -- with __pycache__ dropped on both sides, and the tree came back clean each time.

Guard deleted Result
_zero_divisor (never refuse) caught: test_a_divisor_that_reads_a_missing_row_as_zero_is_refused[a-constraint], [a-named-expression]
binary domain check caught: …_outside_what_the_variable_takes_is_refused[binary]
integer domain check caught: …[integer]
lower bound check caught: …[below-lower]
upper bound check caught: …[above-upper]
finite, not-a-flag check on a variable caught: test_a_variable_missing_that_reads_data_is_refused[a-flag], [an-infinity]
isinstance(declared.missing, bool) in _zero_divisor still green. No file can reach it: false on a numeric parameter is refused, and a bool parameter cannot be a divisor. It is kept on review request, so that False == 0 can never read as a zero divisor

Loading cases cover over-reach: another value as divisor, 0 as a factor, 0 inside a compound divisor, and a variable number on a continuous variable, a binary variable, an integer variable, a literal bound, and a parameter bound.

Coverage moved. neutral cases became 0/false cases. an-int-under-neutral, neutral-on-a-label and the neutral exclusivity reading are removed, and their value counterparts stay. The bare-name refusal cases became loading cases. …_under_points_is_not_refused became …_may_be_refused.

Gates. pixi run lint and pixi run test pass (2863 passed, 1 skipped). docs-build stops only on the strict warning for pathlib.Path, because the container's proxy blocks docs.python.org. compile-tex was not run.

Not done
  • A relation's refused still checks every key combination when the data is attached.
  • missing: <parameter> on a variable. A named expression with cases: already says "the variable where the mask holds, the data elsewhere".
  • specsolve and its linopy backend must move the refusal from attach to build, and read the value forms. That is their own PRs.
  • The 0.3.0 release notes still name neutral. They are history.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng

… `missing: neutral` is gone

`neutral` read a missing row as 0 or false, named for an intent that
holds only where the parameter is an additive term. Elsewhere it read 0
anyway: a whole term dropped, an exponent gave 1, an offset read the
coordinate itself. Write the value instead: `missing: 0` or
`missing: false`. A variable takes `missing: 0` for "zero outside the
mask". A parameter named `missing: 0` that stands as a divisor is
refused at load.

absence.md states what each reading does at each position, amounts
included: under `absent`, a missing offset or window makes the operator
absent there.

The PyPSA module sizes `*_nom_mod` keep the default `refused`: every
arithmetic read sits inside a `> 0` mask, and the divisor refusal caught
them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
@FBumann
FBumann added this pull request to stack #882 October 15, 2026 09:58
…ide its mask

`missing:` on a variable took `absent` or `0`. It now takes any finite
number, refused at load where the number leaves the variable's domain or
its literal bounds. The absence table states a constant once, on either
side of a comparison, and the zero-divisor refusal ignores a `false`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AvtZGymCbY9swLnR8JwHng
@FBumann FBumann changed the title feat(language)!: a missing row reads as the value the file names, and missing: neutral is gone feat(language)!: a missing row is refused only where the model reads it, and a file names its value instead of neutral Oct 15, 2026
@FBumann
FBumann merged commit 5d618fc into main Oct 15, 2026
4 of 5 checks passed

FBumann commented Oct 15, 2026

Copy link
Copy Markdown
Contributor Author

Prompt: And merge 880 and 881 into one PR

Note

The following content was generated by AI.

Closed in favour of #880. GitHub refused to move this PR's base off its stack, so #880's branch was fast-forwarded to carry every commit of this PR. The earlier comment on #880, which pointed here, is out of date.


Generated by Claude Code

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