Skip to content

Move jd/market_models2 onto the OpenAPI 1.x line - #302

Merged
jd-lara merged 50 commits into
jd/market_models2from
rh/market_models_1x
Sep 14, 2026
Merged

jd-lara merged 50 commits into
jd/market_models2from
rh/market_models_1x

Conversation

@rodrigomha

@rodrigomha rodrigomha commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Moves jd/market_models2 onto the OpenAPI 1.x line by merging jd/openapi-1x-pins, which carries main and the #1783 repair (#301). One merge commit.

Environments

Every project pins the 1.x line: InfrastructureSystems IS4, PowerSystems jd/openapi-1x (with #1795), the PowerOpenAPIModels subpackages on main, PowerSystemCaseBuilder, PowerFlowFileParser and PowerTableDataParser on jd/openapi-1x. The interim pins to the #1784 merge commit, the pre-1.x InfrastructureSystems commit and rh/update_enums are gone. InfrastructureOptimizationModels stays on jd/market_models2, which now carries main (merged there directly, so it has #164, #165 and #166). One pin is temporary and says so in a comment: PowerFlows jd/openapi-1x-pins (#449), for the LossCurve reader; back to psy6 once it merges.

Conflicts and how they were resolved

  • PowerOperationsProblemTemplate gains both new fields, market_model and events, in that order, and the constructor fills both.
  • template_validation.jl keeps both appended blocks: the market network-formulation check and the outage-event discovery.
  • market_bid_plumbing.jl drops the IOM.get_must_run forwarder; the repair implements it on the _is_must_run trait in utils/psy_utils.jl, and the PointToPointBid operation-cost seam stays.
  • The three project files take the 1.x side, with the PowerFlows pin above.

Results

Full suite on the pinned stack, Julia 1.12: 130,648 tests pass across all 56 files, the market-model tests included.

🤖 Generated with Claude Code

https://claude.ai/code/session_01XgUkJVy9F23R3G5F8XFaLj

m-bossart and others added 30 commits September 7, 2026 23:42
Adds supports_events(::Type{<:PSY.Component}) plus
get_initial_parameter_value/get_parameter_multiplier methods for the
event parameter types, enabling outage-event discovery and defaults.
Add PowerOperationsProblemTemplate.events, set_event_model!/get_event_models
so a template can hold EventModels ahead of build-time distribution to
DeviceModels.
Populate EventModel.attribute_device_map from the system's supplemental
attributes during template validation, validate each event's time-series
mapping, and distribute the event to every DeviceModel whose device type
carries the attribute. Preserve event-model identity across the template
deep copy performed at DecisionModel construction (mirroring the existing
PNM-matrix-sharing trick) so callers can inspect discovery results on the
same EventModel object they attached to the template.

Also fix a name collision from the earlier event-model work: POM's
get_value(::StateVariableValueCondition) was defined without qualifying
IOM.get_value, which created a separate local generic function that
shadowed IOM's get_value(::InitialCondition) for every other unqualified
caller in the package (storage, hybrid, thermal generation, AGC),
silently breaking all initial-conditions-consuming builds.
Two distinct event models of the same contingency type discovering the
same device type can't both be registered under the device model's single
(contingency type, device type) events slot. Replace the silent-skip guard
in _build_device_model_events! with a loud error when the existing
registration belongs to a different event model than the one being
processed; re-discovering the same event model for the same key stays a
no-op. Add a covering test case.

Also reword the _deepcopy_template override comment to describe the
deepcopy-unsafe solver-cache behavior directly instead of citing a PR
number.
Wires DeviceModel.events into the ArgumentConstructStage: add_parameters!
creates AvailableStatusParameter and AvailableStatusChangeCountdownParameter
containers for devices carrying a matching supplemental attribute, and a
generic add_to_expression! offsets those parameters into the system balance
via _balance_expression_targets, replacing PSI's four per-network methods
with one. add_event_arguments! now overrides the no-op stub for
PSY.StaticInjection devices.
Loads (StaticPowerLoad/PowerLoadDispatch/PowerLoadInterruption) and
FixedOutput devices now inject an ActivePowerOffsetParameter (and
ReactivePowerOffsetParameter on reactive-capable networks) directly
into the system balance expression when an event is attached, on top
of the generic status/countdown parameters from the StaticInjection
default.
…vices

Implements add_event_constraints! for ThermalGen/RenewableGen/ElectricLoad
across active-only and reactive-capable networks, bounding dispatch by
available capacity during an outage event and adding a quadratic reactive
power bound on reactive-capable networks.
Ports HydroGen/HydroPumpTurbine/EnergyReservoirStorage add_event_constraints!
methods and their pump/input-output contingency-constraint helpers, plus fixes
a get_variable/add_constraints_container! instance-vs-type bug in the shared
reactive-power contingency helper (never previously exercised by any test).
Covers full-template build+solve across CopperPlate/PTDF/DCP/ACP network
models with a FixedForcedOutage event, the PTDF two-target balance offset
and ACP reactive-offset paths for load events, and a recurrent-solve mock
test confirming a forced outage drives thermal output to zero. Also
strengthens the initial-conditions exclusion test to confirm event
parameters land in the main container while staying absent from the IC
container.
…es style

Aligns the new E2E thermal-outage testset with the naming convention
already established in test_model_decision.jl (both are exported by IOM
and used unqualified there).
The prior testset only checked that ReactivePowerOffsetParameter and
ReactivePowerBalance both exist, which proves nothing about the offset
being wired into the balance (the expression is allocated for every ACP
build regardless of events). Add a recurrent-solve mock testset that
checks JuMP.coefficient of the offset parameter's variable in the balance
expression directly, so the test fails if the wiring is ever removed.
Adds an Outage events subsection covering the availability parameters
and per-device-family outage constraints added when an EventModel is
attached to a template. public.md needs no changes: it registers
symbols via a blanket @autodocs, which already picks up the new
exports.
Record the completed event framework port in pom_port_plan.md
(Workstream C and execution order), and add the SDD plan and design
spec artifacts for the branch.
Add an objective to the forced-outage mock test so it fails when
ActivePowerOutageConstraint is removed, assert the constraint's baked RHS
equals the device's max active power, cover the FixedOutput offset path,
and smoke-test the event condition accessors. Also correct the
ReactivePowerOutageConstraint docstring/docs math to match the
max((Q^max)^2, (Q^min)^2) implementation, and update the stale
"no event framework" claim in .claude/CLAUDE.md.
`IS.get_uuid` is gone in IS4: supplemental attributes are addressed by integer
id, which is what the security-constrained outage pass on main already uses
(`IS.get_id` / `PSY.get_supplemental_attribute(sys, id)`). Retype
`EventModel.attribute_device_map` to `Dict{Int, ...}` and follow that idiom in
build-time event discovery.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NjXEBDreXLvbXw3pLKtAVR
These were ported from PSI/SSS and then commented out because
`mock_construct_device!` errored on `add_event_model = true`; the events port
made that path work, so uncomment them. Every ported count matched reality
unchanged: +24 LessThan (one ActivePowerOutageConstraint per timestep for the
one device the mock attributes), +48 for storage (charge and discharge), and
+48 variables only under `built_for_recurrent_solves`, where event parameters
are JuMP parameters rather than Float64.

The two storage `EnergyTargetFeedforward` testsets stay commented: they are
blocked on the StorageSystemsSimulations feedforward port, not on events. The
comment now says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NjXEBDreXLvbXw3pLKtAVR
Counts and set types pass through a units error, a swapped variable, or a
missing square. The shared upper-bound path is component-neutral
(`IOM.get_max_active_power` has one POM method, which passes `PSY.SU`), so the
existing thermal RHS check already covers units for renewable/load/hydro too;
what is unchecked is the LHS choice per family and the three builders that
compute their own right-hand sides.

Added:
- each family's outage constraint carries its own power variable, coefficient 1;
- storage input/output constraints are not swapped, with bounds recomputed from
  the fixture in system base;
- the reactive constraint is q^2 bounded by the *squared* reactive limit: tight
  exactly at the limit, symmetric in sign, satisfied inside and violated outside;
- outages drive storage charge and discharge, and hydro pump turbine and pump
  variables, to zero under an objective that maximizes them;
- load offsets reach the balance row with the event-parameter multiplier of 1.0;
- a thermal outage costs exactly what forcing the same unit off costs, on a
  copperplate UC problem.

The last one needs a commitment template: the outage bounds active power from
above only, so a dispatch formulation enforcing `p >= p_min` is infeasible under
an outage. That is what PSI's `has_outage` feedforward override relaxes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NjXEBDreXLvbXw3pLKtAVR
Most event testsets opened with the same ten lines of fixture plumbing. Moved
that into test_utils/events_test_utils.jl:

- `fixed_outage_event` — the FixedForcedOutage EventModel with its status series;
- `build_outage_model` — attach the attribute, build a DecisionModel for it;
- `mock_event_container` — mock-construct a device model with the outage attached;
- `outaged_name` / `outaged_device` — the attributed device, read back off the
  availability parameter rather than picked from the system a second time;
- `maximize_under_outage` — force the outage and maximize what it should suppress;
- `outage_zero_output_cases` — the device families that check drives to zero.

The three near-identical behavioral testsets (thermal, storage, hydro pump) are
now one loop over that last table, and the load-offset coefficient testset that
the multiplier check subsumes is gone. Lookups by name use the three-argument
`PSY.get_component` instead of scanning `get_components`.

35 testsets, all passing; test_events.jl is 170 lines shorter with more coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NjXEBDreXLvbXw3pLKtAVR
POM builds the event parameters but has nowhere to put values between solves;
PSI owns the state arrays, the clock and the RNG. Today PSI also owns the
*meaning* of an outage, fused into SimulationState indexing: what a contingency
type says about occurrence and duration, how a countdown decays, how availability
and balance offsets follow from it. That is domain logic, and it belongs here.

`src/event_models/event_runtime.jl` is that half, as pure functions of values
with no state type in sight:

- `outage_occurred` / `time_to_recover` — the only per-contingency-type behavior,
  deterministic from a profile for FixedForcedOutage, a Bernoulli draw on the
  caller's rng for GeometricDistributionForcedOutage;
- `advance_countdown`, `countdown_trajectory`, `availability_from_countdown`,
  `availability_trajectory`, `outage_power_offset`, `countdown_steps` — the
  arithmetic that carries an outage across steps and projects it into the next
  decision model's horizon;
- `event_step_values` — one call per device per step, returning everything a
  runtime needs to write;
- `event_parameter_keys` — the parameters to update and the order to write them,
  restricted to what the built model actually has, so a runtime stops hardcoding
  POM's parameter types;
- `required_inputs` / `is_triggered` — a condition declares the values it needs,
  the runtime resolves them, and the condition is evaluated as a function of its
  own inputs. Only `DiscreteEventCondition` asks for the runtime's state object,
  and it asks explicitly.

Divergences from PSI are marked `TODO(events)` at their definitions: the offset
window (whole outage here, first step only in PSI) and rounding a duration that
does not divide the state resolution. `mean_time_to_recovery` units are the
caller's to state, since PowerSystems documents minutes and PSI reads hours.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NjXEBDreXLvbXw3pLKtAVR
A runtime needs the write order, not just the keys: `event_parameter_keys` covers
what a built model has, but PSI also writes into simulation state datasets the
container knows nothing about, and it needs the same ordering there.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NjXEBDreXLvbXw3pLKtAVR
`event_step_values` decayed the countdown and sampled a new outage in one call,
so a runtime that wanted the decay had to call it every step — which also
re-sampled — while one that only called it when the condition held froze any
outage in progress. Wiring PSI to it hit exactly that: with a ContinuousCondition
the countdown decayed twice per step and a two-step outage lasted one.

`may_start` splits the two. The countdown decays on every call; only starting a
new outage is gated. A runtime now makes one call per device per step and passes
the condition's verdict, which is what the condition was always supposed to mean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NjXEBDreXLvbXw3pLKtAVR
PNM #356 and #357 landed on psy6 and the pin was already reverted to
rev = "psy6"; only the comment describing the temporary pin remained.
Replaces the two runtime isa checks in template_validation. The event
time-series validation drops its try/catch for PSY.has_time_series, which
also stops a non-missing ArgumentError being reported as missing data.
The security-constrained outage claim gates on a dispatched
_needs_planned_outage_optin method.
- event_runtime: replace the Union{String,Nothing} sentinel with a
  predicate plus a total accessor, drop five ternaries, use iszero.
  The predicate tests the mapped value, not just key presence: the
  default mappings register every key a contingency type accepts.
- event_constraints: merge the byte-identical ThermalGen and HydroGen
  methods onto a two-member Union bound.
- event_model: extract the filter-and-error preamble repeated across
  17 construct sites into _for_each_event_devices.
- event_arguments: replace the _EventLoadFormulations Union alias with
  a supports_event_offset trait, which records that PowerLoadShift is
  excluded rather than leaving it implicit.
- tests: pass moi_tests' 8th argument, which a misplaced paren had been
  discarding so the quadratic-constraint assertion never ran; drop three
  count-only testsets a coefficient-level testset supersedes, keeping
  their size assertions.
- Resolve event devices from the discovery map template validation already
  filled, instead of probing every device's supplemental attributes twice
  per build; the mock now fills that map too.
- Implement IOM.share_template_references! for the events instead of
  overriding IOM's private _deepcopy_template through invoke.
- Dispatch the active-power outage lhs on the device family and route all
  six construct sites through one helper.
- Share VariableTarget between StateVariableValueCondition and
  StateValueInput; drop the field-shuttling converter.
- Narrow EventKey's component bound to PSY.Component.
- Compute each countdown step once for both trajectories.
jd-lara and others added 18 commits September 9, 2026 09:53
…etFeedforward

Adds the EnergyTargetFeedforward struct, accessors and export; the storage
argument method (EnergyTargetParameter plus the StorageEnergyShortageVariable
slack) and constraint method, which validates that target_period is the
horizon end because the storage slack is a one-step container; and a
_add_energy_target_constraints! helper shared with the hydro reservoir
target. The attach-time source-conflict guard now covers both target
feedforwards. Re-enables the two storage feedforward testsets, adds
coefficient-level, mid-horizon-rejection and attribute-collision tests,
documents the feedforward in the formulation table and guide, and records
pending work in .claude/pending-work.md.
Port outage events, and add the runtime interface PSI calls
IOM #164 (share_template_references!) is merged, so the [sources] pins go back
to rev = "main" in the root and test projects. The docs project follows the
upstream PowerTimeSeriesOpenAPIModels -> InfrastructureTimeSeriesOpenAPIModels
rename, which is what broke the Documentation CI job.

feedforward_interface.jl's header still claimed the event infrastructure lived
in PowerSimulations and had not been ported; it describes the real fallbacks
now. get_empty_timeseries_mapping gains a typed error for a non-event
PSY.Contingency instead of a bare MethodError, and outage_power_offset's
whole-duration semantics are documented as the live behavior now that PSI #1664
delegates both offset writes here.
IS4 absorbed lk/loss-curve-units-v2, so get_loss, get_loss_function and
get_converter_loss_from/_to now return a LossCurve wrapper that declares its own
unit system. get_proportional_term and get_constant_term are defined on the
inner curve types only, so every reader had to unwrap; twelve sites needed it,
including the two HVDCVSCConverterPowerConstraint methods.

Curve-shape branching moves from isa checks to dispatch on the unwrapped
ValueCurve: _hvdc_linear_loss_terms and _hvdc_pwl_len_segments replace the
"only accepts LinearCurve" guards and the "Should not be here" fallback, each
with a method that errors and names the offending type.

_get_quadratic_term's untyped `= 0.0` fallback was a silent-failure hazard: a
LossCurve-wrapped QuadraticCurve hit it and returned zero, dropping the a*I^2
term and disarming the guard that exists to refuse quadratic curves under
LinearLossConverter. It now dispatches per curve type and errors on anything
else.

_loss_curve_value converts a curve to system base from whatever base it
declares, replacing the "assume the curve is authored in device base"
base_factor. This reproduces the old arithmetic exactly for a DeviceBaseUnit
curve and corrects it for the NaturalUnit curves the data actually carries,
which the previous code scaled as if they were per-unit.

The unit-system dispatch is resolved once per device, above the time loop, so
the abstract field type PSY declares keeps its cost at build time and the
per-step arithmetic stays plain Float64.
…e-independent

EnergyLimitFeedforward is the storage twin of ReservoirLimitFeedforward: same
FeedforwardIntegralLimitConstraint, same per-block trench math, differing only
in the parameter it reads. EnergyLimitParameter already existed, so the port is
the struct plus a shared _add_integral_limit_constraints! that both types
dispatch into, mirroring the _add_energy_target_constraints! split. The two
testsets fenced in test_storage_device_models are live again; they had been
written against BookKeeping and BatteryAncillaryServices, which POM does not
have, so they run against StorageDispatchWithReserves over both battery
fixtures.

The source-conflict guard dispatched on a four-member Union of concrete
feedforward types, which EnergyLimitFeedforward would have grown to five. It is
a trait now, so each participating type opts in with one method. The property
cuts across the hierarchy rather than along it -- WaterLevelBudgetFeedforward
does not opt in -- so a shared supertype would have been the wrong tool. The
mixed-type fallback is kept: two feedforwards of different types read different
containers and cannot collide.

meta strings interpolated a DataType directly, which renders module-qualified
whenever the name is not visible in the active module. The test runner executes
each file in a fresh module, so these keys were latently unstable; nameof makes
them independent of the caller's module. The slack lookup in the bound-
feedforward constraint had to move in lockstep with its registration, and the
test expectations that build the same keys had to follow, or the read side asks
for a qualified key the write side no longer stores.

Also from the port reviews: the one-step storage shortage slack accepts a
Vector of devices rather than only a FlattenIteratorWrapper, so a Vector caller
cannot fall through to the unbounded full-horizon generic; and the hydro
coefficient testset asserts its constraint set through a dispatched predicate
instead of isa.
The formulation library tabulated five of POM's feedforward types and the
explanation page gave guidance for four. The reservoir target and limit, water
level budget, hydro usage limit and new energy limit feedforwards now have rows
carrying their parameter, constraint container and exact constraint expression,
and a guidance bullet each.

Two behaviours are worth stating because neither is visible from the table:
ReservoirLimitFeedforward dispatches on any PSY.Component despite its name, and
HydroUsageLimitFeedforward reads ActivePowerVariable directly and nets served
regulation reserves when the device model carries a service model.
Two breaking PowerSystems changes, no shims for either.

#1791 renamed the per-unit marker `DU`/`DeviceBaseUnit` to
`CU`/`ComponentBaseUnit`. In src that is a dispatch signature, not just a
name: `_loss_curve_ratio_to_system_base(::PSY.DeviceBaseUnit, ...)` would
stop being called and fall through to the error method, silently losing
the per-unit branch of the loss-curve rescale.

#1790 dropped the `MW`/`kV`/`OHMS`/`SIEMENS` constants and stopped
exporting `MVA`/`MVAr`, leaving Unitful's `u"..."` macro as the only
spelling. `PSY.MW` no longer resolves; `test/includes.jl` already does
`using PowerSystems`, which re-exports `@u_str`, so `u"MW"` needs no new
import.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LP6eB1zx4eyE3hd7tpvSue
Follow the ComponentBaseUnit rename and the u"..." natural units
The OpenAPI model packages pinned `jd/openapi_deps_update`, which predates the
0.1.0 release, its precompilation fix and the Julia 1.13 work; point them at
main. InfrastructureSystems moves from the jd/openapi-1x-suite branch to IS4,
where it merged.

Restore the CI Julia matrices to '1' now that the 1.13 fix has landed.
…ilds again

PowerSystems #1783 replaced must_run with commitment_mode and the Bool status with
OperationalStates, and left no shim. The feedforwards' _is_must_run trait moves to
utils/psy_utils.jl and reads commitment_mode == MUST_RUN for ThermalGen and
HydroPumpTurbine (false for anything else, so SELF_SCHEDULED and RELIABILITY stay
committable); is_online reads status as ONLINE or STARTUP. The 23 must-run sites and the
eleven Bool status reads go through them, as do the test utilities' duration checks, and
IOM.get_must_run forwards to the trait for IOM's own start-up and ramp code.

SwitchedAdmittance lost its Y field: the shunt model spans the blocks and takes
solved_admittance, else the engaged blocks, as the fixed susceptance. FixValueParameter
metas were string(::Type), which carries the module prefix in the test workers now that
the variable types live in IOM; the four sites use nameof like the other feedforward
metas. Tests construct with the enums and the current shunt fields, wrap their HVDC and
converter losses in LossCurve on the component base the readers documented, and the FACTS
shunt test asks for BYP instead of a nothing mode the OpenAPI 0.1.0 schema cannot encode.

The test environment pins InfrastructureOptimizationModels to rh/cost_coefficient_ratio
(IOM #166) until it merges: IOM main still calls the convert_cost_coefficient signature
InfrastructureSystems removed.
Follow PowerSystems #1783 and the OpenAPI 1.x structs so the suite builds again
… the OpenAPI 1.x line

Brings main and the #1783 repair (#301) to the market-models branch and moves every
environment to the 1.x line: InfrastructureSystems IS4, PowerSystems jd/openapi-1x (with
#1795), the PowerOpenAPIModels subpackages on main, PowerSystemCaseBuilder,
PowerFlowFileParser and PowerTableDataParser on jd/openapi-1x. Two pins stay temporary:
IOM rh/market_models_1x (jd/market_models2 plus main, IOM #167) until that merge lands,
and PowerFlows jd/openapi-1x-pins (#449) for the LossCurve reader.

Conflicts: the template gains both new fields (market_model, events) and both validation
blocks (the market network check, the outage-event discovery); the IOM.get_must_run
forwarder in market_bid_plumbing.jl goes, since the repair implements it on the
_is_must_run trait in psy_utils.jl; the pin files take the 1.x side.
main is merged there now, so the integration branch this pointed at is gone.
@jd-lara
jd-lara self-requested a review September 14, 2026 21:52
@jd-lara
jd-lara merged commit 0e14a0d into jd/market_models2 Sep 14, 2026
1 of 6 checks passed
@rodrigomha
rodrigomha deleted the rh/market_models_1x branch September 14, 2026 22:03
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.

4 participants