Skip to content

Apply per-timeslice link limits as per-snapshot p_max_pu/p_min_pu series - #137

Open
nick-gorman wants to merge 12 commits into
mainfrom
apply-per-timeslice-link-limits
Open

Apply per-timeslice link limits as per-snapshot p_max_pu/p_min_pu series#137
nick-gorman wants to merge 12 commits into
mainfrom
apply-per-timeslice-link-limits

Conversation

@nick-gorman

@nick-gorman nick-gorman commented Aug 17, 2026

Copy link
Copy Markdown
Member

Functionality for adding per timeslice transmission limits to the pypsa network in the pypsa_build module.

Pypsa friendly tables of the form (abreviated data):

link_timeslice_limits:
    name            attribute  timeslice        value
    CQ-NQ_existing  p_max_pu   qld_peak_demand  0.857
    CQ-NQ_existing  p_max_pu   ,                1.0     # fallback
    CQ-NQ_existing  p_min_pu   ,                -0.714  # fallback only

timeslice_snapshots:
    timeslice_id     investment_periods  snapshots
    qld_peak_demand  2025                2025-01-13 12:00

Get transformed and inserted into the Pypsa model such that:

network.links_t.p_max_pu:
    investment_periods  snapshots         CQ-NQ_existing
    2025                2025-01-13 12:00  0.857
    2025                2025-01-15 12:00  1.0
        
network.links_t.p_min_pu:
    investment_periods  snapshots         CQ-NQ_existing
    2025                2025-01-13 12:00  -0.714
    2025                2025-01-15 12:00  -0.714

A few points:

  • The implemention is defensive such that if an non-extendable link doesn't have a limit value for each snapshot, from an explicitly named timeslice or a NaN fallback, then a error is raised. Might not be needed when validation is fully in-place, but it was an easy way to satisfy myself about the correctedness for now.
  • Also reached into the translator and changed how zero-capacity new corridors are treated, now they aren't a special case and just get timeslice limits set to zero.
  • One known gap: a limits table whose timeslice column is blank in every row comes back from CSV as float64 and can't be merged onto an empty timeslice_snapshots. That's a general CSV-round-trip typing problem (Column dtypes change on CSV round-trip; apply schema types when reading pypsa-friendly tables #138) and the fix belongs at the read boundary; an xfail test records it here.
  • I also realised that I was using timeslice and timeslice_id as column names, this PR works around it, but Rename the timeslices table's timeslice_id column to timeslice #140 will clean up to just use timeslice everywhere.
src/ispypsa/pypsa_build/links.py                          → new optional args + the per-snapshot expansion
src/ispypsa/translator/network.py                         → zero-capacity corridors get per-unit 0 instead of being skipped
tests/test_model/test_add_links_with_timeslice_limits.py  → new
tests/test_translator/test_network.py                     → zero-capacity test now expects the 0 rows, in the templater's named-timeslice shape

The new-format translator emits link limits per timeslice, in per-unit
form, in a link_timeslice_limits table rather than as per-link series.
Expanding them into per-snapshot p_max_pu / p_min_pu here at network
build time, via the timeslice_snapshots mapping, keeps the
pypsa-friendly directory small and reuses the same mapping that scopes
the custom constraints.

Each (link, attribute) series is seeded from the timeslice = NaN fallback
row and the named timeslices are written over it. Seeding from the
fallback rather than the links table's static value matters: since #126
the translator sets p_nom = max(forward, reverse) and ships p_max_pu=1.0
/ p_min_pu=0.0 only as placeholders, so a series that fell back to them
would over-permit forward flow and disable reverse flow at every
snapshot no named timeslice covers.

Dark until the orchestrator wiring lands: _add_links_to_network keeps
its two-argument form for the current path.

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

codecov Bot commented Aug 17, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
src/ispypsa/pypsa_build/links.py 100.00% <100.00%> (ø)
src/ispypsa/translator/network.py 100.00% <100.00%> (ø)

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

nick-gorman and others added 6 commits August 17, 2026 16:51
…ling for flag retirement

"None when all link limits are static" read as a per-run choice, but on
the new-format path the translator always emits limit rows and the links
table's p_max_pu / p_min_pu are placeholders, so omitting the table there
would silently mis-model every link. Spell out the two paths and mark the
None handling with the existing FEATURE_FLAG_CLEANUP convention.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The Args prose restated what the example can show directly: what each
table looks like, and that the function returns None because the network
is modified in place.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…s its caller

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

Filtering zero-p_nom links out of link_timeslice_limits made them the one
class of link pypsa_build had to know about: their placeholder p_max_pu /
p_min_pu did real work, and every consumer needed a caveat for links with
no limit rows. Defining per-unit-of-zero as 0 at the point of division
lets them flow through pypsa_build like any other link.

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

Seeding each series from the links table's placeholder p_max_pu / p_min_pu
gave an uncovered snapshot a plausible value and hid the gap. The series
are now built as one long table - every (link, attribute) pair at every
snapshot, named-timeslice values merged in where active, the blank-
timeslice fallback filled elsewhere - so a snapshot with neither is a NaN
that raises with the affected links named, and the intermediate table can
be asserted directly in tests.

The merge form also replaces the per-pair label lookups and overwrite
loop with plain pandas joins, so a named timeslice with no snapshots or a
pair with no fallback row falls out of merge semantics rather than needing
its own branch.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The to_string table made the message whitespace-sensitive; a plain
comma-separated (investment_period, snapshot) list reads the same and
lets the test pin the whole message.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nick-gorman and others added 5 commits August 18, 2026 12:39
An all-blank timeslice column read from CSV comes back float64 and can't be
merged onto the object-typed empty timeslice_snapshots. The proper fix is
schema-typed reading of the pypsa-friendly tables (#138), so
this records the case rather than casting at the merge site.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…by extent

The coverage check drew its universe from the limits table itself, so a
(link, attribute) with no rows at all was never checked and the links table's
placeholder p_max_pu / p_min_pu survived silently. The grid is now built from
the non-extendable links x both attributes, so an existing link the limits
never mention raises like a partially covered one does; expansion links keep
their static values and are not checked.

The error message now separates pairs undefined at every snapshot (typically no
limit rows at all) from those undefined at only some, and samples uncovered
snapshots only for the latter.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Per-cell lookups can miss stray columns or a placeholder leaking through; a
side-by-side frame with a non-placeholder p_min_pu pins that the links table's
static values really are the limits when no limit tables are passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The test described a blank-timeslice fallback of 0, but _new_parallel_path_rows
emits explicit zeros per direction and named timeslice with no fallback. Now
that zero rows reach pypsa_build's coverage check the two shapes behave
differently, so the test exercises the real one and keeps the fallback form
as a separate case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
_link_attribute_snapshot_grid read as a noun, so the orchestrator line
didn't say what the helper does. _create_* matches the verb-phrase
convention the other helpers follow.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011fNbcEWRpvRjndGUXvt4FV
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