Skip to content

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

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

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

Conversation

@FBumann

@FBumann FBumann commented Sep 9, 2026

Copy link
Copy Markdown
Collaborator

Prompt: Lets implement both PRs.

Note

The following content was generated by AI.

lookups: {zone_of: {over: generator, into: zone, per: [period]}} binds and builds on both lanes: over is consumed, into produced, per joined on and passed through by sum, at, shift, sum_back, position and every where atom. The map is single-valued per (over, *per) at bind.

Cannot merge before energy-models/mathspec#422 and energy-models/mathspec#428 do. The math-spec pin in pyproject.toml (and uv.lock) points at commit 019430d0 of the branch carrying both; it must move to the release tag that carries them before this merges.

Breaking (the alpha's free kind): the label-space lookup kind ({over, dtype}) retired by math-spec#422 no longer loads. examples/ports/genx_piecewise_fuel.yaml now maps commitment and fuel_use into two declared dimensions, with the indices added to its data; the gallery page was regenerated.

What changed, per lane

The door (sources.py). A lookup's relation carries (over, *per, into), each per column named after its dimension. _read_relation checks one row per (over, *per) and no null in any column; _check_keys_are_labels runs on every key column. The lk.target or lk.name fallbacks and the target is None branch are gone with the kind they served. A dimension a lookup is conditioned per counts as an author of it for the "maps but no labels" refusal.

Relational lane. _mapping selects (over, *per, into…) and meets several coordinates on those keys; _remap_fragment joins on (consumed, *per); _empty_groups subtracts the reached (into, *per) set from the product of targets crossed with the operand's other coordinates; _pulled_back_presences joins on (*into, *per) and the totality shortcut compares against the product of cardinalities over (over, *per); partitioned ranks within (lookup, *per) and _Walk reads and lands on (dim, *per), with the shift edge keyed by (dim, *per, offset dims); join_lookup and join_group_offset join on (over, *per); a short group is named (value, *per).

linopy lane. _lookup_arrays builds each lookup over (over, *per) reindexed onto every master coordinate, NaN where unmapped. operator_grouped_sum groups by [*into, *per]; with per the unmapped members are masked with where(present) rather than isel-dropped, since linopy's multi-key path drops a NaN key where its single-key path refuses one (verified on linopy 0.9.1 / xarray 2026.7). operator_at's partial path fills the unmapped indexer cells with any label and masks the result there, which replaces the isel + reindex pair for every rank. _grouped partitions the axis once per per coordinate; the gathers, _group_offsets and the where atoms broadcast on the wider arrays unchanged.

One behaviour this widens in the unconditioned case too: _empty_groups now reads the spanned coordinates off the operand rather than the grouped frame, so a coordinate at which the operand has rows but no member maps gets the empty sum (0) at every target, which is what the eager lane already answered there.

Tests, and what coverage moved where
  • tests/test_lookup_per.py (new): the math-spec#161 model on both lanes — honest map 220.0, frozen at 2030 140.0, g1 in two zones in 2030 refused at bind naming ('g1', 2030); an empty (zone, period) group as a constant-side zero; at(by=) reading floor[zone, period] back per period, and absent where the key is unmapped; shift, sum_back and position within (block, scenario) groups that differ per scenario; the short-group refusal named per scenario with one sentence on both lanes; four where predicates over a conditioned lookup; the relation's column, key-label and null checks.
  • tests/test_label_coords.py → tests/test_lookups.py: the label-space kind is gone, so the tests of it went with it (parsing the two kinds apart, the promotion rewrite, the by= typo filter, the "label space wearing a dimension's clothes" advice, the pair-comparison cases naming a label space) — math-spec#422 owns those rules now. Everything about the attach contract and where on a lookup stays, rewritten to {over, into}: voltage maps into a kv dimension, period into a period dimension.
  • tests/test_position_of_dim.py: test_a_label_space_groups_a_position_like_a_targeted_lookup deleted; position(by=) through a targeted lookup is the rest of that file.
  • tests/test_resolution_parity.py's coverage map repointed at the renamed file; tests/test_assertions.py's ratchet lowered 211 → 208 for the deleted tests.
Mutation table — the bind-time guards

pixi run python -m tools.mutate on 0609eb9, full suite per row; the two by-hand rows are a changed constant the tool cannot express, taken with the same three precautions (clean tree, git checkout -- restore, __pycache__ dropped on both sides, tree checked clean after each).

mutation result
the single-valued-per-(over, *per) guard (sources.py:381-389) caught
the null-key guard (sources.py:369-379) caught
the key-is-a-label check on every key column (sources.py:285-286) caught
the uniqueness guard grouping on over alone rather than (over, *per) (by hand) caught
the key-is-a-label check on over alone, skipping the per columns (by hand) caught
Verified, and how

Run with pixi run --frozen in a fresh environment (this session's egress policy blocks conda-mapping.prefix.dev, so the environment was solved once with the mapping disabled and then run frozen):

  • pytest -q -n auto: 3764 passed, 251 skipped, 1 xfailed.
  • ruff check ., ruff format --check ., pyrefly check on src/: clean.
  • docs-build: built strictly.
  • tools.constructs and tools.gallery_math regenerated the gallery for the changed port.

Not run: test-floors (the floors environment was not solved in this session).

uv lock refreshed uv.lock for the pin; the newer uv also rewrote a handful of platform markers on unrelated entries, left as the tool produced them.

Deliberately not done
  • No per on sum_back's or shift's named width or offset beyond what the join keys already give: a per-group amount over the target dim is read through the lookup as before, and one over a per dim joins on the column the frame already carries.
  • where.unbound_lookup_message still names two columns; it is reached only when the door has not run, which nothing does.

🤖 Generated with Claude Code

https://claude.ai/code/session_01NnVYJhtcbzsvHyHVtX4oUq


Generated by Claude Code

…enerator's zone can change by period

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

codspeed Bot commented Sep 9, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 16 untouched benchmarks
⏩ 42 skipped benchmarks1


Comparing claude/lookup-per-keyword (0609eb9) with main (7dc8553)2

Open in CodSpeed

Footnotes

  1. 42 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

  2. No successful run was found on main (2928e08) during the generation of this report, so 7dc8553 was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩

@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 lpspec | 🛠️ Build #34473132 | 📁 Comparing 0609eb9 against latest (2928e08)

  🔍 Preview build  

2 files changed
± about/linopy/index.html
± examples/genx_piecewise_fuel/index.html

FBumann commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator Author

Note

The following content was generated by AI.

Closing: this landed as #1650, against the language main moved to.

What it asked for — a lookup conditioned on a second dimension — is relations: {zone_of: {columns: [generator, period, zone], key: [generator, period]}} now, so the per: keyword this branch adds has no place to go: the language spells the condition as a key of two columns since math-spec#437, and what was missing was the engine reading it. This branch's code is written against program.lookups, which the rename retired, and its math-spec pin (019430d0) no longer exists.


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

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants