Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ PowerFlows = "94fada2c-0ca5-4b90-a1fb-4bc5b59ccfc7"
[sources]
InfrastructureSystems = {rev = "IS4", url = "https://github.com/Sienna-Platform/InfrastructureSystems.jl"}
PowerSystems = {rev = "psy6", url = "https://github.com/Sienna-Platform/PowerSystems.jl"}
InfrastructureOptimizationModels = {rev = "main", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"}
InfrastructureOptimizationModels = {rev = "ac/tolerance-option", url = "https://github.com/Sienna-Platform/InfrastructureOptimizationModels.jl"}
Comment thread
acostarelli marked this conversation as resolved.
Outdated

[extensions]
PowerFlowsExt = "PowerFlows"
Expand Down
17 changes: 16 additions & 1 deletion src/PowerOperationsModels.jl
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,7 @@ include("core/constraints.jl")
include("core/auxiliary_variables.jl")
include("core/parameters.jl")
include("core/formulations.jl")
include("core/bilinear_configs.jl")
include("core/network_formulations.jl")
include("core/problem_template.jl")
include("core/feedforward_interface.jl")
Expand Down Expand Up @@ -530,14 +531,28 @@ export HydroWaterFactorModel
export HydroWaterModelReservoir
export HydroTurbineBilinearDispatch
export HydroTurbineWaterLinearDispatch
export HydroTurbineBin2BilinearDispatch
export HydroTurbineMILPBilinearDispatch
export HydroTurbineWaterLinearCommitment
export HydroEnergyModelReservoir
export HydroTurbineEnergyDispatch
export HydroTurbineEnergyCommitment
export HydroPumpEnergyDispatch
export HydroPumpEnergyCommitment

# Bilinear approximation configs for HydroTurbineMILPBilinearDispatch
export Bin2Config
export HybSConfig
export NMDTConfig
export DNMDTConfig
export NoBilinearApprox
# Inner quadratic-approximation method markers
export SolverSOS2
export ManualSOS2
export Sawtooth
export Epigraph
export NMDTQuad
export DNMDTQuad

######## Hydro Variables ########
export WaterSpillageVariable
export HydroEnergyShortageVariable
Expand Down
237 changes: 237 additions & 0 deletions src/core/bilinear_configs.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
# Bilinear-approximation configuration for `HydroTurbineMILPBilinearDispatch`.

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.

It is not just for hydro stuff. Make sure none of this is hydro-specific. Yes that's the name of the branch, but it's not the only use-case for this stuff.

#
# These POM-owned types let a user select the bilinear approximation scheme (and
# its inner quadratic method) for the turbined-flow × head product *by type*,

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.

Also hydro specific

# through the single `"bilinear_config"` `DeviceModel` attribute — without
# depending on `InfrastructureOptimizationModels` (IOM). The accuracy of each
# scheme is driven by a `tolerance`; the discretization depth is derived per
# device at constraint-build time from the tolerance and the device's flow / head

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.

Hydro specific

# ranges (see `_iom_config`), so the user never sets a manual depth / segment
# count.
#
# `_iom_config` translates these descriptors into the corresponding IOM config
# value used by `IOM._add_bilinear_approx!`. The approximation math itself lives
# entirely in IOM; this file is only the tolerance → IOM-config bridge.

############################ Inner quadratic methods #######################################

"""
Abstract supertype for the inner quadratic-approximation method used by the
[`Bin2Config`](@ref) and [`HybSConfig`](@ref) bilinear schemes (those schemes
approximate `f × h` via squared terms like `(f+h)²`, which each need a quadratic

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.

f and h are short for flow and head which, again, are hydro specific

PWL method). The marker types carry no data: the discretization depth is derived
from the bilinear config's `tolerance` per device.
"""
abstract type AbstractQuadApproxMethod end

"""
Solver-handled SOS2 piecewise-linear quadratic approximation (default inner
method). Worst-case gap `Δ²/(4·d²)`, so depth scales with `Δ/(2·√tolerance)`.
"""
struct SolverSOS2 <: AbstractQuadApproxMethod end

"""
Manually-formulated SOS2 piecewise-linear quadratic approximation. Same error
bound as [`SolverSOS2`](@ref); does not rely on solver SOS2 support.
"""
struct ManualSOS2 <: AbstractQuadApproxMethod end

"""
Sawtooth (binary-logarithmic) quadratic approximation. Worst-case gap
`Δ²·2^{-2L-2}`.
"""
struct Sawtooth <: AbstractQuadApproxMethod end

"""
Epigraph (one-sided-under) quadratic approximation. Valid only as an internal
cross-term method; it is *not* a permitted inner quad for [`Bin2Config`](@ref)
or [`HybSConfig`](@ref) (the tolerance derivation requires a one-sided-over
inner quad), and is therefore excluded from their `quad` field types.
"""
struct Epigraph <: AbstractQuadApproxMethod end

"""
NMDT (Normalized Multiparametric Disaggregation) quadratic approximation used as
an inner quad for [`Bin2Config`](@ref). Built at the IOM default `epigraph_depth`;
`IOM.tolerance_depth(Bin2Config{NMDTQuadConfig})` accounts for its two-sidedness.
Distinct from the top-level [`NMDTConfig`](@ref) bilinear scheme.
"""
struct NMDTQuad <: AbstractQuadApproxMethod end

"""
DNMDT (Double NMDT) quadratic approximation used as an inner quad for
[`Bin2Config`](@ref) (see [`NMDTQuad`](@ref)). Distinct from the top-level
[`DNMDTConfig`](@ref) bilinear scheme.
"""
struct DNMDTQuad <: AbstractQuadApproxMethod end

"""
Inner quadratic methods valid for [`Bin2Config`](@ref): everything except
[`Epigraph`](@ref), which is one-sided-under and breaks the Bin2 tolerance
derivation.
"""
const Bin2Quad = Union{SolverSOS2, ManualSOS2, Sawtooth, NMDTQuad, DNMDTQuad}

"""
Inner quadratic methods valid for [`HybSConfig`](@ref): only the SOS2 variants

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.

Please make sure these unions and their doc-strings are up-to-date with what IOM says.

and [`Sawtooth`](@ref). The HybS sandwich requires a one-sided-over inner quad
with no epigraph tightening, which rules out the NMDT/DNMDT inner quads as well
as [`Epigraph`](@ref).
"""
const HybSQuad = Union{SolverSOS2, ManualSOS2, Sawtooth}

############################ Bilinear approximation configs ################################

"""
Abstract supertype for the bilinear-approximation scheme selected through the
`"bilinear_config"` attribute of a [`HydroTurbineMILPBilinearDispatch`](@ref)
`DeviceModel`.
"""
abstract type AbstractBilinearApproxConfig end

"""
Bin2 bilinear approximation (default scheme). Linearizes `f × h` via the identity
`f·h = ½((f+h)² − f² − h²)`, approximating each square with the inner quadratic
method `quad`.

# Fields
- `tolerance::Float64` (default `1e-2`): maximum approximation gap. The
discretization depth is derived per device from this tolerance and the
device's flow / head ranges via `IOM.tolerance_depth` (no manual depth knob).
- `quad::`[`Bin2Quad`](@ref) (default [`SolverSOS2`](@ref)`()`): inner quadratic
method. [`Epigraph`](@ref) is intentionally not assignable.
"""
Base.@kwdef struct Bin2Config <: AbstractBilinearApproxConfig
tolerance::Float64 = 1e-2
quad::Bin2Quad = SolverSOS2()
end

"""
HybS (Hybrid Separable) bilinear approximation. Sandwiches `f·h` between a Bin2
lower bound and a Bin3 upper bound, using the inner quadratic method `quad` for
the shared `f²`, `h²` terms and an internal epigraph approximation (sized from
the same `tolerance`) for the cross terms.

# Fields
- `tolerance::Float64` (default `1e-2`): maximum approximation gap. Both the
inner-quad depth and the cross-term epigraph depth are derived per device from
this tolerance via `IOM.tolerance_depth` / `IOM.tolerance_epigraph_depth`.
- `quad::`[`HybSQuad`](@ref) (default [`SolverSOS2`](@ref)`()`): inner quadratic
method. Only the SOS2 variants and [`Sawtooth`](@ref) are assignable.
"""
Base.@kwdef struct HybSConfig <: AbstractBilinearApproxConfig
tolerance::Float64 = 1e-2
quad::HybSQuad = SolverSOS2()
end
Comment thread
acostarelli marked this conversation as resolved.
Outdated

"""
NMDT (Normalized Multiparametric Disaggregation) bilinear approximation
(discretizes `f` only). Worst-case relaxation gap `Δf·Δh·2^{-L-2}`.

# Fields
- `tolerance::Float64` (default `1e-2`): maximum approximation gap; the depth `L`
is derived per device from it and the flow / head ranges via
`IOM.tolerance_depth`.
"""
Base.@kwdef struct NMDTConfig <: AbstractBilinearApproxConfig
tolerance::Float64 = 1e-2
end
Comment thread
acostarelli marked this conversation as resolved.
Outdated

"""
DNMDT (Double NMDT) bilinear approximation (discretizes both `f` and `h`).
Worst-case relaxation gap `Δf·Δh·2^{-2L-2}`.

# Fields
- `tolerance::Float64` (default `1e-2`): maximum approximation gap; the depth `L`
is derived per device from it and the flow / head ranges via
`IOM.tolerance_depth`.
"""
Base.@kwdef struct DNMDTConfig <: AbstractBilinearApproxConfig
tolerance::Float64 = 1e-2
end
Comment thread
acostarelli marked this conversation as resolved.
Outdated

"""
Pass the quadratic `f × h` term to the solver directly, with no MILP
linearization. Use this with a nonlinear-capable solver; the resulting model is
not a MILP.
"""
struct NoBilinearApprox <: AbstractBilinearApproxConfig end

############################ Translation to IOM configs ####################################

# Map a POM inner-quad marker to the corresponding IOM quadratic-approx config TYPE.
_iom_quad_config_type(::SolverSOS2) = IOM.SolverSOS2QuadConfig
_iom_quad_config_type(::ManualSOS2) = IOM.ManualSOS2QuadConfig
_iom_quad_config_type(::Sawtooth) = IOM.SawtoothQuadConfig
_iom_quad_config_type(::Epigraph) = IOM.EpigraphQuadConfig
_iom_quad_config_type(::NMDTQuad) = IOM.NMDTQuadConfig
_iom_quad_config_type(::DNMDTQuad) = IOM.DNMDTQuadConfig

# TODO: McCormick cuts (`add_mccormick`) are dropped for now — we always defer to
# the IOM config's own default. Decide when they should be enabled and surface
# that through the `tolerance_depth` helper (so it stays a tolerance-driven
# decision) rather than re-exposing a raw knob here.

"""
Translate a POM [`AbstractBilinearApproxConfig`](@ref) into the IOM bilinear
config consumed by `IOM._add_bilinear_approx!`, sizing the discretization from
the config's `tolerance` and the per-device domain widths (`delta_x`, `delta_y`).

Each IOM `tolerance_depth` / `tolerance_epigraph_depth` helper inverts its
method's worst-case-gap bound and allocates the error budget across the inner
quadratic, so POM never sizes the inner quad by hand — it just builds the inner
quad at the returned `depth` (with the IOM-default `epigraph_depth`). Per-scheme
inner-quad validity is enforced statically by the `quad` field types
([`Bin2Quad`](@ref) / [`HybSQuad`](@ref)).
"""
function _iom_config end

_iom_config(::NoBilinearApprox, ::Float64, ::Float64) = IOM.NoBilinearApproxConfig()

function _iom_config(config::Bin2Config, delta_x::Float64, delta_y::Float64)
Q = _iom_quad_config_type(config.quad)
depth = IOM.tolerance_depth(
IOM.Bin2Config{Q};
tolerance = config.tolerance,
max_delta_x = delta_x,
max_delta_y = delta_y,
)
return IOM.Bin2Config(Q(; depth))
end

function _iom_config(config::HybSConfig, delta_x::Float64, delta_y::Float64)
Q = _iom_quad_config_type(config.quad)
depth = IOM.tolerance_depth(
IOM.HybSConfig{Q};
tolerance = config.tolerance,
max_delta_x = delta_x,
max_delta_y = delta_y,
)
epigraph_depth = IOM.tolerance_epigraph_depth(
IOM.HybSConfig{Q};
tolerance = config.tolerance,
max_delta_x = delta_x,
max_delta_y = delta_y,
)
return IOM.HybSConfig(Q(; depth); epigraph_depth)
end

function _iom_config(config::NMDTConfig, delta_x::Float64, delta_y::Float64)
depth = IOM.tolerance_depth(
IOM.NMDTBilinearConfig;
tolerance = config.tolerance,
max_delta_x = delta_x,
max_delta_y = delta_y,
)
return IOM.NMDTBilinearConfig(; depth)
end

function _iom_config(config::DNMDTConfig, delta_x::Float64, delta_y::Float64)
depth = IOM.tolerance_depth(
IOM.DNMDTBilinearConfig;
tolerance = config.tolerance,
max_delta_x = delta_x,
max_delta_y = delta_y,
)
return IOM.DNMDTBilinearConfig(; depth)
end
36 changes: 33 additions & 3 deletions src/core/formulations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -364,9 +364,39 @@ Formulation type to add injection variables for a HydroTurbine connected to rese
struct HydroTurbineBilinearDispatch <: AbstractHydroDispatchFormulation end

"""
Formulation type to add injection variables for a HydroTurbine connected to reservoirs using a bilinear model (with water flow variables) [`PowerSystems.HydroGen`](@extref). Uses a linearized approximation.
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.

The bilinear approximation scheme is selected *by type* through the single
`"bilinear_config"` `DeviceModel` attribute, whose value is an
[`AbstractBilinearApproxConfig`](@ref): [`Bin2Config`](@ref) (default),
[`HybSConfig`](@ref), [`NMDTConfig`](@ref), [`DNMDTConfig`](@ref), or
[`NoBilinearApprox`](@ref). Users do not need to depend on
`InfrastructureOptimizationModels`; POM translates the config internally.

Each config carries a `tolerance` (the maximum approximation gap); the
constraint constructor derives the discretization depth per device from that
tolerance combined with the device's flow and head ranges (via IOM's
`tolerance_depth` helpers), so there is no manual depth / segment-count knob. The
[`Bin2Config`](@ref) and [`HybSConfig`](@ref) schemes additionally take a typed
inner quadratic method (`quad`); invalid scheme/quad combinations are rejected
when the config is constructed.

# Attributes
- `"bilinear_config"` (default [`Bin2Config`](@ref)`()`): an
[`AbstractBilinearApproxConfig`](@ref) selecting the scheme and its parameters.

# Example
```julia
set_device_model!(template, HydroTurbine, HydroTurbineMILPBilinearDispatch) # Bin2, tol 1e-2
set_device_model!(template, HydroTurbine, HydroTurbineMILPBilinearDispatch;
attributes = Dict("bilinear_config" => NMDTConfig(tolerance = 1e-3)))
```

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

"""
Formulation type to add injection variables for a HydroTurbine connected to reservoirs using a linear model [`PowerSystems.HydroGen`](@extref).
Expand Down Expand Up @@ -406,7 +436,7 @@ These types share constructors.
"""
const HydroTurbineWaterFormulation = Union{
HydroTurbineBilinearDispatch,
HydroTurbineBin2BilinearDispatch,
HydroTurbineMILPBilinearDispatch,
HydroTurbineWaterLinearDispatch,
HydroTurbineWaterLinearCommitment,
}
Expand Down
Loading
Loading