Skip to content

feat(language): a lookup may map into its own dimension, so a representative snapshot is sayable - #436

Closed
FBumann wants to merge 2 commits into
claude/lookup-keys-vhvfjdfrom
claude/lookup-self-map-vhvfjd
Closed

FBumann wants to merge 2 commits into
claude/lookup-keys-vhvfjdfrom
claude/lookup-self-map-vhvfjd

Conversation

@FBumann

@FBumann FBumann commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: "can/should we fix 424 right away?" — "Do it" — "We should add this to the PR. self map is always directional! And why?"

Note

The following content was generated by AI. Stacked on #433, which is the base branch. Resolves #424. Draft, since it rides on a proposal the discussion under #275 has not yet taken; nothing in it depends on multi-key lookups, so it rebases onto main unchanged if #433 is not the base chosen.

What this changes

A lookup may map into the dimension it is keyed by:

lookups:
  rep_of: { over: snapshot, into: snapshot }   # every snapshot names the one that stands for it
constraints:
  representative:
    foreach: [snapshot]
    expression: p == at(p, by=rep_of)          # each snapshot takes its representative's value
  weighted:
    foreach: [snapshot]
    expression: sum(p, by=rep_of) <= 100       # the snapshots a representative stands for, summed onto it

The one refusal, "maps into itself", goes. No rule changes: the walked key is consumed and the target is produced, and here they are the same dimension, so the frame is unchanged through sum(by=) and at(by=). shift(by=rep_of) walks inside each representative's group and position(snapshot, by=rep_of) counts within it. The dim checks already pass as written, since they test the target against the operand minus the walked key, and the typesetter already primes the index: the golden case renders as

$$\sum_{t' \in \mathcal{T} : \mathrm{rep_of}(t') = t} \mathit{spill}_{t'} \le \mathit{spill}_{\mathrm{rep_of}(t)} \qquad \forall t \in \mathcal{T}$$

with rep_of: 𝒯 → 𝒯 in the legend.

A self-map is always directional, and why

A lookup is a function: one value per key, and the declaration says which way the arrow points. rep_of sends every snapshot to its representative and never the other way. The two verbs are the two walks of that one arrow, exactly as for a cross-map:

call walks reads
at(x, by=rep_of) along the arrow each snapshot takes its representative's value
sum(x, by=rep_of) against it each representative collects the snapshots that point at it
shift(x, over=snapshot, by=rep_of), position(snapshot, by=rep_of) inside its fibre the snapshots sharing one representative
at(at(x, by=rep_of), by=rep_of) two steps along legal because the frame is unchanged at each step

The inverse of a many-to-one map is one-to-many, so it is reachable as a grouping and never as a function. For a bijection, a successor map next_of, the two walks are the two directions outright: at reads the successor's value, sum lands each value on its predecessor, one term per group.

What has no direction is not a lookup. An undirected neighbour relation between buses is not a function of either end, so it is a parameter over [bus, bus], where #433's placement table already routes every many-to-many relation, and a symmetric reading is sum(w * x, over=...) with a weight table or a bool mask. The reference's "Into its own dimension" section carries this paragraph.

Breaking, for every lookup's data. The value column of a lookup's table is named after the lookup, not the target: gen_bus arrives as columns generator, gen_bus, where it arrived as generator, bus. This is the one decision the self-map forces, since a table keyed by snapshot with a value column also called snapshot has two columns of one name. Of the three ways out, this is the one rule with no special case, and it is what lpspec already does on read, which aliases the value column to the lookup name. The alternatives were a rule that names the column after the target except where the target is a key, and coroa's role-named columns (into: {rep: snapshot}), which is deferred with his over: {bus0: bus} sugar as #433 says.

One thing a self-map raises and this PR does not answer. Selecting the representatives themselves, the rows where the map is the identity, would be where: "rep_of == snapshot", and that compares a lookup to a dimension, which the language refuses (a bare dimension name on the right is a string coordinate). The reference says to declare a bool parameter for them. Whether a lookup-to-dimension comparison should exist is a separate question, and only a self-map can ask it.

Where it lands
  • model.py: the refusal removed from _lookup_targets; the LookupBlock docstring names the case.
  • docs: into may be a key; an "Into its own dimension" section with the example above, the frame rule, the directionality paragraph, and the identity-selection caveat; the data section names the value column after the lookup, with the sources example updated.
  • tests: the two "into itself" refusals become three dim inferences (sum, at, shift through rep_of keep the frame) and two predicate readings; the golden model gains rep_of and one constraint walking it both ways, in all three formats; notation regenerated.
For lpspec
  • _read_relation reads the value column under the lookup's name rather than the target's: one argument.
  • The self-join in the relational lane's _mapping needs an alias on the key column, since both sides carry snapshot.
  • The linopy lane's assign_coords and sel already accept a coordinate over the same dim it indexes.
  • Every converter that emits lookup tables renames its value column once.
Verified

On the current head, in a Python 3.13 venv built from PyPI (pixi.sh is blocked in this environment, so the tool versions are the latest rather than the pinned ones):

  • pytest -q: 1235 passed, 6 skipped.
  • ruff check .: all checks passed. ruff format --check .: 127 files already formatted.
  • Every generated file was regenerated rather than hand-merged — tools.schema, tools.notation, tools.spec_math, tools.home_math, tools.gallery, tests.typesetting.golden — and tests/test_docs.py passes on the result.

Not run: pyrefly, reuse lint, typos, prettier --list-different, compile-tex, and pixi run ci as a whole. mkdocs build --strict reaches its last step and then aborts on the one error the proxy causes — it cannot fetch https://docs.python.org/3/objects.inv for mkdocstrings.

Only the two generated files conflicted on the rebase onto #433's new head — docs/reference/notation.md and tests/typesetting/golden/markdown.out — and both were regenerated. Nothing in this PR's own two commits needed a hand resolution.

Why

A representative-period map is the one capability the word "connector" in #6 reaches for that the language refused, and it needs no new construct: it is a lookup whose target happens to be its key. What it did need was a column-naming rule that does not collide, and the rule that has no special case is the one that names the value column after the lookup.

🤖 Generated with Claude Code

https://claude.ai/code/session_014FHQEdh9u2ppraSniF3iJd

…ntative snapshot is sayable

`rep_of: {over: snapshot, into: snapshot}` loads. The walked key is
consumed and the target produced, so the frame is unchanged through
sum(by=) and at(by=), and shift and position group within each
representative. The value column of every lookup's table is named after
the lookup rather than the target, which is the one rule a self-map needs
and the one every lookup now follows.

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

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FD5LpGRzAWdi5sKWXDdnHC
@FBumann
FBumann force-pushed the claude/lookup-self-map-vhvfjd branch from 5adc494 to cb8ab96 Compare September 10, 2026 08:50
FBumann pushed a commit that referenced this pull request Sep 10, 2026
…e only #437 can say

Adds a primer above the models: seven cards, each one kind of pairing in
everyday terms first and then in a model, with the spelling and what each
proposal does with it. Every `says` entry is measured — the file is under
`models/` or `probes/` and the branch's own answer is in `evidence.json`.

Adds a fifth model, `models/p5`: which regions are neighbours. Both columns
are regions, so the fallback for a relation — a parameter over the pair — is
refused, and the two left-hand panels print that refusal instead of a frame.
`build.py` renders a refused model as evidence rather than treating it as a
failure.

Two measured findings the primer prints verbatim:

- a `dtype: bool` flag cannot be multiplied, so the table of ones that stands
  in for an unweighted relation has to be declared `dtype: int`;
- #436's description sends an undirected neighbour relation to "a parameter
  over `[bus, bus]`", and that file does not load, on #436's own branch or any
  other.

`verify.py` gains #436's branch, which is not a fourth proposal but the draft
stacked on #433: a claim about the self-map is a claim about #433 with #436.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01W4kdrj2n7tESNrgYmfXkAq
@FBumann
FBumann added this pull request to stack #457 September 14, 2026 07:47
@FBumann FBumann added the area: relations relations and dimensions: the relation design label Sep 15, 2026 — with Claude

FBumann commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Prompt: "#437 was merged / Close all related issues and stale PRs"

Note

The following content was generated by AI.

Closed with its base. This was stacked on #433, which lost #275 to #437 — merged as 3284926, released in v0.0.0-alpha.89 — so the branch under it is not going to main.

The capability is not lost. #424 is shipped, by role columns rather than by a lookup mapping into its own dimension:

relations:
  rep: { columns: { snapshot: snapshot, rep: snapshot }, key: snapshot }

A relation may name one dimension twice, and the mapping form of columns: gives the two columns roles. That is the same mechanism a line's two ends use, so the self-map needed no rule of its own.

One limit came with it and is recorded on #424: key: cannot narrow the bind check to one-to-one, so a representative-snapshot table is held to each snapshot appearing once and not to each representative being used once. Several candidate keys are deferred in #437.


Generated by Claude Code

@FBumann FBumann closed this Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: relations relations and dimensions: the relation design

Projects

None yet

Development

Successfully merging this pull request may close these issues.

A lookup from a dimension into itself, so representative snapshots are sayable

2 participants