Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0bf1ff5
Bilinear hydro formulation and IOM/PSY API updates
Apr 29, 2026
001b82a
Merge branch 'main' into ac/hydro-bilinear
acostarelli Apr 29, 2026
f559b20
Merge remote-tracking branch 'origin/main' into ac/hydro-bilinear
May 12, 2026
9bc4add
Address review comments on bilinear hydro test
May 13, 2026
a73d189
Rename HydroTurbineBin2BilinearDispatch to MILP and expose bilinear_a…
May 13, 2026
57bc7ae
switch HydroTurbineMILPBilinearDispatch from n_segments to tolerance
May 28, 2026
147ec06
Replace bilinear attribute config with POM config structs
May 30, 2026
b938cbd
Merge remote-tracking branch 'origin/main' into ac/hydro-milp-rename
Jun 1, 2026
8363d0b
Simplify bilinear _iom_config bridge against updated IOM contract
Jun 1, 2026
f03f49f
Generalize bilinear configs to x×y and validate tolerance
Jun 2, 2026
de5735d
Return to attribute-based bilinear config, deriving epigraph depth fr…
Jun 5, 2026
b4e1a6c
Merge remote-tracking branch 'origin/main' into ac/hydro-milp-rename
Jun 8, 2026
0414f81
update source branches
Jun 8, 2026
14dbbfd
removed excessive comments and tests
Jun 8, 2026
ccca499
Make converter loss approximations attribute-driven
Jun 8, 2026
475769b
Add relative tolerance to bilinear approximation API
Jun 8, 2026
5ebe8f6
Merge NLP+MILP bilinear formulations into single attribute-driven types
Jun 8, 2026
3919d47
Address June 8 review: centralize bilinear attrs, tighten tolerance, …
Jun 8, 2026
9d930d6
two-layer -> one-layer helper with extra noop for ambiguity
Jun 8, 2026
d954775
remove excessive comments and tests
Jun 8, 2026
1f3d6f7
copilot review
Jun 8, 2026
da98c62
fix target for approximation test
Jun 9, 2026
1b1aa08
Fix vacuous HVDC MILP/NLP agreement test
Jun 9, 2026
43465c2
remove bad test
Jun 9, 2026
40a7cc2
copilot bug fix
Jun 9, 2026
d86ae34
apply fixes to hydro models
jd-lara Jun 9, 2026
ac483ac
Merge branch 'ac/hydro-milp-rename' of github.com:NREL-Sienna/PowerOp…
jd-lara Jun 9, 2026
5fc3007
fix conflict
jd-lara Jun 9, 2026
153b281
delete untracked files
jd-lara Jun 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/PowerOperationsModels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -537,6 +537,7 @@ export HydroWaterFactorModel
export HydroWaterModelReservoir
export HydroTurbineBilinearDispatch
export HydroTurbineWaterLinearDispatch
export HydroTurbineMILPBilinearDispatch
export HydroTurbineWaterLinearCommitment
export HydroEnergyModelReservoir
export HydroTurbineEnergyDispatch
Expand Down
35 changes: 35 additions & 0 deletions src/core/formulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,40 @@ Formulation type to add injection variables for a HydroTurbine connected to rese
"""
struct HydroTurbineBilinearDispatch <: AbstractHydroDispatchFormulation end

"""
MILP formulation for the turbined-flow × head bilinear product in the hydro
turbine power-output constraint. Adds injection variables for a HydroTurbine
connected to reservoirs using a linearized approximation of the bilinear model.

Selects the bilinear approximation scheme via `DeviceModel` attributes (so users
do not need to depend on `InfrastructureOptimizationModels`). All attributes
have defaults that reproduce the prior `Bin2` + `SolverSOS2`(4) behavior.

# Attributes
- `bilinear_approximation::String` (default `"bin2"`): top-level scheme.
Supported: `"bin2"`, `"hybs"`, `"nmdt"`, `"dnmdt"`, `"none"`.
- `bilinear_quadratic_method::String` (default `"solver_sos2"`): inner quadratic
PWL method used when `bilinear_approximation ∈ {"bin2","hybs"}`. Supported:
`"solver_sos2"`, `"manual_sos2"`, `"sawtooth"`, `"epigraph"`, `"nmdt"`,
`"dnmdt"`, `"none"`.
- `bilinear_n_segments::Int` (default `4`): PWL segment count or NMDT depth,
depending on the chosen method.
- `bilinear_add_mccormick::Union{Bool,Nothing}` (default `nothing`): when
`nothing`, defers to the IOM struct default (`Bin2Config` → `true`,
`HybSConfig` → `false`). Ignored by `nmdt`/`dnmdt`/`none`.
- `bilinear_epigraph_depth::Union{Int,Nothing}` (default `nothing`): when
`nothing`, defers to the IOM struct default (`NMDTQuadConfig` /
`DNMDTQuadConfig` → `3*depth`). `"hybs"` has no IOM default for this field
and *must* override.

# Sentinel convention
`nothing` means "use the IOM constructor's default value." POM does not
duplicate IOM struct defaults; it just passes through the user's overrides.

See: [`PowerSystems.HydroGen`](@extref).
"""
struct HydroTurbineMILPBilinearDispatch <: AbstractHydroDispatchFormulation end

"""
Formulation type to add injection variables for a HydroTurbine connected to reservoirs using a linear model [`PowerSystems.HydroGen`](@extref).
The model assumes a shallow reservoir. The head for the conversion between water flow and power can be approximated as a linear function of the water flow on which the head elevation is always the intake elevation.
Expand Down Expand Up @@ -364,6 +398,7 @@ These types share constructors.
"""
const HydroTurbineWaterFormulation = Union{
HydroTurbineBilinearDispatch,
HydroTurbineMILPBilinearDispatch,
HydroTurbineWaterLinearDispatch,
HydroTurbineWaterLinearCommitment,
}
Expand Down
206 changes: 206 additions & 0 deletions src/static_injector_models/hydro_generation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,41 @@ function get_default_attributes(
return Dict{String, Any}("head_fraction_usage" => 0.0)
end

"""
Default `DeviceModel` attributes for `HydroTurbineMILPBilinearDispatch`. The
returned dictionary picks the bilinear approximation scheme used inside the
turbine-power constraint; see [`HydroTurbineMILPBilinearDispatch`](@ref) for the
full attribute reference and the `nothing` sentinel convention.

The defaults reproduce the pre-rename behavior bit-for-bit:
`IOM.Bin2Config(IOM.SolverSOS2QuadConfig(4))` with `add_mccormick = true`
(inherited from `Bin2Config`'s one-argument constructor).
"""
function get_default_attributes(
Comment thread
acostarelli marked this conversation as resolved.
::Type{T},
::Type{D},
) where {T <: PSY.HydroTurbine, D <: HydroTurbineMILPBilinearDispatch}
return Dict{String, Any}(
# Top-level bilinear approximation scheme.
# Supported: "bin2", "hybs", "nmdt", "dnmdt", "none".
"bilinear_approximation" => "bin2",
# Inner quadratic PWL method (used when bilinear_approximation ∈ {"bin2","hybs"}).
# Supported: "solver_sos2", "manual_sos2", "sawtooth", "epigraph",
# "nmdt", "dnmdt", "none".
"bilinear_quadratic_method" => "solver_sos2",
Comment thread
acostarelli marked this conversation as resolved.
Outdated
# Segment count / discretization depth. SOS2/sawtooth/epigraph use it as
# n_segments; NMDT/DNMDT use it as depth.
"bilinear_n_segments" => 4,
# `nothing` ⇒ defer to IOM struct default
# (Bin2Config: true; HybSConfig: false; ignored otherwise).
"bilinear_add_mccormick" => nothing,
# `nothing` ⇒ defer to IOM struct default
# (HybSConfig has no default and must be overridden; NMDT/DNMDT
# quadratic: 3*depth; ignored otherwise).
"bilinear_epigraph_depth" => nothing,
)
end

function get_default_attributes(
::Type{T},
::Type{D},
Expand Down Expand Up @@ -1817,6 +1852,177 @@ function add_constraints!(
return
end

"""
Build an `IOM.QuadraticApproxConfig` from attribute values.

`n` is the segment count / discretization depth (`bilinear_n_segments`).
`epi` is either an `Int` (`bilinear_epigraph_depth` override) or `nothing`,
in which case IOM's struct default applies — relevant only for `NMDTQuadConfig`
and `DNMDTQuadConfig`, which use `3*depth` when called with one argument.

Errors with a list of supported method strings when `method` is unrecognized.
"""
function _build_quadratic_config(method::String, n::Int, epi)
if method == "solver_sos2"
return IOM.SolverSOS2QuadConfig(n)
elseif method == "manual_sos2"
return IOM.ManualSOS2QuadConfig(n)
elseif method == "sawtooth"
return IOM.SawtoothQuadConfig(n)
elseif method == "epigraph"
return IOM.EpigraphQuadConfig(n)
elseif method == "nmdt"
return epi === nothing ? IOM.NMDTQuadConfig(n) : IOM.NMDTQuadConfig(n, epi)
elseif method == "dnmdt"
return epi === nothing ? IOM.DNMDTQuadConfig(n) : IOM.DNMDTQuadConfig(n, epi)
elseif method == "none"
return IOM.NoQuadApproxConfig()
else
error(
"Unsupported bilinear_quadratic_method \"$(method)\". " *
"Supported: \"solver_sos2\", \"manual_sos2\", \"sawtooth\", " *
"\"epigraph\", \"nmdt\", \"dnmdt\", \"none\".",
)
end
end

"""
Build an `IOM.BilinearApproxConfig` from the `DeviceModel`'s attributes.

Reads `bilinear_approximation`, `bilinear_quadratic_method`,
`bilinear_n_segments`, `bilinear_add_mccormick`, and `bilinear_epigraph_depth`
from `model`. Sentinel (`nothing`) attribute values mean "let the IOM
constructor's default apply" — POM never duplicates an IOM struct default.

Errors when:
- `bilinear_approximation` is not one of `"bin2"`, `"hybs"`, `"nmdt"`,
`"dnmdt"`, `"none"`.
- `bilinear_approximation == "hybs"` but `bilinear_epigraph_depth === nothing`
(`HybSConfig` has no IOM-side default for `epigraph_depth`).

See [`HydroTurbineMILPBilinearDispatch`](@ref) for the attribute reference.
"""
function _build_bilinear_config(
model::DeviceModel{<:PSY.HydroTurbine, HydroTurbineMILPBilinearDispatch},
)
method = get_attribute(model, "bilinear_approximation")
n_segments = get_attribute(model, "bilinear_n_segments")
add_mc = get_attribute(model, "bilinear_add_mccormick")
epi = get_attribute(model, "bilinear_epigraph_depth")

if method == "bin2"
quad = _build_quadratic_config(
get_attribute(model, "bilinear_quadratic_method"),
n_segments,
epi,
)
return add_mc === nothing ? IOM.Bin2Config(quad) : IOM.Bin2Config(quad, add_mc)
elseif method == "hybs"
epi === nothing && error(
"bilinear_approximation = \"hybs\" requires a non-nothing " *
"bilinear_epigraph_depth attribute (IOM.HybSConfig has no default).",
)
quad = _build_quadratic_config(
get_attribute(model, "bilinear_quadratic_method"),
n_segments,
epi,
)
return if add_mc === nothing
IOM.HybSConfig(quad, epi)
else
IOM.HybSConfig(quad, epi, add_mc)
end
elseif method == "nmdt"
return IOM.NMDTBilinearConfig(n_segments)
elseif method == "dnmdt"
return IOM.DNMDTBilinearConfig(n_segments)
elseif method == "none"
return IOM.NoBilinearApproxConfig()
else
error(
"Unsupported bilinear_approximation \"$(method)\" for " *
"HydroTurbineMILPBilinearDispatch. " *
"Supported: \"bin2\", \"hybs\", \"nmdt\", \"dnmdt\", \"none\".",
)
end
end

"""
This function define the relationship between turbined flow and power produced with a linear approximation for the bilinear product.
Comment thread
acostarelli marked this conversation as resolved.
Outdated
"""
function add_constraints!(
container::OptimizationContainer,
sys::PSY.System,
::Type{TurbinePowerOutputConstraint},
devices::IS.FlattenIteratorWrapper{V},
model::DeviceModel{V, W},
::NetworkModel{X},
) where {
V <: PSY.HydroTurbine,
W <: HydroTurbineMILPBilinearDispatch,
X <: PM.AbstractPowerModel,
}
time_steps = get_time_steps(container)
base_power = get_model_base_power(container)
names = PSY.get_name.(devices)
constraint =
add_constraints_container!(
container,
TurbinePowerOutputConstraint,
V,
names,
time_steps,
)
power = get_variable(container, ActivePowerVariable, V)
flow = get_variable(container, HydroTurbineFlowRateVariable, V)
head = get_variable(container, HydroReservoirHeadVariable, PSY.HydroReservoir)
for d in devices
Comment thread
acostarelli marked this conversation as resolved.
name = PSY.get_name(d)
conversion_factor = PSY.get_conversion_factor(d)
reservoirs = filter(PSY.get_available, PSY.get_connected_head_reservoirs(sys, d))
powerhouse_elevation = PSY.get_powerhouse_elevation(d)

Comment thread
acostarelli marked this conversation as resolved.
fh_prod = IOM._add_bilinear_approx!(
_build_bilinear_config(model),
container,
V,
PSY.get_name.(reservoirs),
time_steps,
flow[name, :, :],
head,
repeat(
[
(
min = get_variable_lower_bound(HydroTurbineFlowRateVariable, d, W),
max = get_variable_upper_bound(HydroTurbineFlowRateVariable, d, W),
Comment thread
acostarelli marked this conversation as resolved.
Outdated
)
], length(reservoirs)),
[
(
min = get_variable_lower_bound(HydroReservoirHeadVariable, res, W),
max = get_variable_upper_bound(HydroReservoirHeadVariable, res, W),
) for res in reservoirs
Comment thread
acostarelli marked this conversation as resolved.
Outdated
],
"$(get_name(d))_FlowHeadProduct",
)

for t in time_steps
constraint[name, t] = JuMP.@constraint(
container.JuMPmodel,
power[name, t] ==
GRAVITATIONAL_CONSTANT * WATER_DENSITY * conversion_factor *
sum(
fh_prod[PSY.get_name(res), t]
-
powerhouse_elevation * flow[name, PSY.get_name(res), t]
for res in reservoirs
) / (1e6 * base_power)
)
end
end
return
end

############################################################################
############################### Expressions ################################
############################################################################
Expand Down
6 changes: 5 additions & 1 deletion src/static_injector_models/hydrogeneration_constructor.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1809,7 +1809,11 @@ _maybe_add_on_variables!(
_maybe_add_on_variables!(
::OptimizationContainer,
devices,
::Union{Type{HydroTurbineBilinearDispatch}, Type{HydroTurbineWaterLinearDispatch}},
::Union{
Type{HydroTurbineBilinearDispatch},
Type{HydroTurbineMILPBilinearDispatch},
Type{HydroTurbineWaterLinearDispatch},
},
) = nothing

function construct_device!(
Expand Down
6 changes: 5 additions & 1 deletion test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,15 @@ const DISABLED_TEST_FILES = [ # Can generate with ls -1 test | grep "test_.*.jl
# "test_device_synchronous_condenser_constructors.jl",
# "test_device_thermal_generation_constructors.jl",
# "test_formulation_combinations.jl",
# "test_import_export_cost.jl",
# "test_initialization_problem.jl",
# "test_is_time_variant_proportional.jl",
# "test_market_bid_cost.jl",
# "test_mbc_parameter_population.jl",
# "test_model_decision.jl",
# "test_multi_interval.jl",
# "test_network_constructors_with_dlr.jl",
# "test_network_constructors.jl",
# "test_network_constructors_with_dlr.jl",
# "test_problem_template.jl",
# "test_storage_device_models.jl",
# "test_transfer_initial_conditions.jl",
Expand Down
Loading
Loading