feat: a lookup may be conditioned on a second dimension, so a generator's zone can change by period - #1567
feat: a lookup may be conditioned on a second dimension, so a generator's zone can change by period#1567FBumann wants to merge 1 commit into
Conversation
…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
Merging this PR will not alter performance
Comparing Footnotes
|
|
Note The following content was generated by AI. Closing: this landed as #1650, against the language What it asked for — a lookup conditioned on a second dimension — is Generated by Claude Code |
Note
The following content was generated by AI.
lookups: {zone_of: {over: generator, into: zone, per: [period]}}binds and builds on both lanes:overis consumed,intoproduced,perjoined on and passed through bysum,at,shift,sum_back,positionand everywhereatom. 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-specpin inpyproject.toml(anduv.lock) points at commit019430d0of 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.yamlnow mapscommitmentandfuel_useinto 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), eachpercolumn named after its dimension._read_relationchecks one row per(over, *per)and no null in any column;_check_keys_are_labelsruns on every key column. Thelk.target or lk.namefallbacks and thetarget is Nonebranch 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.
_mappingselects(over, *per, into…)and meets several coordinates on those keys;_remap_fragmentjoins on(consumed, *per);_empty_groupssubtracts the reached(into, *per)set from the product of targets crossed with the operand's other coordinates;_pulled_back_presencesjoins on(*into, *per)and the totality shortcut compares against the product of cardinalities over(over, *per);partitionedranks within(lookup, *per)and_Walkreads and lands on(dim, *per), with the shift edge keyed by(dim, *per, offset dims);join_lookupandjoin_group_offsetjoin on(over, *per); a short group is named(value, *per).linopy lane.
_lookup_arraysbuilds each lookup over(over, *per)reindexed onto every master coordinate, NaN where unmapped.operator_grouped_sumgroups by[*into, *per]; withperthe unmapped members are masked withwhere(present)rather thanisel-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 theisel+reindexpair for every rank._groupedpartitions the axis once perpercoordinate; the gathers,_group_offsetsand thewhereatoms broadcast on the wider arrays unchanged.One behaviour this widens in the unconditioned case too:
_empty_groupsnow 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,g1in two zones in 2030 refused at bind naming('g1', 2030); an empty(zone, period)group as a constant-side zero;at(by=)readingfloor[zone, period]back per period, and absent where the key is unmapped;shift,sum_backandpositionwithin(block, scenario)groups that differ per scenario; the short-group refusal named per scenario with one sentence on both lanes; fourwherepredicates 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, theby=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 andwhereon a lookup stays, rewritten to{over, into}:voltagemaps into akvdimension,periodinto aperioddimension.tests/test_position_of_dim.py:test_a_label_space_groups_a_position_like_a_targeted_lookupdeleted;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.mutateon0609eb9, 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).sources.py:381-389)sources.py:369-379)sources.py:285-286)overalone rather than(over, *per)(by hand)overalone, skipping thepercolumns (by hand)Verified, and how
Run with
pixi run --frozenin a fresh environment (this session's egress policy blocksconda-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 checkonsrc/: clean.docs-build: built strictly.tools.constructsandtools.gallery_mathregenerated the gallery for the changed port.Not run:
test-floors(the floors environment was not solved in this session).uv lockrefresheduv.lockfor the pin; the newer uv also rewrote a handful of platform markers on unrelated entries, left as the tool produced them.Deliberately not done
peronsum_back's orshift'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 aperdim joins on the column the frame already carries.where.unbound_lookup_messagestill 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