Skip to content

Port hybrid systems#104

Open
acostarelli wants to merge 54 commits into
mainfrom
ac/hybrid
Open

Port hybrid systems#104
acostarelli wants to merge 54 commits into
mainfrom
ac/hybrid

Conversation

@acostarelli

Copy link
Copy Markdown
Member

Thanks for opening a PR to PowerOperationsModels.jl, please take note of the following when making a PR:

Check the contributor guidelines

Anthony Costarelli and others added 3 commits April 29, 2026 09:32
Adds the bilinear (flow*head) hydro dispatch formulation and folds in
adjacent refactors merged through this branch:
  - Hydro and storage updates to IOM helpers (#97)
  - POM-to-IOM type-dispatch API migration
  - MarketBidCost / ImportExportCost static/TS split + IEC refactor
  - Shiftable-load interval indexing and validation fixes
  - HDF system serialization (#75)
  - Pin GitHub revisions; bridge IOM system-query stubs to PSY public API

Co-Authored-By: Luke Kiernan <86331877+luke-kiernan@users.noreply.github.com>
Co-Authored-By: Rodrigo Henríquez-Auba <rodrigo.henriquezauba@nrel.gov>
Co-Authored-By: Jose Daniel Lara <jdlara@berkeley.edu>
Co-Authored-By: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit

JuliaFormatter

[JuliaFormatter] reported by reviewdog 🐶


[JuliaFormatter] reported by reviewdog 🐶

con_ub = add_constraints_container!(container, HybridRenewableReserveLimitConstraint, V, names, time_steps; meta = "ub")
con_lb = add_constraints_container!(container, HybridRenewableReserveLimitConstraint, V, names, time_steps; meta = "lb")


[JuliaFormatter] reported by reviewdog 🐶

mult = get_multiplier_value(HybridRenewableActivePowerTimeSeriesParameter(), d, get_formulation(model)())


[JuliaFormatter] reported by reviewdog 🐶

constraint = add_constraints_container!(container, HybridStatusOutOnConstraint, V, names, time_steps)


[JuliaFormatter] reported by reviewdog 🐶

r_up = has_reserves ? get_expression(container, HybridTotalReserveOutUpExpression, V) : nothing


[JuliaFormatter] reported by reviewdog 🐶

constraint = add_constraints_container!(container, HybridStatusInOnConstraint, V, names, time_steps)


[JuliaFormatter] reported by reviewdog 🐶

r_dn = has_reserves ? get_expression(container, HybridTotalReserveInDownExpression, V) : nothing


[JuliaFormatter] reported by reviewdog 🐶

constraint = add_constraints_container!(container, HybridEnergyAssetBalanceConstraint, V, names, time_steps)


[JuliaFormatter] reported by reviewdog 🐶

p_th = haskey(IOM.get_variables(container), VariableKey(HybridThermalActivePower, V)) ?
get_variable(container, HybridThermalActivePower, V) : nothing
p_re = haskey(IOM.get_variables(container), VariableKey(HybridRenewableActivePower, V)) ?
get_variable(container, HybridRenewableActivePower, V) : nothing
p_ch = haskey(IOM.get_variables(container), VariableKey(HybridStorageChargePower, V)) ?
get_variable(container, HybridStorageChargePower, V) : nothing
p_ds = haskey(IOM.get_variables(container), VariableKey(HybridStorageDischargePower, V)) ?
get_variable(container, HybridStorageDischargePower, V) : nothing
load_param = haskey(IOM.get_parameters(container), ParameterKey(HybridElectricLoadTimeSeriesParameter, V)) ?
get_parameter_array(container, HybridElectricLoadTimeSeriesParameter, V) : nothing


[JuliaFormatter] reported by reviewdog 🐶

mult = get_multiplier_value(HybridElectricLoadTimeSeriesParameter(), d, get_formulation(model)())


[JuliaFormatter] reported by reviewdog 🐶

) where {V <: PSY.HybridSystem, W <: AbstractHybridFormulationWithReserves, X <: AbstractPowerModel}


[JuliaFormatter] reported by reviewdog 🐶

constraint = add_constraints_container!(container, HybridReserveAssignmentConstraint, V, names, time_steps;
meta = "$(s_type)_$s_name")


[JuliaFormatter] reported by reviewdog 🐶

) where {V <: PSY.HybridSystem, W <: AbstractHybridFormulationWithReserves, X <: AbstractPowerModel}


[JuliaFormatter] reported by reviewdog 🐶

constraint = add_constraints_container!(container, HybridReserveBalanceConstraint, V, names, time_steps;
meta = "$(s_type)_$s_name")


[JuliaFormatter] reported by reviewdog 🐶

HybridChargingReserveVariable, HybridDischargingReserveVariable)


[JuliaFormatter] reported by reviewdog 🐶

hybrids_with_thermal = [d for d in devices_vec if PSY.get_thermal_unit(d) !== nothing]
hybrids_with_renewable = [d for d in devices_vec if PSY.get_renewable_unit(d) !== nothing]
hybrids_with_storage = [d for d in devices_vec if PSY.get_storage(d) !== nothing]


[JuliaFormatter] reported by reviewdog 🐶

HybridTotalReserveInUpExpression, HybridTotalReserveInDownExpression,


[JuliaFormatter] reported by reviewdog 🐶

HybridServedReserveInUpExpression, HybridServedReserveInDownExpression,


[JuliaFormatter] reported by reviewdog 🐶

HybridServedReserveOutUpExpression, HybridServedReserveOutDownExpression)


[JuliaFormatter] reported by reviewdog 🐶

HybridServedReserveInUpExpression, HybridServedReserveInDownExpression)


[JuliaFormatter] reported by reviewdog 🐶

lazy_container_addition!(container, E, T, PSY.get_name.(hybrids_with_storage), time_steps)


[JuliaFormatter] reported by reviewdog 🐶

add_to_expression!(container, E, HybridDischargingReserveVariable, hybrids_with_storage, model)


[JuliaFormatter] reported by reviewdog 🐶

add_to_expression!(container, E, HybridChargingReserveVariable, hybrids_with_storage, model)


[JuliaFormatter] reported by reviewdog 🐶

add_to_expression!(container, TotalReserveOffering, v, hybrids_with_storage, model)


[JuliaFormatter] reported by reviewdog 🐶

add_to_expression!(container, ActivePowerBalance, ActivePowerInVariable, devices, model, network_model)
add_to_expression!(container, ActivePowerBalance, ActivePowerOutVariable, devices, model, network_model)
add_to_expression!(container, ReactivePowerBalance, ReactivePowerVariable, devices, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_parameters!(container, HybridRenewableActivePowerTimeSeriesParameter, grouped.with_renewable, model)


[JuliaFormatter] reported by reviewdog 🐶

add_parameters!(container, HybridElectricLoadTimeSeriesParameter, grouped.with_load, model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, ReactivePowerVariableLimitsConstraint, ReactivePowerVariable,


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridStatusInOnConstraint, devices, model, network_model)
add_constraints!(container, HybridEnergyAssetBalanceConstraint, devices, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridThermalReserveLimitConstraint, grouped.with_thermal, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridThermalOnVariableUbConstraint, grouped.with_thermal, model, network_model)
add_constraints!(container, HybridThermalOnVariableLbConstraint, grouped.with_thermal, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridRenewableActivePowerLimitConstraint, grouped.with_renewable, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridRenewableReserveLimitConstraint, grouped.with_renewable, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridStorageBalanceConstraint, grouped.with_storage, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, StateofChargeTargetConstraint, grouped.with_storage, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, ReserveCoverageConstraint, grouped.with_storage, model, network_model)
add_constraints!(container, ReserveCoverageConstraintEndOfPeriod, grouped.with_storage, model, network_model)
add_constraints!(container, HybridStorageChargingReservePowerLimitConstraint, grouped.with_storage, model, network_model)
add_constraints!(container, HybridStorageDischargingReservePowerLimitConstraint, grouped.with_storage, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridStorageStatusChargeOnConstraint, grouped.with_storage, model, network_model)
add_constraints!(container, HybridStorageStatusDischargeOnConstraint, grouped.with_storage, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridReserveAssignmentConstraint, devices, model, network_model)
add_constraints!(container, HybridReserveBalanceConstraint, devices, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

) where {T <: PSY.HybridSystem, D <: HybridDispatchWithReserves, S <: AbstractActivePowerModel}


[JuliaFormatter] reported by reviewdog 🐶

add_to_expression!(container, ActivePowerBalance, ActivePowerInVariable, devices, model, network_model)
add_to_expression!(container, ActivePowerBalance, ActivePowerOutVariable, devices, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_parameters!(container, HybridRenewableActivePowerTimeSeriesParameter, grouped.with_renewable, model)


[JuliaFormatter] reported by reviewdog 🐶

add_parameters!(container, HybridElectricLoadTimeSeriesParameter, grouped.with_load, model)


[JuliaFormatter] reported by reviewdog 🐶

) where {T <: PSY.HybridSystem, D <: HybridDispatchWithReserves, S <: AbstractActivePowerModel}


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridStatusInOnConstraint, devices, model, network_model)
add_constraints!(container, HybridEnergyAssetBalanceConstraint, devices, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridThermalReserveLimitConstraint, grouped.with_thermal, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridThermalOnVariableUbConstraint, grouped.with_thermal, model, network_model)
add_constraints!(container, HybridThermalOnVariableLbConstraint, grouped.with_thermal, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridRenewableActivePowerLimitConstraint, grouped.with_renewable, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridRenewableReserveLimitConstraint, grouped.with_renewable, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridStorageBalanceConstraint, grouped.with_storage, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, StateofChargeTargetConstraint, grouped.with_storage, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, ReserveCoverageConstraint, grouped.with_storage, model, network_model)
add_constraints!(container, ReserveCoverageConstraintEndOfPeriod, grouped.with_storage, model, network_model)
add_constraints!(container, HybridStorageChargingReservePowerLimitConstraint, grouped.with_storage, model, network_model)
add_constraints!(container, HybridStorageDischargingReservePowerLimitConstraint, grouped.with_storage, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridStorageStatusChargeOnConstraint, grouped.with_storage, model, network_model)
add_constraints!(container, HybridStorageStatusDischargeOnConstraint, grouped.with_storage, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridReserveAssignmentConstraint, devices, model, network_model)
add_constraints!(container, HybridReserveBalanceConstraint, devices, model, network_model)

@github-actions

github-actions Bot commented Apr 30, 2026

Copy link
Copy Markdown

Performance Results

Version Precompile Time
Main 2.96307914
This Branch 2.979402521
Version Build Time
Main-Build Time Precompile 88.431531879
Main-Build Time Postcompile 2.040664425
This Branch-Build Time Precompile 83.751822091
This Branch-Build Time Postcompile 1.987534198
Version Solve Time
Main-Solve Time Precompile FAILED TO TEST
Main-Solve Time Postcompile FAILED TO TEST
This Branch-Solve Time Precompile FAILED TO TEST
This Branch-Solve Time Postcompile FAILED TO TEST

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remaining comments which cannot be posted as a review comment to avoid GitHub Rate Limit

JuliaFormatter

[JuliaFormatter] reported by reviewdog 🐶

add_to_expression!(container, ActivePowerBalance, ActivePowerInVariable, devices, model, network_model)
add_to_expression!(container, ActivePowerBalance, ActivePowerOutVariable, devices, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_parameters!(container, HybridRenewableActivePowerTimeSeriesParameter, grouped.with_renewable, model)


[JuliaFormatter] reported by reviewdog 🐶

add_parameters!(container, HybridElectricLoadTimeSeriesParameter, grouped.with_load, model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridStatusInOnConstraint, devices, model, network_model)
add_constraints!(container, HybridEnergyAssetBalanceConstraint, devices, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridThermalReserveLimitConstraint, grouped.with_thermal, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridThermalOnVariableUbConstraint, grouped.with_thermal, model, network_model)
add_constraints!(container, HybridThermalOnVariableLbConstraint, grouped.with_thermal, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridRenewableActivePowerLimitConstraint, grouped.with_renewable, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridRenewableReserveLimitConstraint, grouped.with_renewable, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridStorageBalanceConstraint, grouped.with_storage, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, StateofChargeTargetConstraint, grouped.with_storage, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, ReserveCoverageConstraint, grouped.with_storage, model, network_model)
add_constraints!(container, ReserveCoverageConstraintEndOfPeriod, grouped.with_storage, model, network_model)
add_constraints!(container, HybridStorageChargingReservePowerLimitConstraint, grouped.with_storage, model, network_model)
add_constraints!(container, HybridStorageDischargingReservePowerLimitConstraint, grouped.with_storage, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridStorageStatusChargeOnConstraint, grouped.with_storage, model, network_model)
add_constraints!(container, HybridStorageStatusDischargeOnConstraint, grouped.with_storage, model, network_model)


[JuliaFormatter] reported by reviewdog 🐶

add_constraints!(container, HybridReserveAssignmentConstraint, devices, model, network_model)
add_constraints!(container, HybridReserveBalanceConstraint, devices, model, network_model)

@acostarelli acostarelli marked this pull request as ready for review May 5, 2026 02:59
@jd-lara jd-lara requested review from kdayday and rodrigomha May 5, 2026 04:19

@rodrigomha rodrigomha left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

We need to add a couple of extra tests.

First, we need to test different attributes, that is using reservation = false, and energy target = true. Also, what happened with storage_reservation and regularization attributes?

Also, it would be good to test that the hybrid system builds when there is no thermal, or no storage, or no renewable.

Finally, it would be good to test when the hybrid does not participate in reserves.

Overall, the PR looks good. Once we have the tests, I will QA/QC the model for the different tests to ensure that the constraints are looking good.

Comment thread src/hybrid_system_models/hybrid_systems.jl Outdated
Comment thread src/core/parameters.jl
# Hybrid System Parameters
#################################################################################

"Time-series parameter for the maximum active power available from a hybrid system's

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@jd-lara This is a change from how things are currently defined in HSS with the time series attached to the hybrid, not the subcomponent, but I guess that works and is simpler for this case?

@kdayday kdayday left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Following Rodrigo's lead on the testing, but in addition, can you port over the updated equivalent of what's in the HybridDispatchWithReserves docstring on the HSS PR and have claude add hyperlinks to exported types and functions referenced in the docstring? It looks like the POM docs are way out of date, but would like to update as we go.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR ports/introduces PSY.HybridSystem support into PowerOperationsModels.jl by adding a new HybridDispatchWithReserves device formulation, its constructor plumbing, and a basic end-to-end test that builds/solves a hybrid co-optimization on RTS-GMLC.

Changes:

  • Adds hybrid formulation types, variables/parameters/expressions/constraints, and objective plumbing for HybridDispatchWithReserves.
  • Adds a two-stage construct_device! implementation for HybridSystems (argument + model stages) and hooks it into the main module includes/exports.
  • Adds test utilities and a new test that builds and solves a model containing a HybridSystem with reserves.

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
src/PowerOperationsModels.jl Includes hybrid model files and exports the new hybrid-related types.
src/hybrid_system_models/hybrid_systems.jl Implements hybrid variable bounds, reserve aggregation, constraints, and objective-cost plumbing.
src/hybrid_system_models/hybridsystem_constructor.jl Adds the two-stage construct_device! pipeline for HybridDispatchWithReserves.
src/core/variables.jl Introduces HybridSystem-specific variable types.
src/core/parameters.jl Introduces HybridSystem time-series parameter types and unit-conversion behavior.
src/core/expressions.jl Adds HybridSystem reserve aggregation/served-reserve expression types.
src/core/constraints.jl Adds HybridSystem constraint type definitions.
src/core/formulations.jl Adds hybrid formulation types and user-facing documentation.
test/test_utils/hybrid_test_utils.jl Adds fixtures for building a test HybridSystem in RTS-GMLC.
test/test_device_hybrid_constructors.jl Adds an integration-style test that builds and solves a hybrid dispatch-with-reserves model.
test/includes.jl Wires hybrid test utilities into the test suite.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/hybrid_system_models/hybrid_systems.jl Outdated
Comment thread src/hybrid_system_models/hybrid_systems.jl Outdated
Comment thread src/hybrid_system_models/hybridsystem_constructor.jl Outdated
Comment thread src/hybrid_system_models/hybridsystem_constructor.jl Outdated
Comment thread src/hybrid_system_models/hybrid_systems.jl Outdated
Comment thread src/hybrid_system_models/hybrid_systems.jl Outdated
Comment thread src/core/formulations.jl Outdated
Anthony Costarelli and others added 4 commits May 7, 2026 16:05
…ularization attributes; enable formatting; use jump utils; port docstrings;
Collapse paired Charge/Discharge, In/Out, Up/Down add_constraints! and
add_to_expression! methods into single bodies that dispatch on small
type-keyed trait stubs. Same JuMP shapes, same constraint meta strings,
same dispatch reachability. No public API change.

Hybrid (src/hybrid_system_models/hybrid_systems.jl):
- Charge/DischargeRegularizationConstraint
- HybridStorageStatus{Charge,Discharge}OnConstraint
- HybridStorage{Charging,Discharging}ReservePowerLimitConstraint
  (folds in the _ch/_ds_reserve_up_dn_exprs helpers)
- HybridTotalReserve{Up,Down}Expression /
  HybridServedReserve{Out,In}{Up,Down}Expression add_to_expression!
- ReserveAssignment/Deployment{Up,Down}{Charge,Discharge} ←
  Hybrid{Charging,Discharging}ReserveVariable add_to_expression!
- HybridStatus{Out,In}OnConstraint

Storage (src/energy_storage_models/storage_models.jl):
- StorageRegularizationConstraint{Charge,Discharge}

Verified: 71/71 hybrid + storage tests pass. Net -376 lines.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Replace three custom hybrid constraint types and their hand-written
add_constraints! bodies with a single call to IOM's
`add_semicontinuous_range_constraints!`, paralleling how
`AbstractThermalUnitCommitment` handles the same range-with-on-variable
pattern at thermal_generation.jl:405-419.

Mechanism:
- Define `get_min_max_limits(::PSY.HybridSystem, ::ActivePowerVariableLimitsConstraint, ::AbstractHybridFormulation)`
  to read `PSY.get_active_power_limits(PSY.get_thermal_unit(d))`. IOM's
  helper picks up `OnVariable` keyed by `PSY.HybridSystem` automatically.
- For the with-reserves case, introduce two expression types subtyping
  `RangeConstraint{UB,LB}Expressions`: `HybridThermalActivePowerWithReserve{UB,LB}`.
  Argument-stage `add_expressions!` aggregates `p_th + Σ r_up` (UB) and
  `p_th − Σ r_dn` (LB) into them, after which IOM's expression-typed
  dispatch emits `min·on ≤ p_th − r_dn` and `p_th + r_up ≤ max·on` directly.

Removes:
- HybridThermalOnVariableUbConstraint, HybridThermalOnVariableLbConstraint,
  HybridThermalReserveLimitConstraint (constraint types + exports)
- _thermal_reserve_up_expr / _thermal_reserve_down_expr helpers
- Three add_constraints! bodies (~190 lines)

Renewable cases stay hand-written for now: IOM's parameterized helper
filters by `IS.has_time_series(d, ts_type, ts_name)`, and PSY's
HybridSystem doesn't expose its inner RenewableDispatch's time series
through that accessor. A separate change would be required.

Verified: 50/50 hybrid tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Comment thread src/core/formulations.jl Outdated
Comment thread src/hybrid_system_models/hybrid_systems.jl Outdated
Comment thread src/hybrid_system_models/hybrid_systems.jl Outdated
Anthony Costarelli and others added 6 commits May 8, 2026 16:35
Per PR review (acostarelli): "domain" describes the value space of each
variable (a discrete set or continuous interval) more accurately than
"bounds", which suggests inequality-only.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Address PR review feedback:

- "Don't use `isa`. Add a method to handle this, or restructure
  existing dispatch." Eliminates every `isa(service, PSY.Reserve{...})`
  and `service isa skip_kind` site in hybrid_systems.jl. The
  `_excluded_reserve_kind` trait stub goes away — its information is
  now encoded by union types in helper method signatures.
  Five sites refactored, all sharing the same shape (per-direction
  no-op methods + a fallback `::PSY.Service` work method):
    - `add_to_expression!` for HybridTotalReserveExpression /
      HybridServedReserveExpression → `_accumulate_reserve!`
    - `add_to_expression!` for the eight Reserve*Balance{Up,Down}
      {Charge,Discharge} expressions → `_balance_term!` plus a
      `_deployment_factor` per-T trait that replaces the
      `is_up`/`is_deployment` Booleans
    - `_renewable_reserve_up/down_expr` → `_renewable_reserve_*_term!`
      thunks sharing `_accumulate_renewable_reserve!`
    - `_thermal_reserve_up/down_expr` → analogous restructure
    - `add_constraints!` for ReserveCoverageConstraint{,EndOfPeriod}
      → `_init_coverage_container!` + `_emit_coverage_constraint!`;
      the `(service isa PSY.Reserve) || continue` guard becomes the
      `::PSY.Service` fallback no-op

  Helper arguments use concrete types (OptimizationContainer, String,
  Int, Float64, PSY.Storage, …) plus parametric `::Type{T}`/`::Type{U}`/
  `d::V`/`::W` to reduce precompilation overhead.

- "Combine these if statements." Merges the two adjacent setup
  ternaries (`r_ub, r_lb = if has_reserves …` and
  `con_lb = if has_reserves …`) in
  `add_constraints!(::Type{HybridStatus{Out,In}OnConstraint}, …)` into
  a single 3-tuple ternary.

Test.detect_ambiguities returns 0; full suite passes (50/50).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…milies

Hybrid reserve variables, expressions, and constraints had ~16 paired Charge/Discharge,
Up/Down, Total/Served (a.k.a. Assignment/Deployment), and UB/LB singleton structs plus
~14 paired trait helpers that all differed only by which sibling they referenced.
Introduce marker singletons for ReserveSide, ReserveDirection, ReserveScale (UnscaledReserve
/ DeployedReserve), reuse IOM's BoundDirection (UpperBound/LowerBound), and reparametrize
the family roots:

- ReserveAggregationExpression{D,S,Sd} umbrella with two concrete struct families
  (HybridPCCReserveExpression, StorageReserveBalanceExpression) covering all 16
  historical reserve-expression singletons.
- HybridPCCReserveVariable{Sd}, HybridStorageSubcomponentReserveVariable{Sd},
  HybridStorageSubcomponentPower{Sd}, RegularizationVariable{Sd}.
- HybridStatusOnConstraint{Sd}, HybridStorageStatusOnConstraint{Sd},
  HybridStorageReservePowerLimitConstraint{Sd}, RegularizationConstraint{Sd},
  HybridThermalOnVariableConstraint{B}.

All 34 historical concrete names are retained as const aliases so external imports,
`get_expression`/`get_variable`/`get_constraint` lookups, and module exports are
byte-compatible. Inside hybrid_systems.jl this lets:

- _accumulate_reserve! + _balance_term! collapse into one _add_reserve_term! family
  (PCC boundary and storage subcomponent share the no-op skip and scale dispatch);
- thermal/renewable subcomponent accumulators (10 helpers) collapse into one
  _subcomponent_reserve_term! / _subcomponent_reserve_expr family parametric on
  the variable type;
- the UB/LB thermal-on-variable add_constraints! methods merge;
- ~14 paired trait helpers (storage / PCC / regularization) become parametric
  single-method definitions;
- 5 file-local Union consts (_BalanceUpExpr, _BalanceDownExpr, _BalanceDeploymentExpr,
  _HybridReserveUpExpr, _HybridReserveDownExpr) and _StorageCharge/DischargeSide
  Union consts get deleted.

Additional cleanups:
- get_variable_multiplier hybrid signatures take ::Type{<:Formulation} (matches the
  rest of POM); all W() instance call-sites become type-keyed.
- Three `if W <: ...` body-level subtype checks split into separate parametric
  dispatched methods (HybridStorageBalanceConstraint, RegularizationConstraint,
  HybridStatusOnConstraint).
- _init_coverage_container! uses lazy_container_addition! (idempotent).
- add_proportional_cost!(OnVariable, hybrids) hoists the variant/invariant
  function-handle selection out of the per-t loop.

Net: -142 lines across 5 files. Full Pkg.test passes (13125 / 0 fail / 0 error / 1
pre-existing broken). Zero method ambiguities.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Anthony Costarelli and others added 20 commits June 8, 2026 13:19
Port QuadraticLossConverterMILP and HVDCTwoTerminalVSCLP from a hardcoded
SOS2 depth (DEFAULT_INTERPOLATION_LENGTH) to the same tolerance/attribute
API used by HydroTurbineMILPBilinearDispatch: "bilinear_approximation",
"bilinear_quadratic_method", "bilinear_tolerance".

All five schemes are supported. The squares-based schemes (bin2, hybs,
none) reuse the standalone loss i_sq via IOM's precomputed (xsq, ysq)
overload; the discretization-based schemes (nmdt, dnmdt) never build i²,
so they take the raw form with nothing to duplicate. The precomputed-vs-raw
branch is centralized in _add_converter_bilinear!, and config construction
dispatches on the formulation type so the *NLP types stay exact.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The single absolute bilinear_tolerance was an absolute gap on the v·I /
flow·head product, whose magnitude differs by formulation, so the same 1e-2
was far stricter for converters (depth 15, intractable) than for hydro.

Replace it across all bilinear formulations (HydroTurbineMILPBilinearDispatch,
QuadraticLossConverterMILP, HVDCTwoTerminalVSCLP) with two keys:
bilinear_relative_tolerance (default 0.05, a fraction of the product magnitude
and the default sizing knob) and bilinear_absolute_tolerance (optional). A
relative tolerance is scaled to absolute by the term magnitude via the new
_resolve_tolerance / _max_abs helpers (max|x|·max|y| for the bilinear, max|i|²
for the standalone I² loss term); when both are set the finer binds. The
gap→depth inversion stays in IOM; POM does the relative→absolute scaling since
it needs the bounds. Default depth drops from 15 to 5 on the converter test
systems.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Collapse each NLP/MILP formulation pair into one formulation whose
"bilinear_approximation" attribute defaults to the exact "none" case
(IOM's NoApproximation configs), opting into MILP via a linearizing scheme:

- HydroTurbineMILPBilinearDispatch -> HydroTurbineBilinearDispatch
- QuadraticLossConverterMILP/NLP   -> QuadraticLossConverter
- HVDCTwoTerminalVSCLP/NLP         -> HVDCTwoTerminalVSC

The VSC PQ-capability (exact disk vs octagon) and pq-square registration
are re-keyed from formulation-type dispatch to dispatch on the IOM bilinear
config type, keeping the exact/approximate split branch-free. Old
*MILP/*NLP/*LP names are removed (no aliases). Tests updated to select the
MILP path via an explicit attribute.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rename VSC helpers

- Add shared BILINEAR_APPROX_DEFAULT_ATTRIBUTES constant (single source of the
  MILP approximation defaults + their documentation); merge it into the hydro,
  QuadraticLossConverter, and VSC get_default_attributes instead of duplicating.
- Shorten the three formulation docstrings to reference the constant.
- _resolve_tolerance now requires exactly one of absolute/relative (error on
  both or neither) instead of silently taking the min.
- Rename the cryptic VSC pq/_capability helpers to apparent-power-limit names
  (matching HVDCVSCApparentPowerLimitConstraint); update call sites.
- Drop three stale comments.
- Trim HVDC tests: remove the pure-construction config-bridge testset (replaced
  by a focused tolerance check), coarsen the MILP solve models, and cover only
  representative bilinear schemes (bin2 + nmdt).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The MT-HVDC "QuadraticLossConverter agreement" test compared only the
objective, which is dominated by generation cost while the converters
carry ~zero current at the optimum (the default CopperPlatePowerModel
collapses the two AC islands the DC link bridges, so flow is never
needed). The assertion passed vacuously and the accompanying comment
rationalized it incorrectly.

- Replace it with a conservativeness bound (milp_obj <= nlp_obj * 1.06):
  the bin2 McCormick relaxation lower-bounds the NLP, allowing for the
  5% MIP gap. Use horizon=3h + mip_rel_gap=0.05 so the SOS2 model solves
  in ~1s instead of timing out. Add a TODO documenting why forced flow
  is currently unbuildable (DCPPowerModel + VoltageDispatchHVDCNetworkModel
  fails: QuadraticLossConverter wires into ActivePowerBalance__DCBus,
  which only the copperplate path creates).
- Strengthen the VSC test (genuine forced flow: the VSC replaces a line)
  to assert the solutions agree, not just objectives: both models push
  the VSC past 1.5 pu and aggregate throughput agrees within rtol 0.1.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 8 comments.

Comment thread src/energy_storage_models/storage_constructor.jl
Comment thread src/energy_storage_models/storage_constructor.jl
Comment thread src/energy_storage_models/storage_constructor.jl
Comment thread src/hybrid_system_models/hybridsystem_constructor.jl Outdated
Comment thread src/core/expressions.jl
Comment thread src/PowerOperationsModels.jl
Comment thread src/hybrid_system_models/hybrid_systems.jl
Comment thread test/Project.toml
Anthony Costarelli and others added 3 commits June 9, 2026 17:02
…ids, SOC target

- Fix undefined `Up`/`Down` type params in storage reserve expressions
  (StorageReserveBalanceExpression{Up/Down,...} -> {PSY.ReserveUp/ReserveDown,...}),
  which previously errored when constructing storage ancillary services.
- C4: create TotalReserveOffering containers for every hybrid that participates in a
  reserve service, not just hybrids with storage. get_expression_type_for_reserve
  routes all hybrids' ActivePowerReserveVariable into TotalReserveOffering, so
  storage-less hybrids with reserves no longer hit a missing-container error. The
  subcomponent feed stays gated on storage. Adds a regression test.
- C7: give the hybrid end-of-period energy target its own HybridEnergyTargetConstraint
  (a one-sided floor e_T >= E_T, no slacks) instead of reusing the storage
  StateofChargeTargetConstraint (an equality with surplus/shortfall slacks), and fix
  the hybrid formulation docstring to match.

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

The HybridDispatchWithReserves port of the end-of-period storage energy
target was mis-ported from HybridSystemsSimulations.jl: it dropped the
surplus/shortage slack variables and implemented a hard one-sided floor
(e_T >= E_T) instead of HSS's soft equality with penalized slacks. The
energy_target path was never exercised by any test, so this went unnoticed
(and the slack-typed add_variables!/add_constraints! signatures only
accepted FlattenIteratorWrapper, never the Vector the constructor passes,
so the constraint method never even matched).

Mirror POM's storage StateofChargeTargetConstraint, adapted for the hybrid:
- Add HybridEnergySurplusVariable / HybridEnergyShortageVariable (non-negative,
  final-time-step only) and export them.
- Make HybridEnergyTargetConstraint a soft equality e_T - e+ + e- = E_T.
- Penalize both slacks in the objective from the storage subcomponent's
  StorageCost (energy_surplus_cost / energy_shortage_cost), gated on
  energy_target.
- Add the slacks in the constructor ArgumentConstructStage.
- Broaden the slack add_variables! and the target add_constraints! to accept
  Vector as well as FlattenIteratorWrapper.

Keep the existing target RHS scaling (storage_target is a ratio of capacity in
PSY; the hybrid EnergyVariable is absolute energy), which is the one intentional
divergence from HSS's raw get_storage_target.

Add tests modeled on the storage energy-target tests: assert the slacks exist
and the constraint is an equality (would have caught the regression), that the
slacks are absent when energy_target=false, and an on-vs-off objective check
confirming the penalty reaches the objective.

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

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 16 out of 17 changed files in this pull request and generated 8 comments.

Comment on lines +1592 to +1613
con = get_constraint(container, T, V, "$(s_type)_$(s_name)_discharge")
jm = get_jump_model(container)
if time_offset(T) == -1
con[ci_name, 1] = JuMP.@constraint(
jm,
sustained_param_discharge * reserve_var[ci_name, 1] <= get_value(ic)
)
for t in time_steps[2:end]
con[ci_name, t] = JuMP.@constraint(
jm,
sustained_param_discharge * reserve_var[ci_name, t] <=
energy_var[ci_name, t - 1]
)
end
else # EndOfPeriod
for t in time_steps
con[ci_name, t] = JuMP.@constraint(
jm,
sustained_param_discharge * reserve_var[ci_name, t] <=
energy_var[ci_name, t]
)
end

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

@rodrigomha @jd-lara What do you think of this? I accepted the change, but this isn't how HSS does it.

Comment thread src/hybrid_system_models/hybrid_systems.jl
Comment thread test/test_device_hybrid_constructors.jl Outdated
Comment thread src/core/formulations.jl Outdated
Comment thread src/core/formulations.jl Outdated
Comment thread src/core/constraints.jl
Comment thread test/Project.toml
Comment on lines 35 to 41
[sources]
InfrastructureSystems = {rev = "IS4", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"}
PowerSystems = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerSystems.jl"}
# TODO: Move to main once this branch is merged
PowerSystems = {rev = "ac/hybridsystem-strip-units", url = "https://github.com/Sienna-Platform/PowerSystems.jl"}
InfrastructureOptimizationModels = {rev = "main", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"}
PowerSystemCaseBuilder = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerSystemCaseBuilder.jl"}
PowerNetworkMatrices = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerNetworkMatrices.jl"}
Comment thread test/Project.toml
PowerOperationsModels = "bed98974-b02a-5e2f-9ee0-a103f5c450dd"
PowerSystemCaseBuilder = "f00506e0-b84f-492a-93c2-c0a9afc4364e"
PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd"
PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d"
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
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.

6 participants