Skip to content

feat(language): math can be layered onto a model built elsewhere - #508

Closed
FBumann wants to merge 2 commits into
claude/mathspec-merge-givenfrom
claude/mathspec-given-program
Closed

FBumann wants to merge 2 commits into
claude/mathspec-merge-givenfrom
claude/mathspec-given-program

Conversation

@FBumann

@FBumann FBumann commented Sep 16, 2026

Copy link
Copy Markdown
Contributor

Prompt: "Why would given variables need to land in program? I still do t get it" — and, after the explanation, "Do a stacked pr about this"

Note

The following content was generated by AI.

What this changes

A layer reads what another model holds and adds math of its own. It reads a column through given_variables:, and now the dual of a row family through given_constraints:.

given_variables:
  p: { dims: [snapshot, bus] } # a column linopy already holds
given_constraints:
  balance: { dims: [snapshot, bus] } # a row family it already builds
constraints:
  cap: { dims: [], expression: sum(p * rate) <= 100 }
expressions:
  price: { expression: dual(balance) }

The program carries both groups apart from the ones it builds. That is the distinction a builder needs and nothing else can supply: variables is a column to create, given_variables is a column to bind. Creating one instead of binding it is a second column nothing else refers to, in a model that solves and is wrong.

sense is gone, and that is deliberate

#452 required a sense on a given constraint so a dual could be read against the right sign. The layer does not normalise anything — the number comes back under the host's convention and price * load multiplies whatever it gets. What the field bought was a claim no file could check and no consumer was obliged to check.

dims is the opposite: it is required, because the reported expression has no dimensions without it, and it is the one claim a binder can settle against the host. So the section carries a frame and nothing else.

The rule this replaces, and where its coverage went

#507 had to_program refuse a file that reads what it does not build. That rule fitted a library, where merge folds each given declaration into the file that introduces it. A layer has nothing to fold into, so the refusal would make the case impossible.

What replaces it:

  • advice gains a third kind, given, naming each declaration a consumer has to bind. ADVICE_KINDS grows, and test_advice.py's claim that the fixtures produce every kind now reads across two fixtures instead of one.
  • The two tests that asserted the refusal now assert what the program carries: program.variables holds only what it builds, program.given_variables only what it binds, and the frame is there for the binder to check.
  • reading.md gains what a program does not build, which states the three things a consumer owes these groups: bind each name, check the frame, refuse what it cannot bind. Its examples are executed by tests/test_reading_page.py like every other claim on that page.
What a layer prints

The Given legend lists both, and the dual renders where the reported expression uses it:

#### Given

| Symbol | Meaning |
|---|---|
| $`p`$ | `p` over $`\mathcal{T} \times \mathcal{B}`$ |
| $`\mathrm{balance}`$ | `balance` over $`\mathcal{T} \times \mathcal{B}`$, a row family this file reads the dual of — the host clears each bus |

#### Definitions

**`price`**

```math
\mathit{price}_{t,b} = \lambda_{\mathrm{balance},t,b} \qquad \forall\, t \in \mathcal{T},\ b \in \mathcal{B}

</details>

<details><summary>Verified</summary>

Same environment as #507: python 3.13 with the dependencies installed by pip, not the pinned solve.

- `pytest -q` — **1346 passed, 6 skipped** (1333 on #507's head, so 13 new).
- `pyrefly` 1.2.0 — nothing in the changed modules; the 11 it reports are the stub and unused-ignore ones `main` already has. `ruff` 0.15.8 clean, `typos` 1.50.2 clean.
- `python -m tools.schema` regenerated: the schema gains `GivenConstraintBlock`. `python -m tests.typesetting.golden` regenerates to no diff.
- `mkdocs build --strict` builds, with the usual local drop of `docs.python.org/objects.inv`, which this proxy 403s. The new anchor `#what-a-program-does-not-build` is linked from two pages and resolves.

**Not run:** `reuse`, `zizmor`, `taplo`, `compile-tex`.

</details>

<details><summary>Mutation table</summary>

| Mutation | Result |
| --- | --- |
| a dual may not name a given row family | **6 failed** |
| a given row family has no frame for the dual's dims | **2 failed** |
| a row family both built and given is not a collision | **1 failed** |
| the program does not say which columns it only reads | **6 failed** |
| the program does not say which row families it only reads | **3 failed** |
| nothing advises what a consumer has to bind | **4 failed** |
| a dimension only a given column indexes counts as unreached | **1 failed** |
| a dimension only a given row family indexes counts as unreached | **1 failed** |
| a given row family has no symbol to print | **2 failed** |
| a given row family is not in the legend | **1 failed** |
| merge does not fold what a sibling introduces | **3 failed** |
| restored | 1346 passed |

The two reachability rows were **still green** on the first pass: no fixture had a dimension that only a given declaration indexed, so the never-an-axis pass could lose them both and nothing noticed. `test_a_dimension_only_a_given_declaration_indexes_is_in_use` is the purpose-built probe, and the rows above are with it in place.

</details>

## Deliberately not done

- **No binder.** This repository has no host model to bind against. What a consumer must check is stated in `reading.md` and nowhere else; the lpspec side is its own change, and until it lands lpspec builds a layer as though the columns were its own.
- **No `given_objective`.** A second objective over a model that already has one is a different question, and nothing has brought the case.
- **A given column still cannot gate a `piecewise:` curve or sit in an `sos:`.** Unchanged from #507, and correct — this file introduces neither.
- **The twelve declaration keys.** `given_constraints:` is the second section this stack adds, under the same taxonomy decision #507 asks for.

**Stack:** on [#507](https://github.com/energy-models/math-spec/pull/507), which is on [#505](https://github.com/energy-models/math-spec/pull/505).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

https://claude.ai/code/session_01CA5v9XJvgYViUHP7hSiKPU

---
_Generated by [Claude Code](https://claude.ai/code/session_01CA5v9XJvgYViUHP7hSiKPU)_

A layer reads what another model holds and adds math of its own. It reads a
column through `given_variables:`, which #507 added, and now the dual of a row
family through `given_constraints:` — a frame and nothing else, because the
body is the owner's and a sense written here would be a claim no file could
check. `dual(balance)` is the only place one may be named, and the frame is
what gives the reported expression its dimensions.

The program carries both groups, apart from the ones it builds. That is the
distinction a builder needs and nothing else can supply: `variables` is a
column to create, `given_variables` is a column to bind. Creating one instead
of binding it would be a second column nothing else refers to, in a model that
solves and is wrong. `reading.md` states the three things a consumer owes them:
bind each name, check the frame, and refuse what it cannot bind.

So `to_program` no longer refuses a file that reads what it does not build.
That rule fitted a library, where `merge` folds each given declaration into the
file that introduces it; a layer has nothing to fold into. What replaces it is
a note from `advice`, under a third kind, naming each declaration a consumer
has to bind. The two tests that asserted the refusal now asserts what the
program carries instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CA5v9XJvgYViUHP7hSiKPU
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