Skip to content

Keep the years a PLEXOS plan states, and carry its constraints - #10

Closed
thomaschristopherking wants to merge 19 commits into
issue-116-plexos-pypsa-sienna-capacity-expansion-portfoliofrom
issue-116-plexos-dated-units-and-constraints
Closed

thomaschristopherking wants to merge 19 commits into
issue-116-plexos-pypsa-sienna-capacity-expansion-portfoliofrom
issue-116-plexos-dated-units-and-constraints

Conversation

@thomaschristopherking

@thomaschristopherking thomaschristopherking commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

TL;DR

A PLEXOS plan states when each unit arrives and leaves in a dated Units property, and what
the plan must meet in its Constraint objects — 604 dated generator rows and 186 Constraints
in the AEMO 2024 ISP file alone. stage_plexos_xml narrowed every dated property to the one
year being translated and map_constraints reported every Constraint as not carried, so all
of it stopped at the first leg. The source now stages every date band beside the value in
force, and a readable Constraint reaches the extensions sidecar.

Second of four for transition-zero/tz-infra-interop#116,
stacked on #9.

Files to review (24, +692 / -73):

File Why
interop/plugins/shared/plexos_pypsa_translations/_lifespan.py (new, start here) Walks the dated Units bands for the build year and the retirement year.
interop/plugins/sources/plexos_dated_properties.py The third output: every band, unclipped.
interop/core/extensions.py ExtensionKind.CONSTRAINT and the four models behind it.
interop/plugins/shared/plexos_pypsa_translations/_constraints.py Carries a Constraint instead of reporting it lost.
interop/plugins/steps/pypsa_to_sienna_map_components.py Relays it across the second leg, as it already relays a reserve.
interop/plugins/shared/plexos_dates.py (new) DateBand, which the source and the mapping both read.

Why

apply_window returns the value each property held when the window opened, and
_change_moments keeps only the band edges inside that window. A generator built in 2032
therefore reads Units = 0 in a 2026 run and is dropped as retired, and a generator that
retires in 2035 says nothing about it. Neither year survives to tell an expansion when
capacity arrives or leaves.

A Constraint fared worse: map_constraints read the sense, the right-hand sides, the
members and their coefficients, and then reported every one as not carried. PyPSA's
GlobalConstraint limits one carrier over the whole horizon and cannot name a set of
components, so an emissions budget over 40 named plants has no PyPSA field at all.

How

The source keeps a third table. apply_window still returns the value in force and the steps
within the window; alongside them the source now stages every resolved band with its
date_from and date_to, under PlexosResolvedTable.DATED_PROPERTIES. It is one row per
object per band, so it is component-scale and safe to collect.

build_year rides the hub, because PyPSA carries it on Generator, StorageUnit, Line and
Link and it survives the netCDF round trip as a real field. PyPSA has no retirement year, so
that travels in the sidecar — the same grounds that already put a reserve there.

A Constraint reaches the sidecar as a record rather than a PyPSA component, and the
operations leg relays it unchanged. ExtensionKind.RESERVE is the precedent: the concept
exists in PLEXOS and in Sienna and not in PyPSA, so the sidecar is what gets it across the
hub.

Reviewer notes

  • DateBand moved to interop/plugins/shared/plexos_dates.py. The import contracts
    forbid a step reaching into interop.plugins.sources, and the mapping now needs the same
    reading of what a band covers that the source has.
  • A constraint member carries its PLEXOS class as well as its name. One Constraint can
    weight a Generator and an Emission in the same sum, so a name alone does not identify what
    is being weighted.
  • A turbine gets a build year too. Pumped storage and reservoir hydro are PLEXOS
    Generators, so they read the same dated Units. The mapping document states the rule
    against the component the translation writes rather than repeating the row in three tables.
  • A generator running no units in the translated year is still dropped as retired, so its
    build year reaches the network only when it is also a candidate. Changing that skip belongs
    with the investments leg, not here.
  • The Constraint warning changed wording, since a readable Constraint is no longer lost.
    One log assertion and one decisions.md assertion moved with it.

Tests

Five new scenarios across dated_properties.feature and constraints.feature: a build year,
a retirement year, an object with neither, a Constraint reaching the sidecar with its sense,
its limits and its weighted members, and a Constraint left out for stating no sense. Full
suite (605), ruff, mypy, the import contracts and every plugin lint are green.

CI does not run on this pull request: the workflows trigger on a pull request into main,
and this one targets the branch below it in the stack.

Follow-up

The pypsa-to-sienna-investments leg and the portfolio sink, which read these records.


Open workspace in Conductor

thomaschristopherking and others added 13 commits September 8, 2026 17:13
Writes the agreed design before the work that implements it: what makes a
PLEXOS object a candidate, which half of the cost PyPSA works out for itself,
why one candidate becomes one technology, and why the portfolio is written
beside the base system it expands.

Three things in the SiennaSchemas investments namespace differ from what the
work was scoped against, and each one decides part of the design, so the spec
states them: there is no Node or Zone, the existing-fleet records are
supplemental attributes rather than components, and TechnologyFinancialData
has no field for a weighted average cost of capital.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
The PLEXOS reader named Units and Units Out and nothing else about building,
and every capacity mapping wrote p_nom_extendable False on purpose. So a model
stating what it may build lost all of it on the first leg.

PlexosProperty now names Build Cost, Max Units Built, WACC, Economic Life,
Technical Life and FO&M Charge, and Build Cost and FO&M Charge convert from
$/kW as they stage. Max Units Built above zero is what makes an object a
candidate, and the rule is the same for a Generator, a Battery and a
pumped-storage turbine, so one derivation serves all three.

PyPSA takes overnight_cost in preference to capital_cost and works the annuity
out from the discount rate and the lifetime, so this writes those four fields
and assembles no cost of its own. Economic Life is the recovery period PyPSA
annuitises across; Technical Life and the size of one unit have no PyPSA field
and travel in the extensions sidecar instead.

A candidate that has nothing yet takes the capacity it may build as its p_nom.
PyPSA reads p_nom only for a component whose capacity is fixed, and it is what
every per-unit field on the component is read against, so against nothing each
of those would come out at zero.

A candidate that states no Build Cost is left out: nothing prices building it,
so an expansion would take it for free. Each one is recorded, and one warning
names a few of them.

Every new column is null for an object the model cannot build, and the sink
omits a null column, so the dispatch pipelines write what they wrote before.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
The generator path and the storage path each wrote the expansion mapping, and
the storage copy inferred the size of one unit from the rated power. That
inference was wrong for two of the three classes it served. A pumped-storage
turbine's rated power is Max Capacity times Units, so a turbine with two 100 MW
units and one more to build got a ceiling of 600 MW where the model offers 300;
a battery's rated power is its Max Power alone, so the same expression read one
value as two different things and let a solver remove capacity the model states
as built. The turbine also never reached the rule at all when it stated Units =
0, because its rated power worked out to zero and the object was dropped as
unable to dispatch, which is exactly the candidate the documentation says the
translator carries.

The rated power now arrives as a pair: what the object already has, and what
one unit of it is. Each class states both for itself, so a Generator and a
turbine read Max Capacity as one unit and multiply by Units, and a Battery
reads Max Power. One module, _expansion.py, holds the rule the three share: the
extendable flag, the floor, the ceiling, the build cost, the discount rate, the
lifetime, the fixed cost, and the two values that travel in the sidecar. The
floor is the rated power the object has, and the ceiling adds Max Units Built
units of one unit's rated power to it. A candidate with nothing built takes the
capacity it may build as its p_nom, on the storage path as it already did on
the generator path. A mapping dataclass holds one ExpansionDecisions field and
adopts its column declarations, so the reported events and the destination row
still come from one walk.

A candidate that states a Build Cost but no WACC is now left out with the
Build Cost case. PyPSA's cost consistency check raises whenever an
overnight_cost is set and the discount rate is missing, whatever the strict
setting, and Network.optimize runs that check, so such a network cannot be
solved at all.

On the PyPSA to Sienna leg, base_power and the active power limits took
p_nom_opt from any extendable component. An unsolved network states zero there,
so every PLEXOS candidate this branch now writes would have translated at zero
capacity. The generator, renewable, storage and hydro mappings now guard that
choice with p_nom_opt above zero, as the link mapping already did and as the
mapping document already described.

Two generator scenarios asserted derivation text that named a generator; the
shared rule names an object, and they now assert that. Two scenarios stating a
Build Cost and no WACC gained one, since the translator no longer writes a
network PyPSA refuses. New scenarios cover a pumped-storage turbine that runs
two units and may build a third, and one that has yet to build anything.

Addresses r1-01, r1-02, r1-03, r1-07, r1-08, r1-09, r1-10, r1-11, r1-15, r1-16,
r1-17, r1-18, r1-19, r1-20, r1-21, r1-22, r1-23, r1-24, r1-25, r1-26, r1-27,
r1-29, r1-30 and r1-31.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
code-review/ is committed with the code it judges, so a teammate and a fresh
clone start with the rules this repository has earned rather than with none.
The README says what each file in it is for.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
…ne owner

The effective_p_nom rule stood in seven places across the PyPSA leg, and each
copy read a p_nom_opt of zero as an unsolved network. A solve that builds none
of an extendable component writes p_nom_opt 0; only a network no solve has
touched leaves the column out. Reading that 0 as unsolved carried the whole
candidate capacity into the Sienna operations system, adding a plant the
expansion refused. The rule now lives once in the pypsa_sienna_translations
_shared module, keyed on whether the network states a p_nom_opt at all, and the
generator, renewable, hydro, storage and link modules call it. The four
fill_defaults calls leave p_nom_opt null rather than filling it to zero, so an
unsolved network is still distinguishable.

A candidate battery divided its stated Capacity by the capacity it may build, so
its max_hours came out Max Units Built times too small. A Battery states its
Capacity beside one unit's Max Power, so both now per-unitise against that one
power, and the initial state of charge reads its percentage against p_nom times
max_hours rather than against one unit's energy.

A candidate that states no Economic Life is left out beside one that states no
Build Cost or no WACC. PyPSA's lifetime default is infinity, not NaN, so nothing
refuses the network and the build is annuitised as a perpetuity; a candidate
stating WACC 0 with no Economic Life would be built for free.

The rest is duplication and dead code. SourceGenerator no longer restates the
candidate rule in plain floats: it reads its p_nom off the Decision _expansion
builds, so the reported event and the emitted row cannot disagree. The warning
about an unpriced candidate moves beside that rule, so the generator path and
the storage path both cap it at a few names instead of the storage path warning
once per object. NOT_A_CANDIDATE becomes NOTHING_TO_REPORT, which is what a
genuine candidate stating no FO&M Charge also gets. ExpansionDecisions.is_candidate
and RatedObject.stated_units had no reader, and three docstrings restated their
own signature or a rule the code does not have.

The scenarios that reached the expansion path with a Build Cost and a WACC alone
now state an Economic Life, and one more candidate stands beside them to show
what leaving it out does. Two new assertions pin the numbers the fixes change: a
candidate battery's max_hours and initial level, and the base_power of an
extendable generator a solve built none of.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
…ispatch system

PLEXOS states a Battery's Max Power per unit, as the mapping document's own account of
what a candidate is already said, so a Battery running two units now reaches PyPSA at
twice that power rather than once. The units-out trace derates p_max_pu by
units_out / units, so p_nom has to stand for every unit before that derate means
anything. A Battery stating Units 0 and no Max Units Built now works out to no rated
power and is left out, with the reason recorded and warned about, as a turbine with no
units already was. (r3-01, r3-08)

The pypsa-to-sienna leg leaves out an extendable component no solve has sized. PyPSA
ignores the p_nom of an extendable component, and the network the PLEXOS leg writes puts
the capacity a candidate may build there, so reading it back gave a Sienna operations
system a plant nobody has built. A component a solve has sized still translates at its
p_nom_opt. (r3-06)

One carrier now reports every PLEXOS skip. SkippedComponent moves to decisions.py and
carries the group a warning speaks for, so the unpriced-candidate rule needs neither a
return type of its own nor a field of its own, and every generator skip warns as a
storage skip already did. (r3-02)

Deletes SourceGenerator.existing, which nothing read (r3-11), and two docstrings that
restated the line below them (r3-09, r3-10). The mapping documents now say which reading
of Max Power is correct, that a turbine's p_nom is Max Capacity times Units and nothing
else, that a candidate stating Units 0 is written rather than skipped, and that a
candidate does not reach a Sienna operations system. (r3-13, r3-14)

The scenarios follow the behaviour. The battery p_nom event cites Units beside
Max Power. The three scenarios asserting that p_nom_extendable round-trips into
extensions.json give their component a p_nom_opt, since a network no solve has touched
no longer translates its candidates. The infeasible-dispatch warning is now the note the
skip records, which says "sits above" where the warning it replaces said "is above".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
A dated Max Capacity becomes a p_max_pu series, scaled so the band in force reads
as a share of p_nom. The scale was the unit count the object runs, which a
candidate states as zero, so every snapshot of the band held the candidate at
zero and a solve could build none of it. The unit count p_nom stands for is what
the object runs, or what a candidate running none of them may build, which is the
reading derive_p_nom already takes, so CandidateSource states it once and the
dated rows scale by it. A scenario covers a candidate whose capacity rises during
the year: it fails against the old scale and passes against the new one.
Addresses r4-01 and r4-04.

_drop_unbuilt_candidates had one caller and two working lines, so they move into
_prepare_source beside the SkipRule it already applies (r4-09). The reason the
deleted docstring gave stands in unbuilt_candidate_skip, so no comment replaces
it (r4-06). Drops the paragraph defending the grouped warning in find_unpriced_build
(r4-07), and the two feature comments repeating the decisions.md text the same
scenario asserts (r4-08).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
A Generator, a Battery or a turbine that states a Max Units Built the model
prices nothing for was left out whole, so a plant already generating vanished
from the network because no expansion of it was priced. An object with units in
service now keeps the capacity it runs, with that capacity fixed, and only the
build is left out: decisions.md carries a NOT_MAPPED row naming the property the
model left out, and one warning names a few of the objects it happened to. An
object with no units yet is the build and nothing else, so it is still skipped.
(r5-04)

The battery's state_of_charge_initial named Capacity and Initial SoC while its
value came from the energy of every unit. The energy the battery holds is now a
Decision of its own, so p_nom and max_hours travel with the level and the
derivation states what the code does. (r5-01, r5-09)

Name a case the zero guard in the Sienna storage initial level can still meet,
since an extendable unit no solve has sized is now dropped before it. (r5-10)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
RatedPower carries the PLEXOS class and the capacity property, which is
everything the two rating functions differed by, so the rating becomes a
method on it and the callback field goes. The derivation string still reads
"Max Power * Units" and "Max Capacity * Units", built from the property the
constant names (r6-01).

Drops two docstrings that repeat what the name beside them already says
(r6-08, r6-09), and counts the skipped generator cases the mappings table
actually lists (r6-10).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
The pypsa-to-sienna scope rule read p_nom_extendable and p_nom_opt and nothing
else, so it left out every extendable component no solve had touched. A plant
that already generates and merely states what it may build states that running
capacity as p_nom_min, and PyPSA ignores the p_nom of an extendable component,
so the rule deleted real capacity from the Sienna system. The PLEXOS leg writes
exactly that shape for a plant with a Max Units Built.

A component belongs in the operations system when it holds capacity an
operations model may dispatch: any fixed component, an extendable one a solve
has sized, and now an extendable one whose p_nom_min is a capacity a build
cannot take away. effective_p_nom rates it at that floor, and the events name
p_nom_min as the attribute the rating came from. Only an extendable component
with neither a p_nom_opt nor a p_nom_min is a build the plan has yet to decide,
and that one is still left out and named in decisions.md. (r7-01)

Links read the same rating, so an extendable link with a floor now rates at it
rather than at a p_nom PyPSA never reads.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
The ledger is committed with the code it judges, so a rule earned here is a
rule a teammate and a fresh clone start with.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
…ndings' into issue-116-plexos-pypsa-sienna-capacity-expansion-portfolio

Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>

# Conflicts:
#	tests/features/plexos_to_pypsa/generators.feature
A PLEXOS expansion plan writes a schedule of dated Units: an object runs none of
itself until the year it is built, and its units fall back to zero in the year it
retires. The staging layer narrowed every dated property to the value in force
when the window opened and discarded every band edge outside it, so a generator
built in 2032 read as no units at all in a 2026 window and the year itself was
gone. The source now stages every resolved row a third time, unclipped and beside
the dates it applies between. The generator and battery mappings read the Units
bands off that table: the first year the units rise above zero becomes the
build_year PyPSA carries on the component, and the first year after that in which
they fall back to zero becomes the retirement_year in the extensions sidecar,
since PyPSA has no field for the other end of a plant's life.

A Constraint was read in full -- its sense, its right-hand sides, the objects it
names and the coefficients weighting them -- and then reported as carried nowhere.
PyPSA's GlobalConstraint limits one carrier over the whole horizon and cannot name
a set of components, which is the ground on which a reserve already travels in the
sidecar, so a constraint now travels there too: its sense as the inequality it
holds in, each right-hand side beside the span it applies over, each member with
the class its name belongs to and the coefficient weighting it, and whether the
expansion plan has to meet it. One stating no sense, or no right-hand side at all,
states no inequality to carry, so it is still left out and reported. The
operations leg relays the records on unchanged, so nothing reports them as dropped
on the way to Sienna.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
@coderabbitai

coderabbitai Bot commented Sep 8, 2026

Copy link
Copy Markdown

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@thomaschristopherking thomaschristopherking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review by /review-fix-cycle.

Labels follow Conventional Comments. (blocking) means the reviewer would not merge without it settled. (non-blocking) means it can land after.

Comment thread interop/plugins/shared/plexos_pypsa_translations/_lifespan.py Outdated
Comment thread interop/plugins/shared/plexos_pypsa_translations/_lifespan.py
Comment thread interop/plugins/sources/plexos_dated_properties.py
Comment thread interop/plugins/shared/plexos_pypsa_translations/_lifespan.py Outdated
Comment thread interop/plugins/shared/plexos_dates.py Outdated
Comment thread interop/plugins/shared/plexos_constants.py Outdated
Comment thread interop/plugins/shared/plexos_pypsa_translations/constants.py Outdated
Comment thread interop/plugins/shared/plexos_pypsa_translations/_lifespan.py Outdated
Comment thread interop/plugins/shared/pypsa_constants.py Outdated
Comment thread interop/plugins/shared/plexos_pypsa_translations/_lifespan.py
r1-01: `plexos_dates` now holds `latest_covering`, `band_edges` and `opens_at`,
and both the source's window narrowing and the lifespan reading call them. The
rule about what a band covers, and the rule that an undated band sorts first,
each stand in one place.

r1-02, r1-14, r1-16: the generator path derives the retirement year once and
reads the sidecar value off that decision, as the storage path and the expansion
fields already do. `Lifespan.build_year` and `Lifespan.retirement_year` go with
the second route. The reviewer's suggestion of moving the derivation into the
storage report does not work: that loop carries both batteries and hydro
turbines, and the two name different PLEXOS classes.

r1-03: `dated_rows` now stages only the rows of a property the model dates
somewhere. Staging every band of every property costs a multiple of the
`properties` table on a model with monthly bands, and an undated property states
the same value for all time, which `properties` already carries. Keeping every
dated property rather than only `Units` leaves the table usable by the next
reader.

r1-05: `ConstraintExtension.sense` takes a default, since `ExtensionLookup.get`
builds an empty record for a name the sidecar never mentioned and promises to
answer with a record rather than raise.

r1-17: the note against a Constraint that is left out now says why it is left
out. It stated that PyPSA's GlobalConstraint cannot express the limit, which is
no longer the reason anything is dropped, since a readable Constraint reaches
the sidecar.

r1-08, r1-09, r1-10, r1-11, r1-12, r1-13: delete docstrings that repeat a name,
argue for a module, narrate the line below, or say a third time what the
constants above already said.

r1-15: delete the unread `BUILD_YEAR` from `PyPSALineCol` and `PyPSALinkCol`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>

@thomaschristopherking thomaschristopherking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review by /review-fix-cycle.

Labels follow Conventional Comments. (blocking) means the reviewer would not merge without it settled. (non-blocking) means it can land after.

Comment thread interop/plugins/shared/plexos_pypsa_translations/_lifespan.py Outdated
Comment thread interop/plugins/sources/plexos_dated_properties.py Outdated
Comment thread interop/core/extensions.py Outdated
Comment thread interop/plugins/shared/pypsa_constants.py Outdated
Comment thread interop/plugins/shared/plexos_pypsa_translations/_lifespan.py Outdated
Comment thread interop/plugins/shared/plexos_dates.py Outdated
Comment thread interop/plugins/shared/plexos_pypsa_translations/_constraints.py Outdated
Comment thread interop/plugins/steps/plexos_to_pypsa/map_constraints.py Outdated
Comment thread tests/features/plexos_to_pypsa/constraints.feature Outdated
The build year and the retirement year are two ends of one concept, so they now
travel as one LifespanDecisions bundle beside the ExpansionDecisions bundle:
derive_lifespan states both ends, record_lifespan records the end the sidecar
carries, and each mapping declares one field. The generator path loses the local
dict of retirement years it built inside _carry_to_extensions and reads the ends
off the decisions it already derived (r2-01).

An undated band covers every moment, so latest_covering always finds one where
the property states any, and the fallback to the undated row could never run. It
goes, together with the parameter and the helper feeding it (r2-02).

A Sense that is not -1, 0 or 1 read as no Sense at all, so the report told the
reader the model stated nothing where it stated something the translator cannot
read. The Constraint keeps the code the model stated, the note says the code is
unreadable, and the description names it. A scenario covers it.

The map_constraints docstring said the step carries every Constraint, which a
Constraint stating no sense or no right-hand side does not (r2-11), and the two
sidecar scenarios in constraints.feature built the same model twice, so the
assertions of the second now stand on the first (r2-12).

Comments and docstrings that restated the code they sit on are gone: the
ConstraintPeriod paragraph, the two build_year schema comments, the
_states_a_date and latest_covering docstrings, and the _PERIODS heading (r2-06,
r2-07, r2-08, r2-09, r2-10).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>

@thomaschristopherking thomaschristopherking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review by /review-fix-cycle.

Labels follow Conventional Comments. (blocking) means the reviewer would not merge without it settled. (non-blocking) means it can land after.

Comment thread interop/plugins/shared/plexos_pypsa_translations/_storage_shared.py Outdated
Comment thread interop/plugins/sources/plexos_dated_properties.py Outdated
Comment thread interop/plugins/shared/plexos_pypsa_translations/_lifespan.py Outdated
Comment thread interop/plugins/shared/plexos_pypsa_translations/_constraints.py Outdated
Comment thread docs/translation_mappings/translation-from-plexos-to-pypsa.md Outdated
Comment thread interop/plugins/shared/plexos_pypsa_translations/_lifespan.py Outdated
… not do

StorageLookups stated the same five readings twice, once for a Battery and once
for a Generator. One ClassLookups per PLEXOS class holds them instead, and
staged(plexos_class, name) builds the StagedObject from whichever class the
caller names, so a new per-object reading costs one field rather than two.

The Units row of the PLEXOS to PyPSA mapping claimed a component running no
units in the year translated is left out. It is not: derive_p_nom gives a
candidate its buildable capacity where its existing rating is zero, so the
component is written with the build_year the schedule states. The
skipped-component table already gives the real rule, so the sentence goes.

_states_a_date guarded nothing. A band list that is all undated yields no band
edge, so it already read as NO_LIFESPAN, which is what both callers fall back
to for a name the map does not hold.

Three docstrings that only paraphrased what they sat above go with them.

Addresses r3-01, r3-03, r3-04, r3-05, r3-06, r3-07 and r3-08.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Thomas C. King <thomas.k@transitionzero.org>
@thomaschristopherking
thomaschristopherking added this pull request to stack #13 September 9, 2026 09:58
@thomaschristopherking thomaschristopherking added review-fix-running An automated review-and-fix cycle is running on this branch and removed review-fix-running An automated review-and-fix cycle is running on this branch labels Sep 9, 2026
@thomaschristopherking thomaschristopherking added review-fix-running An automated review-and-fix cycle is running on this branch and removed review-fix-running An automated review-and-fix cycle is running on this branch labels Sep 9, 2026
@thomaschristopherking thomaschristopherking added the review-fix-running An automated review-and-fix cycle is running on this branch label Sep 10, 2026
…pacity-expansion-portfolio' into issue-116-plexos-dated-units-and-constraints

# Conflicts:
#	code-review/ledger.jsonl
#	docs/specs/2026-09-08-plexos-sienna-investments-design.md
#	docs/translation_mappings/translation-from-plexos-to-pypsa.md
#	docs/translation_mappings/translation-from-pypsa-to-sienna.md
#	interop/plugins/shared/plexos_pypsa_translations/_batteries.py
#	interop/plugins/shared/plexos_pypsa_translations/_expansion.py
#	interop/plugins/shared/plexos_pypsa_translations/_generator_decisions.py
#	interop/plugins/shared/plexos_pypsa_translations/_generator_derivation.py
#	interop/plugins/shared/plexos_pypsa_translations/_generators.py
#	interop/plugins/shared/plexos_pypsa_translations/_storage_hydro.py
#	interop/plugins/shared/plexos_pypsa_translations/_storage_shared.py
#	interop/plugins/shared/plexos_pypsa_translations/_storage_units.py
#	interop/plugins/shared/plexos_pypsa_translations/decisions.py
#	interop/plugins/shared/pypsa_constants.py
#	interop/plugins/shared/pypsa_sienna_translations/__init__.py
#	interop/plugins/shared/pypsa_sienna_translations/_generators.py
#	interop/plugins/shared/pypsa_sienna_translations/_hydro.py
#	interop/plugins/shared/pypsa_sienna_translations/_links.py
#	interop/plugins/shared/pypsa_sienna_translations/_renewables.py
#	interop/plugins/shared/pypsa_sienna_translations/_shared.py
#	interop/plugins/shared/pypsa_sienna_translations/_storage.py
#	interop/plugins/sinks/emit_pypsa_network.py
#	tests/features/plexos_to_pypsa/generators.feature
#	tests/features/plexos_to_pypsa/storage_units.feature
#	tests/features/pypsa_to_sienna/thermal_generators.feature
@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

maintainability index

Gate: ✅ PASS
Score: A (85.01) mean over 246 files (function-weighted: 70% mean-of-functions, 30% file)
Lowest file: interop/adapters/inbound/interactive_cli/app.py at A (54.14)
Most strained function: interop/adapters/inbound/interactive_cli/app.py::_run_solve (139 lines, CC 22) at MI 33.5

All files grade A.

Full report
File Combined File MI Func mean Worst function
interop/adapters/inbound/interactive_cli/app.py 54.14 9.79 73.16 _run_solve (139ln, CC22)
interop/plugins/shared/pypsa_constants.py 55.28 55.28 55.28
interop/plugins/steps/sienna_to_pypsa/map_generators.py 57.45 29.65 69.37 _derive_thermal (66ln, CC10)
interop/plugins/validators/pypsa/bus_reference_integrity.py 60.63 68.17 57.39 validate (26ln, CC7)
interop/plugins/shared/plexos_pypsa_translations/_storage_hydro.py 63.17 46.18 70.45 _derive_turbine (29ln, CC3)
interop/plugins/steps/sienna_to_powersimulations/map_components.py 63.94 28.60 79.08 _map_bus_backed_component (134ln, CC15)
interop/plugins/steps/sienna_to_pypsa/map_transmission.py 64.18 38.69 75.10 _map_lines (52ln, CC5)
interop/plugins/shared/plexos_pypsa_translations/_generator_decisions.py 65.19 33.63 78.72 record_generator (28ln, CC8)
libs/interop-testing/src/interop_testing/steps/power_simulations.py 65.81 42.98 75.60 assert_h5_has_ts_association (37ln, CC6)
interop/plugins/sinks/emit_sienna_system_json.py 66.23 39.87 77.54 _build_bus_components (18ln, CC6)
interop/plugins/sources/stage_sienna_results.py 66.96 54.94 72.10 load_into_state (24ln, CC4)
interop/plugins/shared/plexos_pypsa_translations/_generator_derivation.py 67.10 30.80 82.65 _fuel_use (16ln, CC3)
interop/plugins/validators/pypsa/unique_names.py 67.29 82.81 60.63 validate (25ln, CC4)
interop/plugins/steps/pypsa_to_results_map_observations.py 67.34 43.27 77.65 _map_dispatch (46ln, CC5)
interop/di/discovery.py 67.95 35.46 81.88 _discover_off_the_shelf_plugins (34ln, CC3)
interop/plugins/sources/plexos_horizon.py 68.16 46.84 77.30 _model_horizon_id (14ln, CC8)
interop/plugins/steps/sienna_to_results/map_results.py 68.50 47.39 77.54 _map_storage_dispatch (36ln, CC4)
interop/plugins/sources/stage_plexos_xml.py 69.46 27.71 87.36 _property_row (27ln, CC4)
interop/plugins/validators/pypsa/generators.py 69.84 67.85 70.69 validate (13ln, CC2)
interop/adapters/outbound/markdown_report.py 70.28 40.84 82.89 _render_field (12ln, CC6)
interop/plugins/shared/plexos_pypsa_translations/_constraints.py 70.37 47.57 80.14 _carry (18ln, CC5)
interop/plugins/shared/plexos_pypsa_translations/_transmission.py 70.42 38.23 84.21 _read_transmission (24ln, CC6)
interop/plugins/validators/pypsa/storage_units.py 70.52 70.12 70.69 validate (13ln, CC2)
interop/plugins/sinks/emit_results_parquet.py 70.89 52.47 78.79 write (27ln, CC3)
libs/interop-testing/src/interop_testing/builders/pypsa_networks.py 70.89 33.58 86.89 add_generator (59ln, CC12)
interop/plugins/steps/sienna_to_pypsa/map_storage_units.py 71.09 41.90 83.60 _derive_phs (37ln, CC1)
interop/adapters/outbound/csv_report.py 71.24 44.99 82.50 _rows_for_event (15ln, CC7)
interop/adapters/inbound/interactive_cli/schema_prompts.py 71.28 51.67 79.68 _annotation_includes_path (9ln, CC6)
interop/plugins/validators/pypsa/lines.py 71.28 72.66 70.69 validate (13ln, CC2)
interop/plugins/shared/plexos_pypsa_translations/_batteries.py 71.34 54.35 78.62 _battery_initial_level (15ln, CC3)
interop/plugins/shared/plexos_pypsa_translations/_loads.py 71.35 41.35 84.20 _derive_loads (21ln, CC4)
interop/lints/plugin_filesystem.py 71.46 50.97 80.24 _function_violations (23ln, CC13)
interop/plugins/sources/plexos_csv_layouts.py 71.64 45.32 82.92 warn_unstageable_layout (10ln, CC3)
interop/plugins/shared/plexos_dates.py 72.01 49.98 81.44 covers (4ln, CC4)
interop/adapters/inbound/interactive_cli/history.py 72.06 49.18 81.86 record (11ln, CC3)
interop/plugins/validators/pypsa/links.py 72.11 75.42 70.69 validate (13ln, CC2)
interop/plugins/shared/pypsa_sienna_user_mappings.py 72.17 56.58 78.85 get_prime_mover_map (10ln, CC5)
interop/plugins/steps/pypsa_to_sienna_map_components.py 72.59 40.27 86.44 _map_links (36ln, CC3)
interop/plugins/steps/pypsa_to_sienna_relate_components.py 72.63 61.38 77.45 _build_arcs (21ln, CC5)
interop/plugins/shared/plexos_pypsa_translations/_generators.py 72.75 42.60 85.68 _map_one (28ln, CC8)
interop/plugins/sources/stage_pypsa_network_file.py 72.79 51.02 82.12 _stage_time_series (25ln, CC5)
libs/interop-testing/src/interop_testing/builders/plexos_tables.py 73.25 38.13 88.30 _add_property (44ln, CC7)
interop/templates/examples/pypsa/plugins/steps/normalise_carrier.py 73.38 66.49 76.33 run (42ln, CC7)
interop/plugins/steps/sienna_to_pypsa/map_loads.py 73.73 62.73 78.44 _record_load_time_series (22ln, CC4)
interop/plugins/shared/plexos_pypsa_translations/_expansion.py 74.05 44.69 86.63 derive_expansion (35ln, CC3)
interop/plugins/sinks/emit_pypsa_network.py 74.18 49.11 84.93 _add_buses (20ln, CC3)
libs/interop-testing/src/interop_testing/builders/sienna_systems.py 74.19 37.36 89.98 add_line (44ln, CC2)
interop/plugins/shared/plexos_pypsa_translations/_load_shedding.py 74.68 55.83 82.76 add_load_shedding_generators (20ln, CC7)
interop/plugins/steps/sienna_to_pypsa/relate_components.py 74.83 70.56 76.66 run (36ln, CC7)
interop/core/use_cases/compare.py 74.83 49.48 85.69 _coverage (21ln, CC2)
libs/interop-testing/src/interop_testing/steps/sienna_system.py 75.01 34.13 92.54 assert_h5_ts_values (17ln, CC6)
interop/plugins/shared/plexos_units.py 75.02 49.41 86.00 stated_units (12ln, CC6)
interop/lints/plugin_inheritance.py 75.18 55.35 83.67 _check_file (30ln, CC9)
interop/plugins/sinks/emit_power_simulations_h5_sidecar.py 75.21 54.42 84.13 write_h5 (70ln, CC7)
interop/plugins/sources/stage_sienna_system_json.py 75.24 52.18 85.12 load_into_state (22ln, CC4)
interop/adapters/outbound/julia_solver.py 75.25 45.89 87.83 _run_pipeline_inner (159ln, CC2)
interop/plugins/shared/plexos_sienna_user_mappings.py 75.39 67.26 78.88 _reject_unknown_storage_kinds (12ln, CC6)
interop/plugins/shared/pypsa_sienna_translations/_lines.py 75.40 45.84 88.07 fill_line_defaults (38ln, CC6)
libs/interop-testing/src/interop_testing/builders/plexos_csv_files.py 75.42 65.12 79.84 write_csv (10ln, CC5)
interop/plugins/shared/pypsa_sienna_translations/_ts_info.py 75.68 74.11 76.35 collect_ts_info (40ln, CC3)
interop/plugins/shared/plexos_pypsa_translations/_storage_shared.py 75.73 43.48 89.55 rate_object (22ln, CC6)
interop/plugins/shared/plexos_pypsa_translations/_shared.py 75.93 51.40 86.44 _membership_pairs (16ln, CC2)
interop/core/cli_overrides.py 76.14 57.10 84.30 __init__ (8ln, CC2)
interop/plugins/sources/stage_pypsa_network_ensemble.py 76.20 52.75 86.25 load_into_state (21ln, CC3)
interop/adapters/outbound/markdown_comparison_report.py 76.24 53.78 85.87 render (48ln, CC6)
interop/core/composition/document.py 76.33 47.16 88.83 validate_frameworks (9ln, CC3)
interop/core/adapters_config.py 76.61 58.20 84.50 load_adapters_config (14ln, CC6)
interop/plugins/shared/pypsa_destination.py 76.76 95.81 68.60 append_destination_rows (12ln, CC3)
interop/di/factories.py 77.21 42.31 92.17 make_adapter_factory (25ln, CC2)
interop/templates/examples/pypsa/plugins/sinks/emit_sienna_csv.py 77.34 74.49 78.56 write (11ln, CC5)
interop/plugins/shared/plexos_pypsa_translations/_reserves.py 77.58 50.89 89.02 _load_profiles (17ln, CC2)
interop/plugins/steps/sienna_to_powersimulations/_cost_events.py 77.89 57.47 86.65 _record_cost_curve_events (106ln, CC4)
interop/plugins/shared/plexos_pypsa_translations/_buses.py 78.13 59.75 86.00 map_buses (14ln, CC5)
libs/interop-testing/src/interop_testing/steps/pypsa_network/build_network.py 78.28 39.07 95.08 given_network_contains_generator (42ln, CC15)
interop/plugins/shared/translation_runner.py 78.38 56.08 87.94 _build_source_field (12ln, CC2)
interop/plugins/sinks/emit_power_simulations_system_json.py 78.47 63.71 84.79 _build_all_components (18ln, CC5)
interop/plugins/shared/pypsa_sienna_translations/_component_mapping.py 78.54 78.54 78.54
interop/core/use_cases/translate.py 78.80 50.23 91.04 _run_leg (14ln, CC1)
interop/plugins/shared/plexos_pypsa_translations/_storage_units.py 78.84 53.52 89.69 _dropped_values (21ln, CC3)
interop/core/composition/addressing.py 79.07 58.57 87.85 find_node (14ln, CC6)
interop/core/user_mappings_loader.py 79.09 52.67 90.41 _load (17ln, CC3)
interop/plugins/sources/plexos_dated_properties.py 79.19 58.39 88.10 _stepped_frame (16ln, CC4)
interop/core/composition/planning.py 79.21 51.25 91.20 _reject_step_overrides_on_a_chain (9ln, CC3)
interop/plugins/validators/pypsa/loads.py 79.48 73.81 81.91 _check_static (13ln, CC2)
interop/plugins/sinks/emit_json.py 79.70 61.76 87.39 write (11ln, CC3)
interop/plugins/steps/plexos_sienna_mappings_to_carriers.py 79.75 60.11 88.17 _derive_from (13ln, CC3)
interop/core/use_cases/solve.py 79.85 57.87 89.27 _solve_sienna (28ln, CC3)
libs/interop-testing/src/interop_testing/builders/plexos_resources.py 79.93 41.88 96.24 _add_storage (12ln, CC3)
interop/plugins/shared/plexos_pypsa_translations/_generator_lookups.py 80.03 62.92 87.36 _aggregate_series (13ln, CC4)
interop/adapters/outbound/highs_network_solver.py 80.08 62.62 87.56 _zero_look_ahead_outputs (9ln, CC3)
interop/plugins/sinks/emit_power_simulations_files.py 80.12 57.66 89.75 write (20ln, CC2)
libs/interop-testing/src/interop_testing/steps/pypsa_network/_written_network.py 80.21 59.63 89.03 assert_series (15ln, CC5)
interop/plugins/shared/pypsa_sienna_translations/_hydro.py 80.24 59.59 89.09 _hydro_skips (10ln, CC2)
interop/adapters/inbound/headless_cli/app.py 80.26 59.54 89.15 run_headless (54ln, CC10)
libs/interop-testing/src/interop_testing/projects.py 80.46 75.31 82.67 write_project_plugin_in_subdir (5ln, CC1)
interop/plugins/shared/warning_text.py 80.52 97.41 73.29 name_a_few (7ln, CC2)
libs/interop-testing/src/interop_testing/steps/reports.py 80.66 66.14 86.89 assert_manifest_fields (5ln, CC4)
interop/plugins/shared/pypsa_sienna_translations/_loads.py 80.66 56.44 91.04 _load_max_active_power (44ln, CC6)
interop/plugins/shared/plexos_pypsa_translations/_lifespan.py 80.69 56.55 91.03 _derive_year (7ln, CC2)
interop/core/use_cases/init_project.py 80.84 68.96 85.93 __call__ (11ln, CC3)
interop/plugins/sinks/emit_sienna_files.py 80.95 54.53 92.27 write (9ln, CC2)
interop/plugins/shared/pypsa_sienna_translations/_ensemble.py 81.16 83.00 80.38 _report_partial (20ln, CC3)
libs/interop-testing/src/interop_testing/builders/plexos_models.py 81.38 50.10 94.78 save (12ln, CC5)
libs/interop-testing/src/interop_testing/steps/files.py 81.42 61.25 90.07 assert_json_array_length (10ln, CC4)
interop/plugins/sinks/emit_sienna_files_ensemble.py 81.49 72.11 85.52 replication_paths (8ln, CC1)
interop/plugins/shared/pypsa_sienna_translations/_links.py 81.54 53.45 93.58 link_in_scope (8ln, CC1)
interop/plugins/steps/caiso_plexos_to_results.py 81.54 51.18 94.56 run (10ln, CC3)
interop/templates/examples/pypsa/plugins/sources/stage_pypsa_csv.py 81.60 69.87 86.63 load_into_state (30ln, CC6)
interop/plugins/sinks/emit_pypsa_network_ensemble.py 81.69 68.98 87.13 write (12ln, CC3)
interop/plugins/shared/pypsa_time_series.py 81.72 57.60 92.05 series_timing (16ln, CC2)
interop/core/use_cases/solve_network.py 81.82 60.31 91.04 _last_day_of_window (10ln, CC5)
interop/plugins/shared/plexos_constants.py 81.93 75.42 84.72 is_plexos_true (3ln, CC1)
interop/plugins/shared/pypsa_sienna_translations/_storage.py 81.98 59.28 91.71 fill_storage_defaults (18ln, CC1)
interop/plugins/shared/sienna_constants.py 82.16 40.53 100.00 time_series_uuid (4ln, CC1)
interop/plugins/shared/pypsa_sienna_translations/_generators.py 82.20 48.47 96.66 build_generator_ts_association (41ln, CC4)
interop/plugins/shared/power_simulations_schema.py 82.75 42.49 100.00 get_new_uuid (2ln, CC1)
interop/plugins/sources/plexos_tables.py 82.77 89.69 79.80 class_id_of (6ln, CC3)
interop/plugins/sinks/emit_carrier_mappings.py 82.82 73.76 86.71 write (10ln, CC3)
interop/core/runner.py 82.93 57.99 93.62 _build_params (12ln, CC4)
interop/plugins/steps/sienna_to_powersimulations/fill_availability.py 82.94 63.82 91.13 run (12ln, CC4)
libs/interop-testing/src/interop_testing/steps/plexos_model.py 82.97 49.44 97.34 given_model_contains_late_fraction_period_column_data_file (13ln, CC1)
interop/logging_setup.py 83.46 85.61 82.54 configure_logging (14ln, CC5)
interop/core/extensions.py 83.51 49.21 98.21 report_unconsumed (6ln, CC4)
interop/ports/inbound/translate.py 83.65 72.41 88.47 _describe_files (9ln, CC4)
interop/plugins/sinks/emit_sienna_h5_sidecar.py 84.09 66.06 91.82 _association_rows (5ln, CC3)
interop/plugins/shared/pypsa_sienna_translations/_renewables.py 84.14 61.90 93.67 _renewable_cost_loss (25ln, CC2)
interop/plugins/shared/plexos_pypsa_translations/_ensemble.py 84.35 81.18 85.71 choose_ensemble_samples (25ln, CC4)
interop/plugins/shared/plexos_pypsa_translations/constants.py 84.51 84.51 84.51
interop/di/container.py 84.65 53.11 98.17 reporter_builder (27ln, CC1)
interop/plugins/shared/staged_samples.py 84.81 77.03 88.15 filter_to_sample (8ln, CC3)
interop/plugins/sources/stage_caiso_plexos_stack_model.py 85.12 74.39 89.72 load_into_state (15ln, CC2)
interop/plugins/shared/pypsa_sienna_translations/_shared.py 85.14 61.16 95.42 ts_association_row (33ln, CC2)
interop/plugins/shared/plexos_pypsa_translations/decisions.py 85.17 58.30 96.69 record (15ln, CC5)
libs/interop-testing/src/interop_testing/builders/sienna_documents.py 85.70 59.40 96.97 find_sienna_component (10ln, CC6)
interop/plugins/shared/sienna_pypsa_translations/mapping.py 85.90 83.97 86.72 bus_id_to_v_nom (10ln, CC3)
interop/adapters/outbound/markdown_validation_report.py 85.95 70.49 92.58 render (10ln, CC3)
interop/core/use_cases/validate.py 86.20 71.01 92.70 __call__ (37ln, CC2)
libs/interop-testing/src/interop_testing/steps/pypsa_network/assert_network.py 86.33 56.37 99.17 assert_pypsa_network_empty (7ln, CC5)
interop/plugins/shared/pypsa_sienna_translations/_buses.py 86.53 55.09 100.00 fill_bus_defaults (18ln, CC1)
interop/ports/inbound/solve.py 86.60 71.34 93.14 is_success (6ln, CC2)
interop/plugins/shared/plexos_pypsa_translations/_storage_turbines.py 87.00 86.80 87.09 _states_pump_efficiency (12ln, CC2)
interop/ports/outbound/validation.py 87.04 85.79 87.58 __init__ (8ln, CC3)
interop/plugins/shared/validators.py 87.06 89.30 86.10 check_column_bounds (27ln, CC4)
libs/interop-testing/src/interop_testing/pipeline_driver.py 87.08 90.22 85.73 run_pipeline (28ln, CC5)
interop/plugins/shared/extensions_sidecar.py 87.59 77.36 91.98 _stage_extensions_sidecar (7ln, CC3)
interop/main.py 87.60 85.79 88.37 app (31ln, CC3)
interop/plugins/steps/plexos_to_pypsa/map_components.py 87.73 71.41 94.73 _models_with_a_horizon (9ln, CC2)
libs/interop-testing/src/interop_testing/builders/plexos_generator_specs.py 87.74 70.96 94.93 _property_values (6ln, CC3)
libs/interop-testing/src/interop_testing/builders/caiso_stack_models.py 87.79 67.85 96.34 _set_category (9ln, CC3)
interop/adapters/outbound/local_filesystem.py 87.93 64.75 97.86 _resolve (4ln, CC3)
interop/plugins/sinks/emit_extensions_json.py 87.94 84.09 89.59 write (7ln, CC2)
interop/core/pipeline.py 88.01 70.92 95.33 load (12ln, CC2)
libs/interop-testing/src/interop_testing/builders/sienna_results.py 88.54 68.01 97.33 save (13ln, CC4)
interop/adapters/inbound/headless_cli/overrides.py 88.57 76.75 93.64 parse_override_flags (16ln, CC6)
interop/core/use_cases/pipeline_catalog.py 89.75 72.22 97.26 _promptable_steps (5ln, CC2)
interop/plugins/steps/plexos_to_pypsa/map_loads.py 89.90 66.34 100.00 __init__ (7ln, CC2)
interop/plugins/sinks/noop.py 90.05 66.83 100.00 __init__ (2ln, CC1)
libs/interop-testing/src/interop_testing/files.py 90.10 87.03 91.42 navigate_json (26ln, CC7)
interop/plugins/shared/sienna_pypsa_translations/constants.py 90.13 86.66 91.61 pypsa_carrier (34ln, CC8)
interop/plugins/steps/plexos_to_pypsa/map_buses.py 90.24 67.47 100.00 __init__ (2ln, CC1)
interop/plugins/steps/plexos_to_pypsa/map_constraints.py 90.24 67.47 100.00 __init__ (2ln, CC1)
interop/plugins/steps/plexos_to_pypsa/map_reserves.py 90.24 67.47 100.00 __init__ (2ln, CC1)
interop/plugins/steps/plexos_to_pypsa/map_storage_units.py 90.24 67.47 100.00 __init__ (2ln, CC1)
interop/plugins/steps/plexos_to_pypsa/map_transmission.py 90.24 67.47 100.00 __init__ (2ln, CC1)
interop/ports/outbound/reporting.py 90.48 68.27 100.00 render (1ln, CC1)
libs/interop-testing/src/interop_testing/steps/pipeline.py 90.69 87.54 92.04 assert_log_does_not_contain (5ln, CC2)
interop/plugins/shared/framework_reporting.py 90.71 79.25 95.62 _derived (15ln, CC2)
interop/plugins/sources/noop.py 91.00 69.99 100.00 load_into_state (2ln, CC1)
interop/plugins/steps/sienna_to_pypsa/map_buses.py 91.42 75.42 98.27 map_buses (21ln, CC4)
interop/ports/outbound/filesystem.py 91.49 75.25 98.45 location_name (4ln, CC2)
interop/plugins/steps/noop.py 91.68 72.27 100.00 run (2ln, CC1)
interop/core/reporting.py 91.93 80.92 96.64 append (7ln, CC3)
interop/adapters/outbound/http_filesystem.py 92.33 78.45 98.29 __init__ (3ln, CC2)
interop/ports/outbound/comparison_report.py 93.42 78.08 100.00 render (1ln, CC1)
interop/plugins/sources/stage_plexos_sienna_mappings.py 93.47 78.24 100.00 _stated_row (3ln, CC1)
interop/ports/inbound/pipeline_catalog.py 93.52 78.40 100.00 by_framework_pair (3ln, CC1)
interop/plugins/shared/pypsa_sienna_translations/_arcs.py 94.18 80.61 100.00 build_arcs_source_table (27ln, CC1)
interop/plugins/shared/results_constants.py 94.33 94.33 94.33
interop/plugins/steps/sienna_to_pypsa/map_components.py 94.63 82.10 100.00 __init__ (3ln, CC1)
interop/plugins/shared/sienna_carrier_targets.py 95.54 85.12 100.00 carrier_target_discriminator (8ln, CC3)
interop/plugins/shared/pypsa_sienna_translations/_areas.py 96.13 87.11 100.00 build_areas_source_table (14ln, CC1)
interop/plugins/steps/plexos_to_pypsa/map_generators.py 98.09 93.63 100.00 __init__ (2ln, CC1)
interop/plugins/steps/plexos_to_pypsa_add_load_shedding.py 98.96 96.55 100.00 __init__ (2ln, CC1)
interop/__init__.py 100.00 100.00 100.00
interop/adapters/__init__.py 100.00 100.00 100.00
interop/adapters/inbound/__init__.py 100.00 100.00 100.00
interop/adapters/inbound/base.py 100.00 100.00 100.00 run (1ln, CC1)
interop/adapters/inbound/headless_cli/__init__.py 100.00 100.00 100.00
interop/adapters/inbound/interactive_cli/__init__.py 100.00 100.00 100.00
interop/adapters/outbound/__init__.py 100.00 100.00 100.00
interop/adapters/outbound/noop_report.py 100.00 100.00 100.00 render (2ln, CC1)
interop/adapters/outbound/write_tracking_filesystem.py 100.00 100.00 100.00 __init__ (3ln, CC1)
interop/core/__init__.py 100.00 100.00 100.00
interop/core/composition/__init__.py 100.00 100.00 100.00
interop/core/factories.py 100.00 100.00 100.00 __call__ (1ln, CC1)
interop/core/plugin_errors.py 100.00 100.00 100.00 __init__ (7ln, CC1)
interop/core/results_format.py 100.00 100.00 100.00
interop/core/use_cases/__init__.py 100.00 100.00 100.00
interop/core/user_mappings.py 100.00 100.00 100.00
interop/di/__init__.py 100.00 100.00 100.00
interop/lints/__init__.py 100.00 100.00 100.00
interop/plugins/__init__.py 100.00 100.00 100.00
interop/plugins/shared/__init__.py 100.00 100.00 100.00
interop/plugins/shared/caiso_plexos_constants.py 100.00 100.00 100.00
interop/plugins/shared/constants.py 100.00 100.00 100.00
interop/plugins/shared/plexos_pypsa_translations/__init__.py 100.00 100.00 100.00
interop/plugins/shared/pypsa_ensemble_manifest.py 100.00 100.00 100.00 dump_ensemble_manifest (2ln, CC1)
interop/plugins/shared/pypsa_sienna_translations/__init__.py 100.00 100.00 100.00
interop/plugins/shared/pypsa_sienna_translations/_prime_mover.py 100.00 100.00 100.00 enrich_prime_mover (16ln, CC3)
interop/plugins/shared/results_manifest.py 100.00 100.00 100.00 _timezone_must_be_iana (8ln, CC2)
interop/plugins/shared/sienna_pypsa_translations/__init__.py 100.00 100.00 100.00
interop/plugins/shared/sienna_pypsa_translations/reporters.py 100.00 100.00 100.00 record_dropped (4ln, CC1)
interop/plugins/shared/sienna_results_constants.py 100.00 100.00 100.00 _build_variable_csv (2ln, CC1)
interop/plugins/shared/sienna_to_powersimulations/__init__.py 100.00 100.00 100.00
interop/plugins/sinks/__init__.py 100.00 100.00 100.00
interop/plugins/sinks/_sienna_files.py 100.00 100.00 100.00 _serialise (8ln, CC1)
interop/plugins/sources/__init__.py 100.00 100.00 100.00
interop/plugins/steps/__init__.py 100.00 100.00 100.00
interop/plugins/steps/plexos_to_pypsa/__init__.py 100.00 100.00 100.00
interop/plugins/steps/sienna_to_powersimulations/__init__.py 100.00 100.00 100.00
interop/plugins/steps/sienna_to_pypsa/__init__.py 100.00 100.00 100.00
interop/plugins/steps/sienna_to_results/__init__.py 100.00 100.00 100.00
interop/plugins/validators/__init__.py 100.00 100.00 100.00
interop/plugins/validators/pypsa/__init__.py 100.00 100.00 100.00
interop/ports/__init__.py 100.00 100.00 100.00
interop/ports/errors.py 100.00 100.00 100.00 __init__ (8ln, CC1)
interop/ports/inbound/__init__.py 100.00 100.00 100.00
interop/ports/inbound/compare.py 100.00 100.00 100.00 summary (2ln, CC1)
interop/ports/inbound/init_project.py 100.00 100.00 100.00 __init__ (3ln, CC1)
interop/ports/inbound/overrides.py 100.00 100.00 100.00
interop/ports/inbound/validate.py 100.00 100.00 100.00 __call__ (10ln, CC1)
interop/ports/outbound/__init__.py 100.00 100.00 100.00
interop/ports/outbound/netcdf.py 100.00 100.00 100.00 netcdf_engine (11ln, CC3)
interop/ports/outbound/network_solver.py 100.00 100.00 100.00 solve (13ln, CC1)
interop/ports/outbound/solver.py 100.00 100.00 100.00 is_provisioned (7ln, CC1)
interop/ports/outbound/unit_commitment.py 100.00 100.00 100.00
interop/ports/outbound/validation_report.py 100.00 100.00 100.00 render (1ln, CC1)
libs/interop-testing/src/interop_testing/__init__.py 100.00 100.00 100.00
libs/interop-testing/src/interop_testing/builders/__init__.py 100.00 100.00 100.00
libs/interop-testing/src/interop_testing/builders/plexos_vocabulary.py 100.00 100.00 100.00
libs/interop-testing/src/interop_testing/steps/__init__.py 100.00 100.00 100.00
libs/interop-testing/src/interop_testing/steps/caiso_stack_model.py 100.00 100.00 100.00 given_caiso_stack_model (2ln, CC1)
libs/interop-testing/src/interop_testing/steps/isolation.py 100.00 100.00 100.00 isolated_cwd (5ln, CC1)
libs/interop-testing/src/interop_testing/steps/plexos_resources.py 100.00 100.00 100.00 given_model_contains_battery (17ln, CC1)
libs/interop-testing/src/interop_testing/steps/pypsa_network/__init__.py 100.00 100.00 100.00
libs/interop-testing/src/interop_testing/steps/sienna_results.py 100.00 100.00 100.00 _series (2ln, CC2)

@github-actions

github-actions Bot commented Sep 10, 2026

Copy link
Copy Markdown

Mutation testing report

Score: 79.4% (259 killed / 326 tested)

⚠️ The run was cut short with 663 mutants unchecked, so this score covers only the part that ran.

Status Count
🎉 Killed 259
🙁 Survived 67
🫥 No tests 1399
⏰ Timeout 37
🤔 Suspicious 0
🔇 Skipped 0
💥 Segfault 0
🧙 Caught by type check 974
❓ Not checked 663

Timeout-prone functions

Each timeout burns its whole budget (timeout_multiplier and timeout_constant in pyproject.toml) before mutmut kills it, so the functions below dominate wall-clock cost. Adding them to do_not_mutate is the fastest way to cut CI time.

Function Timeouts
interop.adapters.outbound.highs_network_solver.x__reported_cost 7
interop.adapters.outbound.highs_network_solver.x__zero_look_ahead_outputs 6
interop.adapters.outbound.highs_network_solver.xǁHighsNetworkSolverǁsnapshot_bounds 5
interop.core.use_cases.solve_network.xǁSolveNetworkUsingPortǁ_solve_one 3
interop.ports.outbound.netcdf.x_netcdf_engine 3
interop.adapters.outbound.highs_network_solver.x__warn_if_cut_short 3
interop.core.use_cases.solve_network.xǁSolveNetworkUsingPortǁ_resolve_bounds 2
interop.core.use_cases.solve_network.x_solve_windows 2
interop.core.use_cases.solve_network.x__last_day_of_window 1
interop.adapters.outbound.highs_network_solver.x__snapshot_coordinate 1
interop.adapters.outbound.highs_network_solver.x__solver_options 1
interop.adapters.outbound.highs_network_solver.x__record_reported_snapshots 1
interop.adapters.outbound.highs_network_solver.x__discard_look_ahead 1
interop.adapters.outbound.highs_network_solver.x__combine 1

Round 1 of the automatic review cycle. Addresses r1-01, r1-02, r1-03, r1-04,
r1-05, r1-06, r1-08 and r1-10.

r1-01: the merge with the parent branch left the unbuilt-candidate rule in two
places. _rows_in_scope already applies it to the same rows, so the copy in
_prepare_source dropped nothing and cost one Polars pass per mapping.

r1-02: a schedule whose Units return above zero after a dated zero band read as
a build in the return year. A generator running two units with a dated zero from
2035 to 2040 reported build_year 2041, although it runs in the year translated.
An object that runs before its first dated change now states no build year, and
one that runs again after a zero band states no retirement year. A new scenario
covers it, and the retirement label says "the last year" because the rule now
takes the last fall to zero.

r1-03: the Units row of the mapping document named a shape the translator drops.
A generator that runs no units and states no Max Units Built is left out as
retired, so no build_year reaches the network for it.

r1-04: GeneratorExtension and StorageExtension declared unit_size_mw and
technical_life_years again, which they inherit from ExpansionExtension.
retirement_year moves to the base class, where both records need it.

r1-05: _expansion.py holds the sidecar keys as constants now, rather than as
literals beside the constants that spell the same three strings.

r1-06: StorageLookups answers properties_of(plexos_class), so a caller stops
reaching through by_class. The docstring says that by_class holds the Battery
and the Generator alone.

r1-08 and r1-10: two docstrings that paraphrase the name they sit on go.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@thomaschristopherking thomaschristopherking left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automatic review-and-fix, round 1. Four reviewers ran over the diff against issue-116-plexos-pypsa-sienna-capacity-expansion-portfolio. Ten comments after merging the duplicates. I acted on eight and deferred two. Commit f982f34 carries the fixes.

id file claim severity
r1-01 interop/plugins/steps/pypsa_to_sienna_map_components.py:311 _prepare_source applies unbuilt_candidate_skip a second time to rows _rows_in_scope already filtered with the same rule. critical
r1-02 interop/plugins/shared/plexos_pypsa_translations/_lifespan.py:147 A schedule whose Units return above zero after a dated zero band is read as a build in the return year. major
r1-03 docs/translation_mappings/translation-from-plexos-to-pypsa.md:782 The Units row names a shape the generator mapping drops before any build_year is written. major
r1-04 interop/core/extensions.py:149 GeneratorExtension and StorageExtension redeclare two fields they inherit from ExpansionExtension. major
r1-05 interop/plugins/shared/plexos_pypsa_translations/constants.py:21 Two sidecar field constants have no reader, and _expansion.py holds the same strings as literals. major
r1-06 interop/plugins/shared/plexos_pypsa_translations/_storage_shared.py:200 A caller reaches through by_class for a battery's properties, and by_class holds no Storage entry. major
r1-08 interop/plugins/shared/plexos_pypsa_translations/_constraints.py:264 The docstring paraphrases the function name. minor
r1-10 interop/plugins/shared/plexos_pypsa_translations/_lifespan.py:60 The docstring restates the field types. nit

Two comments are deferred, not rejected. A rejection states the reader's taste, and this run has no reader in it.

  • r1-07 _constraints.py:106 (minor, thermo): _Constraint keeps the parsed sense beside the raw code, and three functions read the pair to decide which of three states the constraint is in. A _Sense value holding the value, the text and is_unreadable would state the three cases once.
  • r1-09 _lifespan.py:3 (nit, pruning): the first sentence of the module docstring packs both rules into one clause chain.

r1-01 and r1-04 are artefacts of the merge with the parent branch: this branch carried older copies of the parent's own commits, so the merge kept a rule and two fields in two places each.

@thomaschristopherking thomaschristopherking removed the review-fix-running An automated review-and-fix cycle is running on this branch label Sep 10, 2026
@thomaschristopherking

Copy link
Copy Markdown
Contributor Author

Superseded by #14, which squashes this whole stack into one branch off main. Closing this in favour of it.

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