Skip to content

feat(language): a lookup may be conditioned on a second dimension, so a generator's zone can change by period - #428

Closed
FBumann wants to merge 1 commit into
mainfrom
claude/lookup-per-keyword-vhvfjd
Closed

FBumann wants to merge 1 commit into
mainfrom
claude/lookup-per-keyword-vhvfjd

Conversation

@FBumann

@FBumann FBumann commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Prompt: "Lets implement both PRs. Do the mathspec one stacked onto #422"

Note

The following content was generated by AI. The lpspec half is a separate PR against fluxopt/lpspec.

What this changes

Implements #161. A lookup takes per:, the dimensions the map varies along besides over and into:

lookups:
  zone_of: { over: generator, into: zone, per: [period] }

over is consumed, into is produced, per is joined on and passed through. Nothing changes at the call site: sum(p, by=zone_of) takes p[generator, period] to [zone, period], and at(price, by=zone_of) reads price[zone, period] back at [generator, period]. shift(by=) and position(by=) group within each per coordinate, and a where naming the lookup is read at them.

Decided at load, each with its refusal and a test:

  • every per dim is declared, and is neither over nor into, and named once;
  • the operand of sum, at, shift and sum_back carries every per dim, and a where leaf naming the lookup reads them (so the frame must carry them);
  • a by=[a, b] list shares its per as it shares its over, since one grouping is one join;
  • two lookups compared in a where share it, or no row carries both.

Left to the consumer, as #161 asks: the map binds single-valued per (over, *per), and its table is (over, *per, into).

Consumer-visible. GroupSum, At, the three lookup where nodes and DimensionPositionNode gain per: tuple[str, ...] = (); LookupDeclaration too. Namespace.lookups is name -> (over, into, per). The legend prints zone_of: 𝒢 × 𝒫 → 𝒵, and every application prints zone_of(g, p).

The golden model's new lines, rendered
gen_zone: { over: generator, into: zone, per: [snapshot] }
zonal:
  foreach: [snapshot, zone]
  expression: sum(p, by=gen_zone) <= zone_cap
zonal_pullback:
  foreach: [snapshot, generator]
  where: "gen_zone == 'north' AND position(generator, by=gen_zone) == 0"
  expression: p <= at(spill * zone_cap, by=gen_zone)

$$\sum_{g \in \mathcal{G} : \mathrm{gen_zone}(g, t) = z} p_{t,g} \le \mathrm{zone_cap}_{z} \qquad \forall t \in \mathcal{T}, z \in \mathcal{Z}$$

$$p_{t,g} \le \mathit{spill}{t} \cdot \mathrm{zone_cap}{\mathrm{gen_zone}(g, t)} \qquad \forall t, g : \mathrm{gen_zone}(g, t) = \text{'north'} \wedge \mathrm{pos}_{\mathrm{gen_zone}(g, t)}(g) = 0$$

The pullback subscript reads the row's own snapshot, which is the reading composite over: [generator, snapshot] could not give at.

Where it lands
  • model.py: LookupBlock.per, the three declaration refusals in _lookup_targets; lookups_of returns the blocks so the legend can read per.
  • program.py: per on LookupDeclaration, GroupSum, At, DimensionPositionNode, LookupComparisonNode, LookupPairComparisonNode, LookupDefinedNode; _atom_dims reads a lookup leaf and a grouped position at per too.
  • resolution.py: Namespace.per_of, the list-must-share-per refusal, the pair-must-share-per refusal, per stamped on every node.
  • dimensions.py: _check_conditioned, called from the sum, at and translation rules.
  • lowering.py: passes per through.
  • typesetting/walk.py: _lookup takes the conditioning indices under the row's context; _domain for the legend.
  • docs: a per field row and a section on the dimensions page with the A lookup conditioned on a second dimension: per:, so a generator's zone may change by period #161 example and the rules; rows and sentences in operators, expressions, reading; notation regenerated.
  • tests: 4 declaration refusals, 2 call-site refusals (list, pair), 3 dim inferences, 3 dim refusals, 3 predicate readings, 1 lowering test over every node, and the golden case above in all three formats.
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: 1223 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.

Three conflicts against main were resolved by hand:

Why

A map keyed by over alone cannot say a zone that changes by period, so the relation had to be flattened into a 0/1 membership parameter, which carries no cardinality claim: a generator in two zones in one period was a legal model that said what the modeller did not mean (#161's 170.0 row). per: keeps the map a map, so the slip becomes a refusal at bind, and it lands on the one lookup block #422 leaves.

🤖 Generated with Claude Code

https://claude.ai/code/session_014FHQEdh9u2ppraSniF3iJd

… a generator's zone can change by period

`per:` on a lookup names the dimensions the map varies along besides
`over` and `into`. Every operator joins on them and passes them through:
`sum(by=)`, `at(by=)`, `shift(by=)`, `position(by=)` and a `where`
naming the lookup all require the operand to carry them. A `by=` list
and a two-lookup comparison must share them. The map binds single-valued
per `(over, *per)`, which is the consumer's check.

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

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: #275 is decided the other way. #437 merged as 3284926 and released in v0.0.0-alpha.89, so per: is not the spelling and lookups: is now relations:.

This was the second of three proposals on one base, and it lost on capability rather than on taste. From #437's own comparison, on the rows where the three differ:

capability this PR (per:) #437
a map that varies along a second dimension yes yes
the same table walked from its other key no — a second declaration yes
several value columns, walked to one no yes
a line's two ends in one table no yes
a self-map no yes
one calendar table partitioning at day, week and season no yes
single-valuedness checked at bind per (over, *per) per key tuple

What this PR argued for is kept: a map conditioned on a second dimension, and the bind check that refuses a generator on two buses where a 0/1 parameter would have doubled the sum in silence. It arrives as key: [generator, period] — #161, closed with it.

The five models the three proposals were compared on are #453.


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.

2 participants