Skip to content

Bring the translator and pypsa_build up to the new table formats#117

Open
nick-gorman wants to merge 8 commits into
update-network-resource-schema-formatsfrom
new-format-translator
Open

Bring the translator and pypsa_build up to the new table formats#117
nick-gorman wants to merge 8 commits into
update-network-resource-schema-formatsfrom
new-format-translator

Conversation

@nick-gorman

Copy link
Copy Markdown
Member

Brings the translator and pypsa_build up to speed with the new table formats, behind the use_new_table_format feature flag. With this change the new-format pipeline runs end to end — templater → translator → PyPSA network — for the network and custom-constraint functionality. Generators, batteries and timeseries are deliberately out of scope and remain the main follow-up.

Two ideas shape most of the diff. First, transmission limits and custom-constraint RHS values are timeslice-tagged in the new tables, so the model gains temporal scope: links get per-snapshot p_max_pu/p_min_pu series built from per-timeslice limits, and each custom-constraint RHS value becomes one linopy constraint per investment period (and timeslice), rather than a single constraint over everything. Second, the timeslice calendar is decoded per reference year: AEMO's PLEXOS calendar marks hot/typical/winter windows on absolute dates following their rolling reference-year sequence, and the templater inverts that (using Table 1 of the Draft 2026 ISP Market Model Instructions) into one month-day pattern per weather year, which the translator re-sequences to the configured reference_year_cycle — the same assignment used for demand and VRE traces, so peak-day limits land on the modelled peak days whatever cycle a user picks.

src/ispypsa/
├── templater/
│   ├── plexos/7.5/timeslice_RefYear5000.csv      ← AEMO's calendar, shipped with the other PLEXOS extracts
│   ├── manually_extracted_template_tables/7.5/
│   │   └── reference_year_sequence.csv           ← Table 1 transcription (planning year → reference year)
│   ├── timeslices.py                             ← decode: windows → per-reference-year patterns, fail-loud validation
│   └── create_template.py                        ← granularity-invariant output set; header-only CC tables when collapsed
├── translator/
│   ├── network.py                                ← geography → buses; paths/limits/expansion → links + per-unit limits
│   ├── timeslices.py                             ← re-sequence patterns to reference_year_cycle → snapshot mapping
│   ├── constraints.py                            ← per-period/timeslice constraint instances, relaxation generators
│   └── create_pypsa_friendly.py                  ← flag dispatch; snapshots from config alone (no traces yet)
├── pypsa_build/
│   ├── links.py                                  ← expand per-unit limits into p_max_pu/p_min_pu series
│   └── custom_constraints.py                     ← one linopy constraint per RHS row, scoped to its snapshots
└── validation/schemas/                           ← timeslices + custom_constraints{,_lhs,_rhs} contracts

Design choices a reviewer would otherwise have to reverse-engineer:

  • Link p_nom is the forward winter limit, with other timeslices as per-unit multipliers (which may exceed 1.0). Winter is the calendar's default season; snapshots outside any window keep the static value. Asymmetric reverse limits live in p_min_pu, not separate links.
  • Per-unit limits ship as two small tables (link_timeslice_limits + timeslice_snapshots) expanded in pypsa_build, rather than per-link series files from the translator. Same formulation, less I/O, and the mapping table doubles as the snapshot-restriction input for custom constraints.
  • The timeslice decode is fail-loud: every planning year assigned the same reference year must carry an identical window pattern, which proves the Table 1 transcription against all 33 usable years of the calendar. Planning years truncated at the calendar's 2058 horizon are dropped whole — their reference years recur earlier with complete windows.
  • Constraint LHS terms keep generator/battery names even though those components aren't translated yet: pypsa_build skips-and-logs missing components, and the logging silences itself once generator translation lands. Relaxation generators are period-aware (a generator built for 2040 can't relax a 2030 constraint instance), which the old single-constraint formulation couldn't express.
  • The templater emits the same table set at every granularity, with the PLEXOS custom-constraint tables header-only once sub-regions are collapsed. This reverses 9366945's granularity gating in favour of "all columns, no rows", so file lists and downstream consumers need no granularity awareness.
  • A configured reference_year_cycle outside the calendar's weather years (2011–2025, year-ending) raises at translation time rather than silently never binding any timeslice.

The commits are chunked to be reviewable in order: schemas → templater (timeslices, wiring) → translator (network, timeslices, constraints) → pypsa_build → orchestrator/CLI. Old-format behaviour is untouched; every site to delete when the flag is retired is marked FEATURE_FLAG_CLEANUP[use_new_table_format].

Known follow-ups: generator/battery translation for the new format (which also unlocks trace-based snapshot aggregation and timeseries), demand-trace geography at coarser granularities, and operational-phase wiring.

🤖 Generated with Claude Code

nick-gorman and others added 8 commits June 11, 2026 12:23
The templater has emitted custom_constraints, custom_constraints_lhs and
custom_constraints_rhs since the PLEXOS templater landed, but the tables
had no schemas, so user-supplied versions of them had no documented
contract. The schemas pin the vocabulary the translator now relies on:
direction senses (<=, >=, =), term types (including load, which
the 7.5 PLEXOS extract emits for Node Load Coefficient terms), the
timeslice cross-reference, and date_from semantics (the value active at
the start of an investment period applies for that whole period).

Also fixes network_expansion_options cross-referencing the non-existent
constraints_rhs table.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AEMO timeslice calendar (timeslice_RefYear5000.csv, shipped under
templater/plexos/ with the other PLEXOS data-package extracts) marks hot
day / typical summer / winter windows on absolute dates, with each
planning year's dates derived from the weather of the reference year
AEMO assigned to it. Templating the calendar as absolute windows would
bake AEMO's reference-year sequence into every run regardless of the
user's configured reference_year_cycle, so peak-day limits would fall on
days that aren't peaks in the modelled traces.

Instead the templater inverts the calendar: it assigns windows to the
planning year they start in, labels them via Table 1 of the Draft 2026
ISP Market Model Instructions (transcribed as the manually extracted
reference_year_sequence table, extended cyclically per AEMO's repeating
sequence), and emits one month-day window pattern per reference year for
the translator to re-sequence. Every occurrence of a reference year must
carry an identical pattern — the check that proves the Table 1 decode
against the data; it passes for all 33 usable planning years. Planning
years truncated at the calendar horizon (windows that never turn off)
are dropped whole: their reference years recur earlier with complete
windows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The new-format templater now emits the same table set at every
granularity: timeslices and costs_connection are wired in, and the
custom-constraint tables are emitted header-only at nem_regions /
single_region instead of being absent. This reverses the gating from
9366945 — the PLEXOS constraints are sub-regional export-group limits
with no meaningful representation once sub-regions are collapsed, but
writing "all columns, no rows" tables means list_templater_output_files
needs no granularity awareness and downstream consumers never check for
missing tables.

costs_connection is also now tracked in the output list — it was being
written but untracked, the same class of bug 9366945 fixed for the
constraint tables.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
network_geography becomes buses (REZ buses dropped when rezs is
attached_to_parent_node), and the path/limit/expansion tables become
links. The winter_reference limit is the link's static p_nom — winter is
the calendar's default season — with the other timeslices' forward and
reverse limits emitted as per-unit values in a link_timeslice_limits
table for pypsa_build to expand into p_max_pu/p_min_pu series (per-unit
values can exceed 1.0 when a season's limit tops winter's). Asymmetric
reverse limits land in p_min_pu rather than separate links.

Expansion options become one extendable link per (path, investment
period) with annuitised capital costs, gated by transmission_expansion /
rez_transmission_expansion according to whether the path connects a REZ.
Paths with no capacity data take
rez_to_sub_region_transmission_default_limit, replicating the existing
REZ behaviour.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The translator assigns a reference year to each model financial year
with the identical construct_reference_year_mapping call the trace
pipeline makes, so timeslice windows and demand/VRE traces always come
from the same weather years — the binding interaction between hot-day
limits and hot-day demand survives any configured cycle. The year before
the first model year also gets a pattern (the cycle's last reference
year, cycle-consistent) because winter windows run April-October and
must cover the first model year's July-September snapshots.

Month-day boundaries of 29 February (reference year 2024 carries them)
clamp to 28 February in non-leap model years. The wrap decision for
exclusive ends compares the month-day strings rather than the placed
dates, so clamping a one-day 02-28..02-29 window collapses it to empty
instead of stretching it out a year. A configured cycle requesting
reference years the timeslices table has no patterns for raises rather
than silently never binding.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Each RHS value becomes one constraint instance per investment period
(and per timeslice where tagged), with date_from resolved as the value
active at the start of each period. LHS terms keep generator and battery
names even though those components aren't translated yet — pypsa_build
skips-and-logs missing components, and the logging silences itself once
generator translation lands. The generators/batteries parameters are
accepted now as the future name-mapping hook.

Constraint-relaxation expansion options become dummy generators on
bus_for_custom_constraint_gens, with their LHS terms filtered per period
by build_year so a generator built for 2040 can't relax a 2030
constraint instance — an improvement over the old single-constraint
formulation, which couldn't express this. Extendable links and
relaxation generators get "_expansion_limit" constraints capping total
p_nom at the option's allowed expansion; the suffix keeps their names
clear of the parent constraints'.

Duplicate input rows and RHS rows without LHS pairs raise rather than
collapse silently; the "load" term type maps to Load/p, which
pypsa_build logs as unimplemented and skips, matching the old path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Links expand their per-unit timeslice limits into per-snapshot
p_max_pu/p_min_pu series at network-build time, using the
timeslice_snapshots mapping; snapshots outside any window keep the
static (winter) value. Doing the expansion here rather than shipping
per-link series from the translator keeps the pypsa-friendly directory
small and reuses the same mapping table that scopes the custom
constraints.

New-format custom constraints build one linopy constraint per RHS row,
restricting time-indexed variables to the row's investment period and
timeslice via label-based selection on (period, timestamp) pairs. LHS
terms for components not in the model (generators and batteries, until
their translation lands) are skipped with a log line each; constraint
instances with no snapshots in scope are skipped silently because the
translator already warns about timeslices that never apply. build.py
dispatches between the old and new constraint paths on the presence of
the investment_period column.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
create_pypsa_friendly_inputs dispatches to the network-tables path under
the use_new_table_format flag. Snapshots come straight from the temporal
config — aggregation methods needing demand traces aren't available
until generator translation lands — and the output set covers the
network and custom-constraint functionality only: no generators,
batteries, or timeseries yet, so the CLI skips the timeseries task and
list_timeseries_files returns nothing under the flag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@codecov

codecov Bot commented Jun 11, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.16342% with 30 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/ispypsa/translator/constraints.py 92.85% 6 Missing and 6 partials ⚠️
src/ispypsa/pypsa_build/custom_constraints.py 89.06% 5 Missing and 2 partials ⚠️
src/ispypsa/pypsa_build/build.py 57.14% 1 Missing and 2 partials ⚠️
src/ispypsa/templater/timeslices.py 97.14% 2 Missing ⚠️
src/ispypsa/translator/create_pypsa_friendly.py 92.00% 1 Missing and 1 partial ⚠️
src/ispypsa/translator/network.py 97.72% 1 Missing and 1 partial ⚠️
src/ispypsa/translator/timeslices.py 96.82% 1 Missing and 1 partial ⚠️
Files with missing lines Coverage Δ
src/ispypsa/pypsa_build/links.py 100.00% <100.00%> (ø)
src/ispypsa/templater/create_template.py 92.77% <100.00%> (ø)
...spypsa/templater/custom_constraints_from_plexos.py 100.00% <100.00%> (ø)
src/ispypsa/translator/mappings.py 100.00% <ø> (ø)
src/ispypsa/templater/timeslices.py 97.14% <97.14%> (ø)
src/ispypsa/translator/create_pypsa_friendly.py 93.03% <92.00%> (-0.25%) ⬇️
src/ispypsa/translator/network.py 97.72% <97.72%> (ø)
src/ispypsa/translator/timeslices.py 96.82% <96.82%> (ø)
src/ispypsa/pypsa_build/build.py 83.33% <57.14%> (-9.26%) ⬇️
src/ispypsa/pypsa_build/custom_constraints.py 85.57% <89.06%> (+5.57%) ⬆️
... and 1 more
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

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.

1 participant