Read a PLEXOS expansion plan and write an extendable component for it - #9
Read a PLEXOS expansion plan and write an extendable component for it#9thomaschristopherking wants to merge 21 commits into
Conversation
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: trueThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
thomaschristopherking
left a comment
There was a problem hiding this comment.
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.
thomaschristopherking
left a comment
There was a problem hiding this comment.
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.
thomaschristopherking
left a comment
There was a problem hiding this comment.
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.
Comments that do not anchor to the diff
docs/translation_mappings/translation-from-plexos-to-pypsa.md:376
[r3-13] issue (blocking): This paragraph says a Battery or a turbine with Units 0 is not written, which the branch's own candidate rule no longer does.
Now:
374 | SoC` are `dropped`, and the full energy capacity is available.
375 |
376 | A `Battery` or a turbine can have a rated power of zero. For example, `Units 0` puts a unit
377 | into storage. Such a unit cannot dispatch. The translator does not write it, and it makes a
378 | `COMPONENT_SKIPPED` event that gives the name of the unit.
379 |
380 | ## Pumped storage → `StorageUnit`
Fix: Qualify the paragraph: a rated power of zero skips the object only where it states no Max Units Built; a candidate with Units 0 takes the capacity it may build as its p_nom and is written. Net: 0 lines.
thomaschristopherking
left a comment
There was a problem hiding this comment.
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.
thomaschristopherking
left a comment
There was a problem hiding this comment.
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.
thomaschristopherking
left a comment
There was a problem hiding this comment.
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.
| def as_rate(stated: float | None, stated_unit: str | None) -> float | None: | ||
| """A rate the model writes as a percentage, read as the fraction the destination wants.""" | ||
| if stated is None: | ||
| return None | ||
| return stated / PERCENT if is_percent(stated_unit) else stated |
There was a problem hiding this comment.
[r6-02] suggestion (blocking): as_rate converts a WACC percentage at map time, but CANONICAL_UNIT already converts Build Cost, FO&M Charge and Economic Life at stage time.
Now:
162 |
163 |
164 | def as_rate(stated: float | None, stated_unit: str | None) -> float | None:
165 | """A rate the model writes as a percentage, read as the fraction the destination wants."""
166 | if stated is None:
167 | return None
168 | return stated / PERCENT if is_percent(stated_unit) else stated
169 |
170 |
Fix: The code now:
# _shared.py
def as_rate(stated: float | None, stated_unit: str | None) -> float | None:
if stated is None:
return None
return stated / PERCENT if is_percent(stated_unit) else stated
# _expansion.py
def _discount_rate(source: CandidateSource) -> Decision:
wacc = source.props.get(PlexosProperty.WACC)
rate = as_rate(wacc, source.stated_units.get(PlexosProperty.WACC))The code to use:
# plexos_units.py, beside the other four expansion properties
(PlexosCollection.GENERATORS, PlexosProperty.WACC): UNIT_FRACTION,
(PlexosCollection.BATTERIES, PlexosProperty.WACC): UNIT_FRACTION,
...
UNIT_FRACTION: {UNIT_FRACTION: 1.0, UNIT_PERCENT: 1.0 / _HUNDRED},
# _expansion.py
discount_rate=_from_property(source, PlexosProperty.WACC, None, DIRECT_DERIVATION),This deletes as_rate, the stated_units field on CandidateSource, StagedObject and SourceGenerator, and the gen_units, battery_units and generator_units lookups.
thomaschristopherking
left a comment
There was a problem hiding this comment.
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.
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>
ec9ed2f to
773fa30
Compare
|
Gate: ✅ PASS All files grade A. Full report
|
Mutation testing reportScore: 80.5% (343 killed / 426 tested)
Timeout-prone functionsEach timeout burns its whole budget (
|
|
review-fix-cycle heartbeat: 2026-09-10T15:57:41Z |
… no price Addresses r9-01, r9-02, r9-03, r9-04, r9-05, r9-06, r9-09, r9-10, r9-11, r9-12, r9-13 and r9-16. r9-01 and r9-02: `_find_unpriced_build` tested only that a property was present, so a candidate that states `Build Cost` 0 became an extendable component PyPSA prices at nothing, and one that states `Economic Life` 0 gave PyPSA an infinite annuity factor. `UnpricedBuild.prices_a_build` now reads a stated zero as no price. It keeps a stated `WACC` of 0, which the suggested fix would have dropped: a zero discount rate is the rate of a model that does not discount, and PyPSA prices such a build at overnight_cost / lifetime. `_names_unpriced` carries the source's own value, so `decisions.md` shows the zero the model stated. r9-03: `_map_links` applied no candidate drop, so an extendable Link no solve had sized reached Sienna rated at its `p_nom` while the generators beside it were dropped. r9-05: the candidate drop belongs to a whole source table, so it moves from `_prepare_source`, which runs one time for each mapping, up to `_scope_rules`. `fill_capacity_defaults` fills the four capacity columns for the group, so a new mapping cannot reach the drop without the column it reads. r9-04: `CapacityColumns` bundles the four column names the seven call sites each stated in full. r9-06: `warn_about_groups` in `decisions.py` now writes every grouped warning, and `SkipGroup.outcome` carries the words that differ. r9-09, r9-10, r9-11, r9-12 and r9-16: cut five docstrings back to the constraint each one states. r9-13: a feature step asserted `capital_cost` is 0, which is PyPSA's own default for a column this repository never writes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomaschristopherking
left a comment
There was a problem hiding this comment.
Automatic review-and-fix, round 9
Four reviewers ran against origin/main...HEAD. Commit 53e4227 acts on the
comments below.
Acted on
- r9-01 (
correctness, major)interop/plugins/shared/plexos_pypsa_translations/_expansion.py:296—_find_unpriced_buildtests only that a property is present, so a candidate statingBuild Cost0 becomes an extendable component whose expansion PyPSA prices at nothing. - r9-02 (
correctness, major)interop/plugins/shared/plexos_pypsa_translations/_expansion.py:219— A candidate statingEconomic Life0 passes_PRICES_A_BUILDand writeslifetime = 0, which makes PyPSA's annuity factor infinite. - r9-03 (
correctness, major)interop/plugins/steps/pypsa_to_sienna_map_components.py:312—_map_linksdoes not applyunbuilt_candidate_skip, so an extendable Link no solve has sized reaches Sienna rated at itsp_nomwhile the generators beside it are dropped. - r9-04 (
thermo, major)interop/plugins/shared/pypsa_sienna_translations/_shared.py:61— The four capacity column parameters hold the same four strings at all seven call sites, because PyPSA names these columns the same on a Generator, a StorageUnit and a Link. - r9-05 (
thermo, major)interop/plugins/steps/pypsa_to_sienna_map_components.py:311— The candidate drop applies to a whole source table, but the step applies it one time for each mapping, below the_scope_ruleslayer that already holds the drops a table shares. - r9-06 (
thermo, major)interop/plugins/shared/plexos_pypsa_translations/_expansion.py:263—warn_about_dropped_buildsgroups names, counts them and names a few, whichwarn_about_skipsindecisions.pyalready does. - r9-09 (
pruning, minor)interop/plugins/shared/plexos_pypsa_translations/_expansion.py:3— This paragraph argues for the shared module and narrates its calling convention, so it is reviewer justification. - r9-10 (
pruning, minor)interop/plugins/shared/plexos_pypsa_translations/_expansion.py:189— This docstring repeatsdocs/translation_mappings/translation-from-plexos-to-pypsa.mdalmost word for word, so it duplicates another artifact. - r9-11 (
pruning, minor)interop/plugins/shared/plexos_pypsa_translations/_expansion.py:256— This docstring lists the three statements below it, so it paraphrases the code. - r9-12 (
pruning, minor)interop/plugins/shared/pypsa_sienna_translations/_shared.py:84— The first three sentences repeatEFFECTIVE_P_NOM_DERIVATIONandUNBUILT_CANDIDATE_NOTE, so only the last clause earns its place. - r9-13 (
pointless, minor)tests/features/plexos_to_pypsa/generators.feature:517— The step asserts thatcapital_costis 0, which is PyPSA's own default for a column no part ofinteropever writes. - r9-16 (
pruning, nit)interop/plugins/shared/plexos_pypsa_translations/decisions.py:87— The summary line names the two branches of the loop below it, so it narrates the code.
Deferred
An automatic run acts only on a blocking comment, or on one that takes lines away.
These stay for a person to decide.
- r9-07 (
thermo, minor)interop/plugins/shared/plexos_pypsa_translations/_storage_shared.py:409—derive_max_hourstakes the rated power and the words that name that power as two separate parameters, and only the docstring keeps the two in agreement. - r9-08 (
correctness, minor)docs/translation_mappings/translation-from-pypsa-to-sienna.md:1385— The Link "Capacity Expansion Handling" section still states the rule this branch replaced, so the authoritative mapping document now describes the opposite of what_links.pydoes. - r9-14 (
pointless, minor)interop/plugins/shared/plexos_pypsa_translations/_generators.py:161—_carry_to_extensionssays it puts values in the sidecar, but the body also writes a console warning about builds the translator left out. - r9-15 (
pointless, minor)interop/plugins/shared/plexos_pypsa_translations/_storage_shared.py:368— The module docstring lists the guardsrate_objectapplies, and this branch adds a guard the list does not name.
Addresses r10-01, r10-02, r10-03, r10-04, r10-05, r10-06, r10-07, r10-08, r10-09, r10-10 and r10-12. r10-01: the `base_power` event named `p_nom` as its source while the value came from `p_nom_opt` or `p_nom_min`, so `decisions.md` gave a reader the wrong attribute and the wrong number. `direct_translation` now takes a callable `source_col`, so a translation whose expression reads a different column for each row names the column that row was read from. `base_power`, `active_power` and the hydro `active_power_limits` all use it, on generators, renewables, hydro units and storage units. r10-03: `_pick_capacity` states the three-way capacity choice one time. `effective_p_nom` reads it over the column values, `capacity_attribute` reads it over the column names, and `states_built_capacity` reads the same two tests through `has_solved_capacity` and `has_capacity_floor`. `with_effective_p_nom` writes the chosen column name into `_capacity_attribute`, which `rated_from` reads, so `choose_capacity_attribute` goes. r10-02: `fill_capacity_columns` fills the four capacity columns and `fill_capacity_defaults` rates the two columns from them. Each mapping's `fill_defaults` calls the first, and `_rows_in_scope` and `_map_links` call the second, so one layer owns the rating. r10-04: `DroppedBuild` held the same three fields as `SkippedComponent`, so it goes and `warn_about_dropped_builds` calls `warn_about_skips`. r10-10: `_CarrierGroup.capacity` had one value, so it goes with the `CapacityColumns` export it needed. r10-05, r10-06, r10-07, r10-08, r10-09 and r10-12: cut five docstrings and one comment sentence back to the fact each one states. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomaschristopherking
left a comment
There was a problem hiding this comment.
Automatic review-and-fix, round 10
Commit a4efa9d acts on the comments below.
Acted on
- r10-01 (
correctness, major)interop/plugins/shared/pypsa_sienna_translations/_generators.py:357— Thebase_powerevent namesp_nomas its source while the value it reports comes fromp_nom_minorp_nom_opt, sodecisions.mdrecords the wrong source attribute and the wrong source number. - r10-02 (
thermo, major)interop/plugins/steps/pypsa_to_sienna_map_components.py:255—_rows_in_scoperates the two enrichment columns for the whole source table, and then each mapping'sfill_defaultsrates the same two columns again. - r10-03 (
thermo, major)interop/plugins/shared/pypsa_sienna_translations/_shared.py:89—choose_capacity_attributewrites in Python the same three-way choice thateffective_p_nomwrites in Polars, andstates_built_capacitywrites the same two tests a third time. - r10-04 (
thermo, major)interop/plugins/shared/plexos_pypsa_translations/_expansion.py:226—DroppedBuildholds the same three fields asSkippedComponent, so this module states one record shape a second time. - r10-05 (
pruning, minor)interop/plugins/shared/pypsa_sienna_translations/_shared.py:57— TheCapacityColumnsdocstring paraphrases the class name and still does not say what the four fields hold. - r10-06 (
pruning, nit)interop/plugins/shared/pypsa_sienna_translations/_shared.py:65— The second sentence of the POWER_CAPACITY comment argues for the bundle and predicts a Line bundle no code has. - r10-07 (
pruning, minor)interop/plugins/shared/pypsa_sienna_translations/_shared.py:128— Thefill_capacity_defaultsdocstring restates the name and names neither column it adds. - r10-08 (
pruning, minor)interop/plugins/shared/plexos_pypsa_translations/decisions.py:90— Thewarn_about_groupsdocstring narrates the three statements below it. - r10-09 (
pruning, minor)interop/plugins/shared/plexos_pypsa_translations/_expansion.py:129— Thedropped_buildcomment repeats theDroppedBuilddocstring in the same file. - r10-10 (
pointless, minor)interop/plugins/steps/pypsa_to_sienna_map_components.py:125—_CarrierGroup.capacityis a configuration point with one value, and its type is the only use ofCapacityColumnsoutside its own package. - r10-12 (
pointless, nit)interop/plugins/shared/pypsa_sienna_translations/_shared.py:66— The second sentence of the POWER_CAPACITY comment argues for the design instead of stating a fact.
Deferred
An automatic run acts only on a blocking comment, or on one that takes lines away.
- r10-11 (
pointless, minor)interop/plugins/shared/plexos_pypsa_translations/_expansion.py:202— The class nameUnpricedBuildsays the opposite of what the class holds, which is a property that prices a build. - r10-13 (
pruning, nit)interop/plugins/shared/plexos_pypsa_translations/decisions.py:64— TheSkipGroupdocstring uses a metaphor and shorthand, so the reader cannot tell what groups the objects together.
Addresses r11-01, r11-02, r11-03, r11-04, r11-06, r11-07, r11-08, r11-09 and r11-10. r11-03: the `sienna-to-pypsa` leg wrote `p_nom_extendable` with no `p_nom_min`, so PyPSA ignored the capacity the component already held. The next `pypsa-to-sienna` hop then read it as a build the plan had not made and left it out, which loses every extendable ThermalStandard, RenewableDispatch, EnergyReservoirStorage and TwoTerminalGenericHVDCLine on a round trip. Each of the four rows now states `base_power` as `p_nom_min`, and each reporter records it. `LINKS_DESTINATION_SCHEMA` gains the `p_nom_min` column that needs. `tests/features/pypsa_to_sienna_to_pypsa.feature` drives both hops over one extendable generator, which is the chain that catches this. r11-01: `fill_link_defaults` stated the four PyPSA capacity defaults by hand. A Link names those columns as a Generator does, so it calls `fill_capacity_columns` with `POWER_CAPACITY` like every other table. r11-02: `direct_translation` took `source_col` as a string or a callable, and had to reject a callable with no `expr` at run time. `row_source_translation` now takes the per-row form with a required `expr`, and `direct_translation` calls it with a constant, so the rejected pair cannot be written. r11-04, r11-06 and r11-07: the `_find_unpriced_build` docstring, one paragraph of `translation-from-plexos-to-pypsa.md` and three of its table rows all stated the pricing rule before a stated zero counted as no price. r11-08, r11-09 and r11-10: cut three docstrings that narrate the code below them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomaschristopherking
left a comment
There was a problem hiding this comment.
Automatic review-and-fix, round 11
Commit ae62871 acts on the comments below.
Acted on
- r11-01 (
thermo, major)interop/plugins/shared/pypsa_sienna_translations/_links.py:93—fill_link_defaultsstates the four PyPSA capacity defaults by hand, andfill_capacity_columnsin the same package states the same four. - r11-02 (
thermo, major)interop/plugins/shared/translation_runner.py:50— Thesource_colunion lets a caller pass a callable with noexpr, sodirect_translationmust reject that pair at run time. - r11-03 (
correctness, major)interop/plugins/shared/pypsa_sienna_translations/_shared.py:199— The new candidate drop deletes every component that thesienna-to-pypsaleg wrote, because that leg restoresp_nom_extendablewithout ap_nom_minor ap_nom_opt. - r11-04 (
pruning, major)interop/plugins/shared/plexos_pypsa_translations/_expansion.py:271— The diff made this docstring wrong:_find_unpriced_buildno longer tests only for a property a candidate leaves out. - r11-06 (
pruning, major)docs/translation_mappings/translation-from-plexos-to-pypsa.md:746— This paragraph states the pricing rule the branch replaced, so the authoritative mapping document describes behaviour the code does not have. - r11-07 (
pruning, major)docs/translation_mappings/translation-from-plexos-to-pypsa.md:290— These three table rows state the pricing rule the branch replaced, because a stated zero now drops the same candidate as a missing property. - r11-08 (
pruning, minor)interop/plugins/shared/pypsa_sienna_translations/_shared.py:107— Therated_fromdocstring paraphrases the name and states what the one line below it shows. - r11-09 (
pruning, minor)interop/plugins/shared/pypsa_sienna_translations/_shared.py:88— The_pick_capacitydocstring names the three branches below it and the two callers below those. - r11-10 (
pruning, minor)interop/plugins/shared/plexos_pypsa_translations/decisions.py:105— Thewarn_about_skipssentence argues for doing both a record and a warning, and describes work this function does not do.
Deferred
An automatic run acts only on a blocking comment, or on one that takes lines away.
- r11-05 (
pruning, minor)interop/plugins/shared/plexos_pypsa_translations/_expansion.py:202— TheUnpricedBuilddocstring says stating the property is enough, but a stated zero prices no build. - r11-11 (
pruning, nit)interop/plugins/shared/plexos_pypsa_translations/decisions.py:70— Thereasondocstring shows a sentence that stops at , but the newoutcomefield means every warning continues past it. - r11-12 (
pointless, minor)interop/plugins/shared/plexos_pypsa_translations/decisions.py:78—SkippedComponentsays the mapping did not translate the object, butdropped_buildnow holds one for an object the mapping does write. - r11-13 (
pointless, nit)interop/plugins/shared/plexos_pypsa_translations/decisions.py:89— The module docstring says this module records decisions as events, but it now also writes the console warning.
Addresses r12-01 to r12-12. Round 11 wrote `p_nom_min` for an extendable component, and left three gaps. r12-01 and r12-02: `_add_links` copies only the columns it names, and it named no `p_nom_min`, so the link floor never reached the written network. `decisions.md` stated the floor while the file held PyPSA's default of 0, and the next `pypsa-to-sienna` hop dropped the link. r12-03 and r12-04: `_renewable_row` wrote `p_nom_min` and `_record_renewable` recorded no event for it, so the report named the floor of a thermal generator, a storage unit and a link, but not of a renewable. r12-05, r12-06 and r12-07: `_EXTENDABLE_FLOOR_DERIVATION` opened with `base_power -> p_nom_min`, but the link event names `active_power_limits_from.max` as its source and a Sienna HVDC line has no `base_power`. Each event already names its own source field, so the prefix goes. r12-08 and r12-09: `translation-from-pypsa-to-sienna.md` stated that v1 takes `p_nom_opt` and drops the rest, two lines above the paragraph that says `effective_p_nom` also reads `p_nom_min`. `translation-from-sienna-to-pypsa.md` still said an extendable component comes back with no floor, and its four tables named no `p_nom_min` row. r12-10: `rated_translation` in `_shared.py` binds the factory the four translation modules each built by hand. r12-11 and r12-12: two feature comments repeat what the run itself prints and what the steps beside them say. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomaschristopherking
left a comment
There was a problem hiding this comment.
Automatic review-and-fix, round 12
Commit 0631eb7 acts on every comment below. Nothing was deferred this round.
Three reviewers independently found the two gaps round 11 left: the link floor never
reached the written network, and a renewable's floor carried no event.
- r12-01 (
correctness, major)interop/plugins/sinks/emit_pypsa_network.py:259—_add_linksnever writesp_nom_min, so the link capacity floor thatmap_transmission.pyputs in the row is thrown away. - r12-02 (
pointless, major)interop/plugins/steps/sienna_to_pypsa/map_transmission.py:294— The Linkp_nom_minthe branch adds never reaches the written network, because_add_linkslists it in neitherrequirednoroptional. - r12-03 (
thermo, major)interop/plugins/steps/sienna_to_pypsa/map_generators.py:428—_renewable_rowwritesp_nom_min, but_record_renewablerecords no event for that value. - r12-04 (
correctness, major)interop/plugins/steps/sienna_to_pypsa/map_generators.py:428— A renewable generator getsp_nom_minwritten with no translation event. - r12-05 (
pruning, major)interop/plugins/shared/sienna_pypsa_translations/reporters.py:103—_EXTENDABLE_FLOOR_DERIVATIONopens withbase_power -> p_nom_min, but the link event it is shared with namesactive_power_limits_from.maxas its source. - r12-06 (
correctness, minor)interop/plugins/shared/sienna_pypsa_translations/reporters.py:964— The link event states the derivationbase_power -> p_nom_min, but a Sienna HVDC line has nobase_power. - r12-07 (
pointless, minor)interop/plugins/shared/sienna_pypsa_translations/reporters.py:960— The same derivation prefix names a field the link's own source field is not. - r12-08 (
pruning, major)docs/translation_mappings/translation-from-pypsa-to-sienna.md:82— This paragraph says the translation takesp_nom_optand drops the expansion parameters, but the paragraph two lines below sayseffective_p_nomalso readsp_nom_min. - r12-09 (
pruning, major)docs/translation_mappings/translation-from-sienna-to-pypsa.md:465— The authoritative mapping document for the Sienna to PyPSA leg is untouched and now describes behaviour the code no longer has. - r12-10 (
thermo, major)interop/plugins/shared/pypsa_sienna_translations/_generators.py:244— The_ratedpartial stands four times, becauserow_source_translationtakes a callable that holds one value at every call site. - r12-11 (
pruning, minor)tests/features/sienna_to_pypsa/generators.feature:170— This comment repeats_EXTENDABLE_FLOOR_DERIVATION, which the same translate run writes intodecisions.md. - r12-12 (
pruning, minor)tests/features/pypsa_to_sienna_to_pypsa.feature:35— This comment restates the two Gherkin steps it sits between.
…s floor Addresses r13-01 to r13-06, r13-08, r13-09, r13-12, r13-13 and r13-14. r13-06: a candidate Battery took its starting charge against the whole capacity it may build, so a PyPSA solve had to build part of it to hold a charge the translator invented. `RatedObject.running_power` gives the capacity the object already has, and the stored energy reads against that. A battery that runs no units now starts at 0 MWh, and a battery that already runs is unchanged. r13-05: an extendable Line reached the written network with no `s_nom_min`, so a solve could delete a line the Sienna system rates. It now carries the same floor a generator, a storage unit and a link carry, through `LINES_DESTINATION_SCHEMA`, `_add_lines` and `LineReporter.record_s_nom_min`. r13-03: the link `p_nom_min` event gave `p_nom` as the value of `active_power_limits_from.max`. The two differ wherever the sidecar states a `p_max_pu`, so `_LinkPowerLimits` now carries `limit_max` and the event names it. r13-04: `_extendable_floor` states the floor rule one time for a line and a link, and `p_nom_min` is one field on each generator and storage mapping that the record function and the row builder both read. r13-01: `_links.py` built the capacity choice as its own expression while the step had already written that value into `_effective_p_nom`. It reads the column. r13-02: `rated_translation` returned `Callable[..., Translation]`, which stopped mypy checking any argument at the eight call sites. It is a plain function now, and each module binds it with `partial` as it binds `direct_translation`. r13-08 and r13-09: eight rows of `translation-from-plexos-to-pypsa.md` stated rules the code stopped following, and contradicted the prose of the same document. `translation-from-sienna-to-pypsa.md` gains the `s_nom_min` row. r13-12, r13-13 and r13-14: one docstring repeated the one below it, one counted drops that have since grown, and the package exported two entry points onto one enrichment. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomaschristopherking
left a comment
There was a problem hiding this comment.
Automatic review-and-fix, round 13
Commit dd51a39 acts on the comments below.
Acted on
- r13-01 (
thermo, major)interop/plugins/shared/pypsa_sienna_translations/_links.py:196— The Link module builds the capacity choice as its own expression, but the step already wrote that value into the_effective_p_nomcolumn. - r13-02 (
thermo, major)interop/plugins/shared/pypsa_sienna_translations/_shared.py:113—rated_translationreturnsCallable[..., Translation], which stops mypy from checking any argument at the eight call sites. - r13-03 (
thermo, major)interop/plugins/shared/sienna_pypsa_translations/reporters.py:960— The linkp_nom_minevent givesp_nomas the value ofactive_power_limits_from.max, but the two differ when the sidecar states ap_max_pu. - r13-04 (
thermo, major)interop/plugins/steps/sienna_to_pypsa/map_generators.py:414— The rule that an extendable component takes itsbase_powerasp_nom_minstands twice per component. - r13-05 (
correctness, major)interop/plugins/steps/sienna_to_pypsa/map_transmission.py:240— An extendable Line reaches the written PyPSA network with nos_nom_min, so a solve can delete the line the Sienna system rates. - r13-06 (
correctness, major)interop/plugins/shared/plexos_pypsa_translations/_batteries.py:129— A candidate Battery gets astate_of_charge_initialcomputed from the whole capacity it may build, which forces a PyPSA solve to build part of it. - r13-08 (
pruning, major)docs/translation_mappings/translation-from-plexos-to-pypsa.md:262— Thep_nomrow is wrong: a generator that runs no units takesMax Capacity x Max Units Built, and theRatingrule no longer applies. - r13-09 (
pruning, major)docs/translation_mappings/translation-from-plexos-to-pypsa.md:273— These seven rows state a rule the code does not follow for a generator that already runs and prices no build. - r13-12 (
pruning, minor)interop/plugins/shared/pypsa_sienna_translations/_shared.py:114— Therated_translationdocstring restates the docstring ofrow_source_translation, which it calls two lines below. - r13-13 (
pointless, minor)interop/plugins/steps/pypsa_to_sienna_map_components.py:263— The_scope_rulesdocstring names three drops the table shares, but the list now returns four. - r13-14 (
pointless, nit)interop/plugins/shared/pypsa_sienna_translations/__init__.py:66— The package exports bothfill_capacity_defaultsandwith_effective_p_nomfor one call site each.
Deferred
An automatic run acts only on a blocking comment, or on one that takes lines away.
- r13-07 (
correctness, minor)interop/plugins/steps/sienna_to_pypsa/map_generators.py:281— A thermal generator whose sidecar states nocommittablegetscommittableFalse written with no translation event. - r13-10 (
pruning, minor)docs/translation_mappings/translation-from-pypsa-to-sienna.md:84— The sentence names four mappings that leave an unbuilt candidate out, but the link mapping applies the same drop. - r13-11 (
pruning, nit)docs/translation_mappings/translation-from-sienna-to-pypsa.md:469— The words 'of a solved network' name one of the two capacities the opposite direction writes intobase_power. - r13-15 (
pointless, nit)tests/features/plexos_to_pypsa/generators.feature:557— The generator nameUndiscounted_REZstands for two opposite outcomes twenty lines apart.
… power Addresses r14-01, r14-02, r14-04 to r14-09, r14-11, r14-13 to r14-15, r14-17 and r14-19. r14-05: PyPSA reads `fom_cost` as a charge for the whole modelled horizon and adds it to the annuitised build cost without scaling, so the PLEXOS `FO&M Charge` in $/MW/yr was wrong on any horizon other than a year. On the two-day horizon of the generator scenario the 15000 $/MW yearly charge stood beside a 564 $/MW build cost, so no candidate could ever be built. The charge now travels in the sidecar as `fom_charge_per_mw_year`, with an event naming why. r14-06: `_ramp_limit` divided by `base_power` with no guard, and this branch makes a `ThermalStandard` with `base_power` 0.0 an ordinary output of the opposite leg, so a round trip stopped on model data. It uses `per_unit_of`, which the same file already uses three lines below. r14-04: a candidate reservoir turbine started with the head reservoir's whole Initial Volume although it runs no units, which forced a solve to build capacity to hold it. It reads `running_power`, as the battery does. r14-01: `extendable_floor` in `sienna_pypsa_translations/mapping.py` states the floor rule one time for a generator, a renewable, a storage unit, a line and a link. r14-02: `rated_translation` only bound one argument on `row_source_translation`, and each of the four call sites wrapped it in `partial` again. The call sites bind that argument themselves. r14-07, r14-08 and r14-09: five sections of `translation-from-plexos-to-pypsa.md` stated rules the code stopped following, about the starting energy of a battery and a reservoir, the `p_nom` of an object that runs no units, and the `Rating` of a candidate. r14-11, r14-13, r14-14, r14-15 and r14-17: five docstrings that narrate the code below them or name one caller's expression. r14-19: three feature steps asserted PyPSA's own default for `p_nom_min`, so none of them could fail. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomaschristopherking
left a comment
There was a problem hiding this comment.
Automatic review-and-fix, round 14
Commit 4ae593c acts on the comments below.
Acted on
- r14-01 (
thermo, major)interop/plugins/steps/sienna_to_pypsa/map_transmission.py:60— One rule stands in five places: an extendable component keeps its rated capacity as the floor a build cannot take away. - r14-02 (
thermo, major)interop/plugins/shared/pypsa_sienna_translations/_shared.py:113—rated_translationonly bindssource_col_of=rated_from, and each of the four call sites wraps it inpartialagain. - r14-04 (
correctness, major)interop/plugins/shared/plexos_pypsa_translations/_storage_hydro.py:220— A candidate reservoir-hydro turbine starts with the head reservoir's whole Initial Volume although it runs no units. - r14-05 (
correctness, major)interop/plugins/shared/plexos_pypsa_translations/_expansion.py:168—fom_costcarries the PLEXOSFO&M Chargein $/MW/yr, but PyPSA readsfom_costas a charge for the whole modelled horizon. - r14-06 (
correctness, major)interop/plugins/steps/sienna_to_pypsa/map_generators.py:157—_ramp_limitdivides bybase_powerwith no guard, and a Sienna ThermalStandard withbase_power0.0 is an ordinary output of the opposite leg. - r14-07 (
pruning, major)docs/translation_mappings/translation-from-plexos-to-pypsa.md:360— Thestate_of_charge_initialrow and the paragraph below it say the starting energy reads againstp_nom x max_hours. - r14-08 (
pruning, major)docs/translation_mappings/translation-from-plexos-to-pypsa.md:393— The pumped-storage and Batteryp_nomrows statex Units, but an object that runs no units takesx Max Units Built. - r14-09 (
pruning, major)docs/translation_mappings/translation-from-plexos-to-pypsa.md:811— The rule that a staticRatingaboveMax Capacity x Unitsbecomes the capacity no longer holds for a generator that runs no units. - r14-11 (
pruning, minor)interop/plugins/shared/plexos_pypsa_translations/_storage_shared.py:434— Thederive_state_of_charge_initialdocstring repeats the signature and names a ceiling the battery caller no longer passes. - r14-13 (
pruning, minor)interop/plugins/steps/sienna_to_pypsa/map_transmission.py:61— The_extendable_floordocstring names the return value and narrates the one line below it. - r14-14 (
pruning, minor)interop/plugins/steps/pypsa_to_sienna_map_components.py:262— The first sentence of the_scope_rulesdocstring argues for one report per drop rather than stating a fact. - r14-15 (
pruning, minor)interop/plugins/shared/plexos_pypsa_translations/constants.py:21— The twoEXT_*docstrings carry the same sentence twice. - r14-17 (
pointless, minor)interop/plugins/shared/plexos_pypsa_translations/_batteries.py:131— The battery passesrunning_power * max_hoursas the clamp bound, but the callee docstring still saysp_nom * max_hours. - r14-19 (
pointless, minor)tests/features/plexos_to_pypsa/storage_units.feature:549—p_nom_min is 0asserts PyPSA's own default, so it passes whether or not the translator writes the value.
Deferred
An automatic run acts only on a blocking comment, or on one that takes lines away.
r14-20 also contradicts r14-01, which blocks and therefore wins.
- r14-03 (
thermo, minor)interop/plugins/shared/plexos_pypsa_translations/_batteries.py:110—_derive_batteryreaches through three objects for the unit size, then reads the running power through a property. - r14-10 (
pruning, minor)docs/translation_mappings/translation-from-plexos-to-pypsa.md:943— The glossthat is, / (Max Capacity x Units)states an old reading ofp_nom. - r14-12 (
pruning, nit)interop/plugins/shared/plexos_pypsa_translations/_storage_shared.py:354— Therunning_powerdocstring says again what the name says, and does not name the mistake it protects against. - r14-16 (
pruning, nit)docs/translation_mappings/translation-from-sienna-to-pypsa.md:332— Thes_nom_minrow writes the system base as words while every other row writes it as100. - r14-18 (
pointless, nit)interop/plugins/shared/plexos_pypsa_translations/decisions.py:89—warn_about_groupsis a public name whose only caller iswarn_about_skipsin the same file. - r14-20 (
pointless, nit)interop/plugins/steps/sienna_to_pypsa/map_transmission.py:60—_extendable_floorwraps a one-line conditional and its docstring restates the derivation the same call sites write.
…_cost column Addresses r15-01, r15-02, r15-05, r15-07 to r15-13. r15-05 (critical): a Sienna ThermalStandard whose `base_power` is 0.0 raised `ZeroDivisionError` in `_map_generators` and stopped the whole translation. This branch makes a zero `base_power` an ordinary output of the opposite leg, where a solve builds none of an extendable generator, so a round trip hit it. The three other divisions in the file already went through `per_unit_of`; this one did not. r15-01, r15-07, r15-09 and r15-13: round 14 sent the yearly charge to the sidecar and left `fom_cost` in two destination schemas and two sink lists with nothing to write it, so both tables carried an always-null column and the schema told a reader the translator fills a PyPSA field it does not fill. r15-02: `RatedObject` stated `name` and `properties` a second time, although its `candidate` field already held both, so a reader could not tell whether the two could differ. It reads them from the candidate, and `unit_size` gives `_derive_battery` a name for the value it reached three objects deep for. r15-10: round 13 changed the clamp bound to the energy of the units the object already runs, but `_CLAMPED_DERIVATION`, which `decisions.md` carries, still named `p_nom * max_hours`. For a candidate turbine those differ by the whole capacity it may build. r15-08: the design record said the translator writes the PyPSA `fom_cost` column, which no part of the branch now fills. r15-11 and r15-12: three feature comments each repeated a rule already stated in the same file or in the derivation the run itself prints. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomaschristopherking
left a comment
There was a problem hiding this comment.
Automatic review-and-fix, round 15
Commit e9ff964 acts on the comments below. This round found the run's first
critical: a zero base_power, which this branch makes an ordinary output of the
PyPSA to Sienna leg, stopped the Sienna to PyPSA leg with a ZeroDivisionError.
Acted on
- r15-05 (
correctness, critical)interop/plugins/steps/sienna_to_pypsa/map_generators.py:92— A Sienna ThermalStandard whosebase_poweris 0.0 raisesZeroDivisionErrorhere and stops the whole translation. - r15-01 (
thermo, major)interop/plugins/shared/pypsa_constants.py:489— No code writesfom_cost, so the two destination schemas and the two sink lists declare a column that stays null. - r15-09 (
pruning, major)interop/plugins/shared/pypsa_constants.py:482— The comment says the expansion columns are null only for a component whose capacity the source fixes, butfom_costis null for every component. - r15-13 (
pointless, minor)interop/plugins/shared/pypsa_constants.py:489— The branch adds afom_costcolumn to two schemas and to the sink, but no code writes a value into it. - r15-07 (
correctness, minor)interop/plugins/shared/pypsa_constants.py:489— No code writesfom_costany more, so the column stands in two schemas and two sink lists with nothing behind it. - r15-02 (
thermo, major)interop/plugins/shared/plexos_pypsa_translations/_storage_shared.py:350—RatedObjectstatesname,propertiesandp_noma second time, because itscandidatefield already holds all three. - r15-10 (
pruning, major)interop/plugins/shared/plexos_pypsa_translations/_storage_shared.py:91— The branch changed the clamp bound, so_CLAMPED_DERIVATION, which decisions.md carries, names a bound the code no longer applies. - r15-08 (
pruning, major)docs/specs/2026-09-08-plexos-sienna-investments-design.md:61— The design record says the translator writes the PyPSAfom_costcolumn, but the branch writes the yearly charge to the sidecar. - r15-11 (
pruning, minor)tests/features/plexos_to_pypsa/storage_units.feature:551— This comment states the same rule, over the same two numbers, as a comment earlier in the same file. - r15-12 (
pruning, minor)tests/features/plexos_to_pypsa/storage_units.feature:603— This comment is the fourth copy of the candidatep_nomrule.
Deferred
An automatic run acts only on a blocking comment, or on one that takes lines away.
- r15-03 (
thermo, minor)interop/plugins/steps/pypsa_to_sienna_map_components.py:515—_map_linksrepeats the table fill and the candidate drop that_rows_in_scopeand_scope_rulesalready hold. - r15-04 (
thermo, minor)interop/plugins/shared/pypsa_sienna_translations/_generators.py:101— The step fills the four capacity columns for the whole source table, and then each of the five mapping modules fills the same four again. - r15-06 (
correctness, minor)interop/plugins/shared/plexos_pypsa_translations/_expansion.py:245— The warning and the decisions.md note say the candidate states no Build Cost when the candidate states a Build Cost of zero.
…xists Addresses r16-01, r16-02, r16-05 to r16-10 and r16-12. r16-06: `has_capacity_floor` read PyPSA `p_nom_min` as capacity a component already holds, but PyPSA reads `p_nom_min` as the lower bound of the build, which a user also sets to force a minimum build on a candidate nobody has built. A generator with `p_nom` 0, `p_nom_min` 500 and no `p_nom_opt` reached Sienna as a 500 MVA plant. `capacity_floor` caps the bound by `p_nom`, so a brownfield plant keeps its floor and a forced build is left out as a candidate. r16-05: `build_hydro_ts_associations` divided by `efficiency_dispatch` with no guard, and `fill_defaults` replaces only a null or a NaN, so a stated 0 stopped the whole translation. r16-01: every caller passed `POWER_CAPACITY`, so the `columns` parameter on nine functions had one value. The four column names are module constants now. r16-02: `ExpansionExtension` holds the three sidecar fields that `GeneratorExtension` and `StorageExtension` each declared with the same three comments. r16-07 and r16-08: the design record said the operations steps skip every extendable component, and the `Units` row of `translation-from-plexos-to-sienna.md` still stated the retirement rule without the candidate qualifier its sibling document carries. r16-09, r16-10 and r16-12: two feature comments repeated another artifact, and three storage modules each built a logger no line reads. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomaschristopherking
left a comment
There was a problem hiding this comment.
Automatic review-and-fix, round 16
Commit 44fde5f acts on the comments below.
Acted on
- r16-06 (
correctness, major)interop/plugins/shared/pypsa_sienna_translations/_shared.py:84—has_capacity_floorreads PyPSAp_nom_minas capacity the component already holds, butp_nom_minis the lower bound of the build. - r16-05 (
correctness, major)interop/plugins/shared/pypsa_sienna_translations/_hydro.py:205—build_hydro_ts_associationsdivides byefficiency_dispatchwith no zero guard, so a source StorageUnit that states 0 stops the whole translation. - r16-01 (
thermo, major)interop/plugins/shared/pypsa_sienna_translations/_shared.py:64— Every caller passesPOWER_CAPACITY, so thecolumnsparameter on nine functions has one value. - r16-02 (
thermo, major)interop/core/extensions.py:159—StorageExtensiondeclares the same three expansion fields, with the same three comments, asGeneratorExtension. - r16-07 (
pruning, major)docs/specs/2026-09-08-plexos-sienna-investments-design.md:86— The design record states that the operations steps skip every extendable component, but the code keeps one that states a solved capacity or a floor. - r16-08 (
pruning, major)docs/translation_mappings/translation-from-plexos-to-sienna.md:446— TheUnitsrow states the old retirement rule, because aUnitsof zero beside aMax Units Builtis now a candidate. - r16-09 (
pruning, minor)tests/features/plexos_to_pypsa/generators.feature:513— This comment is a third copy of the reason a yearly charge travels in the sidecar. - r16-10 (
pruning, minor)tests/features/plexos_to_pypsa/generators.feature:517— This comment repeats the field comments onGeneratorExtension. - r16-12 (
pointless, nit)interop/plugins/shared/plexos_pypsa_translations/_storage_units.py:68— Three PLEXOS storage modules each build a module logger that no line reads.
Deferred
An automatic run acts only on a blocking comment, or on one that takes lines away.
- r16-03 (
thermo, minor)interop/plugins/shared/pypsa_sienna_translations/_generators.py:408—GENERATOR_APLbuilds by hand the event thatrow_source_translationbuilds, and only the derivation text changes per row. - r16-11 (
pointless, minor)interop/plugins/shared/plexos_pypsa_translations/decisions.py:189— Themapped_fieldsdocstring says a field declared withoutmaps_tois skipped, but a field declared withholdsrecurses. - r16-13 (
pointless, nit)interop/plugins/shared/plexos_pypsa_translations/_batteries.py:93—_SOC_FROM_PERCENT_DERIVATIONwrites out the whole text of_STORED_ENERGY_DERIVATION.
One thermo comment, on the double capacity fill in pypsa_to_sienna_map_components.py,
repeated r15-04 and was dropped rather than put a second time.
Addresses r17-01, r17-02, r17-05 to r17-12. r17-01 and r17-05 (critical): round 16 guarded the hydro budget division by answering 0.0, which did not remove the division. The h5 sink divides the inflow series by that scaling factor, so numpy wrote `inf` into `system_time_series_storage.h5` with only a warning, and `decisions.md` said nothing. A unit with no capacity to convert its inflow with, or no dispatch efficiency, is left out and reported instead, through the skip mechanism the mapping already has. `ComponentMapping.skips` holds a sequence, because one table now states two rules. A stated `efficiency_dispatch` of 0 never reaches this code through the shipped pipeline, because `pypsa_storage_units` rejects it first; a `p_nom` of 0 does, and the new scenario drives that case. r17-06: a candidate's static `Rating` was read against the whole capacity it may build and had no ceiling, so a `Rating` of 150 against a buildable 100 MW wrote a `p_max_pu` of 1.5, and a `Rating` equal to `Max Capacity` derated a two-unit candidate to 0.5. PLEXOS states a Rating for one unit, so a candidate reads it against one unit's `Max Capacity`, held at full availability. r17-02: `effective_p_nom` and `capacity_attribute` stated the same three-way rule in two chains that could disagree, and `decisions.md` carries the attribute name against every component. `capacity_choice` gives both from one chain. r17-07 and r17-08: the note `decisions.md` carries for a dropped candidate said `p_nom_min is 0`, which is false for the forced-build case round 16 added, and the mapping document still stated the rule round 16 replaced. r17-09, r17-10, r17-11 and r17-12: a docstring naming an unreachable case, a scenario description copying a docstring it already disagreed with, three constants with one reader each, and a module summary that named none of what the module now holds. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
thomaschristopherking
left a comment
There was a problem hiding this comment.
Automatic review-and-fix, round 17
Commit c0f08d0 acts on the comments below.
Round 16's hydro guard did not remove the division: it moved it into the h5 sink,
which divides the inflow series by the scaling factor. A unit that cannot be scaled
is now left out and reported. A stated efficiency_dispatch of 0 never reaches that
code through the shipped pipeline, because pypsa_storage_units rejects it first; a
p_nom of 0 does, and the new scenario drives that case.
Acted on
- r17-01 (
thermo, critical)interop/plugins/shared/pypsa_sienna_translations/_hydro.py:157—_hydro_budget_scalereturns a scaling factor of 0.0, and the h5 sink divides the inflow series by that factor. - r17-05 (
correctness, major)interop/plugins/shared/pypsa_sienna_translations/_hydro.py:160— Ahydro_budgetscale of zero is used as a divisor by the h5 sink, so the stored budget becomes infinity instead of nothing. - r17-06 (
correctness, major)interop/plugins/shared/plexos_pypsa_translations/_generator_derivation.py:125— A candidate generator'sRatingis read against the whole capacity it may build, so itsp_max_puis no longer held at or below 1. - r17-02 (
thermo, major)interop/plugins/shared/pypsa_sienna_translations/_shared.py:90—effective_p_nomandcapacity_attributestate the same three-way capacity rule twice. - r17-07 (
pruning, major)interop/plugins/shared/pypsa_sienna_translations/_shared.py:194— The notedecisions.mdcarries for a dropped candidate saysp_nom_min is 0, but the drop now turns on the lower ofp_nom_minandp_nom. - r17-08 (
pruning, major)docs/translation_mappings/translation-from-pypsa-to-sienna.md:84— The authoritative mapping document states the old rule for an extendable component in an unsolved network. - r17-09 (
pruning, major)interop/plugins/shared/pypsa_sienna_translations/_hydro.py:158— The docstring names a case the code cannot reach, becausefill_hydro_defaultsfills a missing efficiency to 1.0. - r17-10 (
pruning, minor)tests/features/pypsa_to_sienna/thermal_generators.feature:208— The scenario description copies thecapacity_floordocstring word for word, and the two already disagree. - r17-11 (
pointless, minor)interop/plugins/shared/plexos_pypsa_translations/constants.py:20— The threeEXT_*_FIELDconstants sit in the shared module, but only_expansion.pyreads them, once each. - r17-12 (
pointless, minor)interop/plugins/shared/pypsa_sienna_translations/_shared.py:3— The module docstring lists three things this module holds, and the branch added a fourth the list does not name.
Deferred
- r17-04 (
thermo, minor)interop/plugins/shared/plexos_pypsa_translations/_generator_derivation.py:131—SourceGenerator.p_nombuilds aDecision, keeps the number and drops the sources, and_p_nombuilds the sameDecisionagain.
One thermo comment, on the console warning written per dropped generator, repeated
r6-06 and was dropped rather than put a second time.
The eleven comments the four reviewers raised in round 17, with the verdict each one got. Commit `c0f08d0` acts on the ten the run accepted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Superseded by #14, which squashes this whole stack into one branch off |
TL;DR
A PLEXOS model states what it may build in
Build Cost,Max Units Built,WACC,Economic Life,Technical LifeandFO&M Charge, and the reader named none of them: theAEMO 2024 ISP file alone carries 8,539
Generator.Build Costrows and 372 objects with aMax Units Built, all of which the first leg dropped.Max Units Builtabove zero now makesan object a candidate, and a candidate becomes an extendable PyPSA component carrying what
building it costs.
First of four for transition-zero/tz-infra-interop#116.
Later parts carry the build and retirement years and the emissions constraints, then write
the SiennaSchemas investments portfolio. Based on
issue-333-plexos-pypsa-review-findings.Files to review (38, +1534 / -252):
docs/specs/2026-09-08-plexos-sienna-investments-design.md(start here)interop/plugins/shared/plexos_pypsa_translations/_expansion.py(new)interop/plugins/shared/plexos_pypsa_translations/_storage_shared.pyRatedCapacity: what an object has, and what one unit of it is.interop/plugins/shared/pypsa_sienna_translations/_shared.pyinterop/plugins/sinks/emit_pypsa_network.pytests/features/plexos_to_pypsa/generators.featureImportant
A PLEXOS
BatterystatesMax Powerper unit, and the mapping read it as the wholeobject's power. A Battery stating
Units 3therefore reached PyPSA at a third of itspower. This corrects it, so an existing dispatch translation of a multi-unit battery
changes.
record_battery_outagesalready deratedp_max_pubyunits_out / units,which only means anything if
p_nomcovers every unit.Why
Every translation this repository makes gives an operations model, and the PLEXOS leg fixed
every capacity on purpose in five places. The note beside each read "v1 translates a dispatch
model, so capacity is fixed". A published expansion plan therefore reached PyPSA as a fleet
that cannot change, and the cost of building anything never left the XML.
How
One reading decides it:
Max Units Builtabove zero._expansion.pyholds that rule once,so a Generator, a Battery and a pumped-storage turbine cannot drift apart. Each class hands
it a
RatedCapacity— the capacity the object already has, and the size of one unit — ratherthan leaving the second to be inferred from the first, which is what let a turbine's
p_nom_maxcome out at twice the right number in the first draft.PyPSA 1.2 takes
overnight_costin preference tocapital_costand works the annuity outitself from
discount_rateandlifetime. So this writes those three plusfom_costandassembles no capital cost of its own — an annuity here would be a second implementation of
one PyPSA already ships, and the two would disagree the first time either changed.
Economic Lifeis the capital recovery period, which is the period PyPSA annuitises across,so it takes
lifetime.Technical Lifeand the size of one unit have no PyPSA field, sothey travel in the extensions sidecar as
technical_life_yearsandunit_size_mw.Reviewer notes
overnight_costis set and
discount_rateis not, so a candidate needs all three ofBuild Cost,WACCand
Economic Lifeor the network cannot be solved at all. Each drop is aCOMPONENT_SKIPPEDevent and one warning names a few.it runs, fixed, and the missing price is recorded as
NOT_MAPPEDagainst the component.Dropping it would lose real capacity from the network.
p_nom.PyPSA reads
p_nomonly for a component whose capacity is fixed, so this binds nodispatch. It is what every per-unit field is read against —
p_min_pu, a ramp limit, anavailability profile stated in MW — and against zero each of those would come out at zero.
p_nom_optfor an extendable component. A PLEXOS candidate mustnot reach a Sienna operations system as if it were built. A component with a
p_nom_minabove zero still reaches it: that is capacity a build cannot take away.
WACCreads as a percentage only when the model says so.as_ratedivides by 100where the stated unit is
%and takes the number as written otherwise.Tests
Sixteen scenarios across
generators.feature,storage_units.feature,dated_properties.featureand thepypsa_to_siennafeatures: a candidate generator, batteryand turbine with every field checked; a plant that already runs and may also expand; three
ways a build goes unpriced; a plant kept despite an unpriced build; a candidate's dated
capacity; a solve that builds none of one candidate and 400 MW of another. Full suite (600),
ruff, mypy, the import contracts and both plugin lints 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
Unitsseries thehorizon_yearnarrowing throws away.ExtensionKind.CONSTRAINT, so an emissions target survives the hop through PyPSA.pypsa-to-sienna-investmentsleg and the portfolio sink.Open workspace in Conductor