From 88c219990e7e675c203811aea49dbf903b3bf0f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 12:41:02 +0000 Subject: [PATCH 1/4] docs(language): the rules a call through a relation keeps, in one place MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `dimensions.md` states the frame law and the four invariants the notation is held to, above the bullets that were already its consequences. Two of the five were on the page (the key is fixed, a call brings the dims it lands on) and stay in their bullets, which carry the worked example each needs; three were nowhere. The law is the one the loader computes, checked against it line by line: `_sum_dims` and `_at_dims` both return `(operand − consumed) ∪ produced`, `_check_joined` holds the operand to `consumed ∪ joined`, and `_check_lands_clear` refuses on `(produced ∩ operand) − consumed`. The joined set is `key − (consumed ∪ produced)`, which is symmetric: for `sum` it reduces to `key − consumed`, for `at` to `key − produced`. The block says "a call" throughout rather than "a walk", so it reads the same whichever way #494 goes. Sentence length on the page: n 117, avg 16.8, median 17, 13 over 25 — against n 105, avg 17.0, median 17, 13 over 25 before, so the block adds no long sentence. `mkdocs build --strict` clean, `tests/test_docs.py` 33 passed, prettier and typos clean. The page is not generated, so no generator was involved. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NUhbXWGiyZF46wHp3fVnr2 --- docs/reference/language/dimensions.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/docs/reference/language/dimensions.md b/docs/reference/language/dimensions.md index 5a1384830..14730b047 100644 --- a/docs/reference/language/dimensions.md +++ b/docs/reference/language/dimensions.md @@ -144,6 +144,27 @@ the operand had, less the consumed dimensions, plus the produced ones. `sum` consumes key columns and produces value columns. `at` consumes value columns and produces the key. +#### The rules a call keeps + +Five rules hold whatever a call looks like. They are what makes a relation safe +to edit, and any change to the notation is measured against them. + +1. **The frame law.** `result = (operand − consumed) ∪ produced`. The operand + carries `consumed ∪ joined`, where `joined = key − (consumed ∪ produced)`. +2. **The result gains a dimension only from the operand**, never from an edit to + the relation. +3. **Adding a value column is safe.** A relation that gains one changes no + existing call. A call names both of its ends, and a value column it does not + name is not read. +4. **The key is fixed.** A relation that gains or loses a key column is a + different table. Every call through it joins on the key columns it named at + neither end, so an edit to the key re-aims them all. Declare a new relation + instead. +5. **An operand may grow.** A call means the same when the operand gains a + dimension the relation does not name. That dimension passes through to the + result. Growing it into a dimension the call lands on is refused rather than + silently joined: `(produced ∩ operand) − consumed` has to be empty. + ```yaml dimensions: generator: { dtype: str } From aba530be2bfec8b229298b4c817c03f46e7bd967 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:01:11 +0000 Subject: [PATCH 2/4] docs(language): which operator a call is legal for is one of the rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `sum` consumes a key column and `at` consumes none — the sixth rule, and the only one that says whether a call is allowed rather than what it computes or what survives an edit. It is exactly the check in the code. A column is consumed, produced or joined, so `key <= produced | joined`, which `Direction.is_function_read` computes, holds precisely when no key column was consumed. `at` needs it true and `sum` needs it false. Stated as one line because the fuller reading — sum lands on a value, at lands on a key — is the common case rather than the law, and the loader accepts two calls it would refuse: `sum(p, by=connection, over=generator, into=bus)` lands on a key column, because a bare relation has no value columns; and `at(cap_bg, by=gen_bt, over=bus, into=technology)` lands on a value column, because the key is covered by the join rather than by the result. Both were run against the loader. The section's prose above the block still teaches the common case, which is what it is for. Sentence length on the page: n 118, avg 16.8, median 17, 13 over 25 — unchanged from the previous commit but for the added sentence. `mkdocs build --strict` clean, `tests/test_docs.py` 33 passed, prettier and typos clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NUhbXWGiyZF46wHp3fVnr2 --- docs/reference/language/dimensions.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/reference/language/dimensions.md b/docs/reference/language/dimensions.md index 14730b047..d7a528d83 100644 --- a/docs/reference/language/dimensions.md +++ b/docs/reference/language/dimensions.md @@ -146,8 +146,9 @@ columns and produces the key. #### The rules a call keeps -Five rules hold whatever a call looks like. They are what makes a relation safe -to edit, and any change to the notation is measured against them. +Six rules hold whatever a call looks like: what it computes, which operator is +legal, and what survives an edit. Any change to the notation is measured +against them. 1. **The frame law.** `result = (operand − consumed) ∪ produced`. The operand carries `consumed ∪ joined`, where `joined = key − (consumed ∪ produced)`. @@ -164,6 +165,8 @@ to edit, and any change to the notation is measured against them. dimension the relation does not name. That dimension passes through to the result. Growing it into a dimension the call lands on is refused rather than silently joined: `(produced ∩ operand) − consumed` has to be empty. +6. **`sum` consumes a key column; `at` consumes none.** Each is refused in the + other's case. ```yaml dimensions: From b4adc9c10303d7f48a05cd03189d86618b7c2deb Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 13:20:07 +0000 Subject: [PATCH 3/4] docs(language): what each end of a call may name, and how far the rules reach MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two tables. The first says what `over=` and `into=` may name on each operator, which is rule 6 and nothing else: `sum` consumes at least one key column, `at` consumes value columns only, and either end may name any subset otherwise. Both were run against the loader, including the mixed cases — `sum(x, by=gen_bt, over=[generator, bus], into=technology)` consumes a key and a value together, and `at(cap, by=gen_bt, over=bus, into=[generator, technology])` produces one of each. The second says which rules reach the other two ways to read a relation. A partition moves no frame, so the frame law is silent; rule 4 holds with force, and rule 3 holds only where the call writes `within=`. A `where` builds no direction at all. That last gap is a real one rather than a wording: `within=` omitted means every value column, so a relation that gains one regroups every partition through it. The page says so and cites #538, which proposes the fix. Sentence length on the page: n 131, avg 16.8, median 16, 14 over 25 — against n 105, avg 17.0, median 17, 13 over 25 before the rules block. `mkdocs build --strict` clean, which is what checks the new `#where-the-rules-reach` anchor and the link into `#partitions`. `tests/test_docs.py` 33 passed, prettier and typos clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NUhbXWGiyZF46wHp3fVnr2 --- docs/reference/language/dimensions.md | 44 +++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/docs/reference/language/dimensions.md b/docs/reference/language/dimensions.md index d7a528d83..87c6b11ef 100644 --- a/docs/reference/language/dimensions.md +++ b/docs/reference/language/dimensions.md @@ -146,9 +146,10 @@ columns and produces the key. #### The rules a call keeps -Six rules hold whatever a call looks like: what it computes, which operator is -legal, and what survives an edit. Any change to the notation is measured -against them. +Six rules hold whatever a `sum` or an `at` looks like: what it computes, which +operator is legal, and what survives an edit. Any change to the notation is +measured against them. A relation is read in two other ways, which keep fewer +of them ([below](#where-the-rules-reach)). 1. **The frame law.** `result = (operand − consumed) ∪ produced`. The operand carries `consumed ∪ joined`, where `joined = key − (consumed ∪ produced)`. @@ -168,6 +169,43 @@ against them. 6. **`sum` consumes a key column; `at` consumes none.** Each is refused in the other's case. +What the two ends may name follows from rule 6, and nothing else distinguishes +them: + +| operator | `over=`, consumed | `into=`, produced | +| -------- | -------------------------------------- | ----------------- | +| `sum` | any columns, at least one a key column | any columns | +| `at` | value columns only | any columns | + +A key column named at neither end is joined on, and the operand carries it. A +value column named at neither end is not read. Neither end may name a column +twice, name one the other end names, or name two columns over one dimension: +the operand has one axis per dimension, so nothing would say which column +indexes it. + +#### Where the rules reach + +The six above govern the calls that move the frame. The other two ways to read +a relation move nothing, and keep less: + +| reading a relation | rules that hold | +| ----------------------------------------------------------------------- | ---------------- | +| `sum` and `at` | all six | +| a partition — `shift`, `sum_back`, `position` ([below](#partitions)) | 4, and 3 in part | +| a relation in a `where` ([where strings](expressions.md#where-strings)) | none of the six | + +A partition names neither end: `along=` picks the key column it steps along, +and `within=` the value columns the group is made of. The frame law says +nothing, because the frame does not change. Rule 4 holds with force, since a +new key column changes which column is stepped along. Rule 3 holds only where +the call writes `within=`: omitted, it means every value column, so a relation +that gains one regroups the call +([#538](https://github.com/energy-models/math-spec/issues/538)). + +A `where` builds no direction at all. It reads the relation at the frame's own +coordinates, so the frame carries the key's dimensions, and nothing is +consumed, produced or joined. + ```yaml dimensions: generator: { dtype: str } From d5039732ec724ea9427a6e79d1a731725fee2ee7 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 18 Sep 2026 14:15:49 +0000 Subject: [PATCH 4/4] docs(language): what a relation and an operator each contribute, before the rules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The section opened on the mechanics of a call. It now opens on the division of labour: a relation says which rows exist and what identifies them, and an operator says what to do with the rows a coordinate finds. The key is the hinge — consume a key column and many rows share what is left, leave it whole and one row is found. One table puts every use of a relation on that single axis: many rows added up is `sum`, one row read is `at`, one row as a group label is a partition, one row or none as an answer is a `where`. Only the first finds more than one, which is why only `sum` adds anything up — rule 6 as the reader meets it rather than as a law. The table `Where the rules reach` listed the same four categories one screen below, so it goes; its prose stays, since what it says about partitions and `where` is not in the new table. Sentence length on the page: n 136, avg 16.8, median 16, 15 over 25. `mkdocs build --strict` clean, which checks the two links out of the new table. `tests/test_docs.py` 33 passed, prettier and typos clean. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01NUhbXWGiyZF46wHp3fVnr2 --- docs/reference/language/dimensions.md | 35 ++++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/docs/reference/language/dimensions.md b/docs/reference/language/dimensions.md index 87c6b11ef..59b290c44 100644 --- a/docs/reference/language/dimensions.md +++ b/docs/reference/language/dimensions.md @@ -136,10 +136,27 @@ a many-to-many relation can say, and all it can say. ### Walks -A walk consumes one or more columns of a relation, produces one or more, and -joins on every other key column. The call writes both ends. The operand carries -every dimension the walk consumes and every one it joins on. The result is what -the operand had, less the consumed dimensions, plus the produced ones. +A relation says which rows exist and what identifies them. An operator says what +to do with the rows a coordinate finds. The key is the hinge: consume a key +column and the row is no longer pinned, so many rows share what is left; leave +the key whole and exactly one row is found. + +| a coordinate finds | what is done with it | that is | the frame | +| --------------------------------------------- | ----------------------------- | ------------------------------- | --------- | +| many rows | they are added up | `sum` | moves | +| one row, whose value becomes a coordinate | the operand is read at it | `at` | moves | +| one row, whose value becomes a label | the coordinate joins a group | `shift`, `sum_back`, `position` | unchanged | +| one row or none, whose presence is the answer | the coordinate is kept or cut | a relation in a `where` | unchanged | + +Only the first finds more than one row, which is why only `sum` adds anything +up. The rest of this section is about the two that move the frame: a partition +is [below](#partitions), and a `where` is +[where strings](expressions.md#where-strings). + +A call consumes one or more columns of a relation, produces one or more, and +joins on every other key column. It writes both ends. The operand carries every +dimension consumed and every one joined on. The result is what the operand had, +less the consumed dimensions, plus the produced ones. `sum` consumes key columns and produces value columns. `at` consumes value columns and produces the key. @@ -185,14 +202,8 @@ indexes it. #### Where the rules reach -The six above govern the calls that move the frame. The other two ways to read -a relation move nothing, and keep less: - -| reading a relation | rules that hold | -| ----------------------------------------------------------------------- | ---------------- | -| `sum` and `at` | all six | -| a partition — `shift`, `sum_back`, `position` ([below](#partitions)) | 4, and 3 in part | -| a relation in a `where` ([where strings](expressions.md#where-strings)) | none of the six | +The six above govern the calls that move the frame. The other two readings keep +less. A partition names neither end: `along=` picks the key column it steps along, and `within=` the value columns the group is made of. The frame law says