Skip to content

Translate the new-format custom constraints, with relaxation and expansion-limit constraints - #139

Open
nick-gorman wants to merge 18 commits into
mainfrom
translate-custom-constraints
Open

Translate the new-format custom constraints, with relaxation and expansion-limit constraints#139
nick-gorman wants to merge 18 commits into
mainfrom
translate-custom-constraints

Conversation

@nick-gorman

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

Copy link
Copy Markdown
Member

Translate the new-format custom constraints, with relaxation and expansion-limit constraints

  • Custom constraints are reformated to pypsa friendly format.
    • Including from_date functionality which allows lhs coefficients and rhs values to change with time.
    • Timeslice reference so rhs can change with active timeslice
    • Constraint variable name mapping to pypsa variable names. Assuming that tables are provided mapping new entrant generator names like "CNSW CCGT" to "CNSW CCGT_2026" and "CNSW CCGT_2028". The column names of this table are open to change and the exact name format doesn't affect the mapping, but docstrings can be updated to what the names will actually look like.
  • Add in dummy generators to allow constraint relaxation
  • Add in link and constraint relaxation limit constraint, i.e. capping the total transmission expansion over all model years.

nick-gorman and others added 2 commits August 18, 2026 13:36
Each RHS value becomes one constraint instance per investment period
(and per timeslice where tagged), with date_from resolved as the value
active at the start of each period. LHS terms keep generator and battery
names even though those components aren't translated yet — pypsa_build
skips-and-logs missing components, and the logging silences itself once
generator translation lands. The generators/batteries parameters are
accepted now as the future name-mapping hook.

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
#126 changed the expansion tables to allow blank key cells as wildcards, changed
_prepare_expansion_costs to take the enabled element ids, and made expansion links
carry forward/reverse per unit of max(forward, reverse). The cherry-picked module
still read unresolved options, capped paths at their forward capacity and imported a
helper #126 deleted.

The expansion-limit cap is now max(forward, reverse), re-resolved for the paths that
have expansion links with the network module's own helpers, and constraint
relaxations resolve their options and costs against the constraints in the model so
blank-id defaults apply to every constraint.

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

codecov Bot commented Aug 18, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

Files with missing lines Coverage Δ
...spypsa/templater/custom_constraints_from_plexos.py 100.00% <ø> (ø)
src/ispypsa/translator/constraints.py 100.00% <100.00%> (ø)
src/ispypsa/translator/mappings.py 100.00% <ø> (ø)
src/ispypsa/translator/network.py 100.00% <100.00%> (ø)

... and 2 files with indirect coverage changes

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

nick-gorman and others added 2 commits August 18, 2026 15:19
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…raint docstrings

_keep_rows_for_enabled_elements is shared by the network and constraints
translators, but "enabled elements" only described the network caller — the
constraints side passes constraint_ids. Renamed to _keep_rows_for_expansion_ids
and reworded the docstrings so each caller's selection is described where the
decision is made. The constraint helpers' I/O examples now show full tables,
including the blank-cell wildcard forms the options and costs accept.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
nick-gorman and others added 14 commits August 25, 2026 10:47
A relaxation generator's p_nom was always subtracted from the parent
constraint's LHS, which loosens a "<=" but tightens a ">=". The sign now
follows the constraint's direction (-1.0 for "<=", +1.0 for ">=") so
building relaxation capacity always loosens the constraint; an option on
an "=" is rejected, since no single-signed term can loosen an equality.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXjsJKX7ANSJxVyJbszLYW
… periods

The translator raised on duplicate rows, missing directions, unpaired
constraints and relaxed equalities -- all properties of the input tables,
so they're now declared as schema rules the translator trusts without
re-checking. What's genuinely per-run remains: after date_from resolution
a (constraint, period) can end up with terms on one side only, so those
periods are dropped from both sides with an INFO line each way.
Relaxation-generator terms derive from the post-drop RHS so they can't
re-create one-sided periods.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXjsJKX7ANSJxVyJbszLYW
…nerators

Every option has a cost in every investment period (the costs schema's
coverage rule), so joining options to costs gives exactly one generator
per option and period. The test fixture violated that rule -- SWQLD1 had
a 2026 cost only -- which hid the second per-period generator from every
relaxation expectation; it now carries schema-valid costs and the tests
exercise both SWQLD1_exp_2026 and SWQLD1_exp_2028.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXjsJKX7ANSJxVyJbszLYW
Terms naming components not in the model were described as skipped with
a log line downstream -- a silent per-term drop. The
custom_constraints_lhs schema now ties each term_type's variable_name to
the table it must exist in, so the translator and pypsa_build take the
names as given. The templater TODO points at the rule as the end state
for its IASR ID lookup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXjsJKX7ANSJxVyJbszLYW
A blank timeslice isn't "binds at every snapshot": it's the constraint's
fallback row, binding only at the snapshots none of its named-timeslice
rows cover, matching the path-limits convention. Resolving timeslices to
snapshots stays pypsa_build's job; this module passes them through.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXjsJKX7ANSJxVyJbszLYW
Set-membership probes like "SWQLD1_exp_2026 not in variable_name" pass
for many wrong outputs. The relaxation-disabled, late-date_from and
missing-link tests now compare whole expected frames, and the negative
log case covers every INFO drop line the module can emit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXjsJKX7ANSJxVyJbszLYW
allowed_expansion rode through the relaxation-generator pipeline without
any step using it, only to be read off at the end. The orchestrator now
passes the resolved options' caps directly to
_create_expansion_limit_constraints, the rez_transmission_expansion gate
moves into _resolve_relaxation_options where the decision is made, and
_finalise_generators disappears.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXjsJKX7ANSJxVyJbszLYW
The module docstring and the orchestrator's docstring had become two
accounts of the same pipeline, with fixes having to land in both. The
orchestrator docstring now carries the lot -- the translation steps, the
relaxation and expansion-limit behaviour, the schema rules the pipeline
trusts without re-checking, and the blank-timeslice fallback semantics --
next to the I/O example, and the module docstring is deleted. Accuracy
fixes folded in along the way: the one-sided period drop names both of
its causes (date_from coverage and model scope), the flow-path cap is
stated as max(forward, reverse), link_flow expansion is described as
replacing the path_id term with per-link terms, and the
rez_transmission_expansion gate, per-option-per-period generators and
annuitised costs are now stated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXjsJKX7ANSJxVyJbszLYW
The side-by-side table pairs and the compressed links one-liner in the
constraints I/O examples saved vertical space at the cost of scanning --
every input table now sits sequentially in the same tabular format as
the rest of the examples.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXjsJKX7ANSJxVyJbszLYW
…trator

Dropping a term whose component isn't in the model left the constraint
applied with a weakened LHS -- plausible-looking and silently wrong. The
translator now raises when any LHS term fails to resolve to a model
component (load terms, which pypsa_build doesn't implement, raise too),
and resolves every term through an isp_name -> name mapping supplied by
new generators and storage inputs, so new entrant units expand to their
per-build-year components the same way link terms expand over a path's
links. The generator/storage frame contract (isp_name + name, mirroring
links) is assumed ahead of the translators that will produce it, for
review by the team building that side.

The orchestrator is renamed _translate_custom_constraints (the
constraints aren't network-specific any more) and split into three block
producers -- translate the user-authored tables, create the constraint
relaxations, create the expansion limits -- assembled and finalised once,
so future producers of constraint blocks (e.g. new entrant build limits)
have a seam to append into before relaxation resolves. The reusable
pieces' docstrings now state their source-agnostic contracts rather than
relaxation-specific vocabulary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXjsJKX7ANSJxVyJbszLYW
The blanket raise on load terms made the real constraint set
untranslatable -- the baseline CNSW-SNW South GPG constraint carries
date_from-varying load terms. Demand is exogenous (p_set on the Load
components pypsa_build attaches per demand node), so a load term is a
data term, not a variable term: it now resolves through the standard
isp_name -> name mapping to the "load_<bus>" component with attribute
p_set, ready for pypsa_build to fold coefficient x demand into the
constraint. pypsa_build owes that implementation and should raise, not
skip, until it lands.

Validation is against a new demand_nodes input rather than all buses,
because REZ buses carry no demand and which buses do follows the
regional granularity -- a load term for a sub-region the granularity
aggregates away raises through the same not-in-model check as link
terms. Also labels the missing-component error's tuple structure as
(constraint_id, variable_name).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QXjsJKX7ANSJxVyJbszLYW
…l_name

The mapping's inherited isp_name/name columns said where the data came from,
not what the mapping means, so the constraints module now names both sides
for their role. The custom_constraints_lhs schema also sets out the
convention those names imply: a variable_name refers to a whole element -
a new entrant across all its build years, a path across its existing and
expansion links - never a per-build-year component.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182yKiTKCwGoVPs5hToavKn
…trings

Condensed column=value examples made the reader reconstruct the table shape
in their head, so the I/O Example convention now requires full tables (with
abridged column/row sets for otherwise large ones) and the constraints
module's remaining condensed examples follow it. The expansion example also
gains a new entrant term, showing the per-build-year fan-out next to the
link and existing-unit cases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0182yKiTKCwGoVPs5hToavKn
…nches

Coverage showed four untested paths: the all-empty assembly branch, calendar
year period starts, the unmapped-term_type drift guard, and the duplicate
RHS name check. The last needed the one realistic collision - a relaxable
constraint sharing its ID with an expandable path, giving both the same
expansion-limit cap row - which the new test pins down.

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