Source code for monee.model.formulation.bundles
"""Sector formulation constants and sector-complete network bundles.
Per-sector building blocks follow ``<SECTOR>_<CLASS>_FORMULATION``; the
network-wide bundles cover all three sectors with one consistent
optimization class:
* :data:`SMOOTH_NLP_FORMULATION` - smooth non-convex NLP (IPOPT/APOPT):
polar AC + smooth Weymouth + smooth Darcy-Weisbach.
* :data:`CONVEX_MIQCQP_FORMULATION` - convex relaxations a MISOCP/MIQCQP
solver can certify: branch-flow MISOCP + epigraph-relaxed Weymouth +
McCormick district heating (incl. convex heat-exchanger variants).
* :data:`NONCONVEX_MIQCQP_FORMULATION` - the exact quadratic models for
global solvers (SCIP, Gurobi): exact branch flow + exact Weymouth +
bilinear Darcy-Weisbach.
* :data:`DEFAULT_SIMULATION_FORMULATION` - the deliberate hybrid
:class:`~monee.model.network.Network` applies by default (polar-AC NLP +
relaxed Weymouth + bilinear Darcy-Weisbach).
"""
from monee.model.branch import (
GasPipe,
GenericPowerBranch,
HeatExchanger,
PassiveHeatExchanger,
WaterPipe,
)
from monee.model.grid import GasGrid, WaterGrid
from monee.model.node import Bus, Junction
from .common import GasNodeFormulation, WaterNodeFormulation
from .core import NetworkFormulation
from .milp.gas import PwlWeymouthBranchFormulation
from .milp.heat import (
FixedFlowHeatExchangerFormulation,
McCormickHeatBranchFormulation,
McCormickHeatExchangerFormulation,
McCormickHeatNodeFormulation,
McCormickPassiveHeatExchangerFormulation,
)
from .miqcqp.convex.el import (
MISOCPElectricityBranchFormulation,
MISOCPElectricityNodeFormulation,
)
from .miqcqp.convex.gas import RelaxedWeymouthBranchFormulation
from .miqcqp.nonconvex.el import (
ExactBranchFlowBranchFormulation,
ExactBranchFlowNodeFormulation,
)
from .miqcqp.nonconvex.gas import ExactWeymouthBranchFormulation
from .miqcqp.nonconvex.heat import (
BilinearDarcyWeisbachBranchFormulation,
BilinearPassiveHeatExchangerFormulation,
PwlDarcyWeisbachBranchFormulation,
)
from .nlp.el import AcPolarNlpBranchFormulation, AcPolarNlpNodeFormulation
from .nlp.gas import SmoothWeymouthBranchFormulation
from .nlp.heat import (
SmoothDarcyWeisbachBranchFormulation,
SmoothHeatExchangerFormulation,
SmoothPassiveHeatExchangerFormulation,
)
[docs]
def combine(*network_formulations: NetworkFormulation) -> NetworkFormulation:
"""Merge per-sector :class:`NetworkFormulation` objects into one apply.
Later arguments win on type collisions."""
branch, node, child, compound = {}, {}, {}, {}
for nf in network_formulations:
branch.update(nf.branch_type_to_formulations)
node.update(nf.node_type_to_formulations)
child.update(nf.child_type_to_formulations)
compound.update(nf.compound_type_to_formulations)
return NetworkFormulation(
branch_type_to_formulations=branch,
node_type_to_formulations=node,
child_type_to_formulations=child,
compound_type_to_formulations=compound,
)
# ---------------------------------------------------------------------------
# Electricity
# ---------------------------------------------------------------------------
# Single AC formulation for both modes. Under a simulation solve the node
# formulation's ``ensure_var(simulation=True)`` demotes vm_pu_squared to a
# PostProcess report (no solver variable), removing the phantom DOF so an
# IMODE=1 square solve is feasible; the optimization solve keeps it.
EL_NLP_FORMULATION = NetworkFormulation(
branch_type_to_formulations={GenericPowerBranch: AcPolarNlpBranchFormulation()},
node_type_to_formulations={Bus: AcPolarNlpNodeFormulation()},
)
EL_MISOCP_FORMULATION = NetworkFormulation(
branch_type_to_formulations={
GenericPowerBranch: MISOCPElectricityBranchFormulation()
},
node_type_to_formulations={Bus: MISOCPElectricityNodeFormulation()},
)
EL_NONCONVEX_MIQCQP_FORMULATION = NetworkFormulation(
branch_type_to_formulations={
GenericPowerBranch: ExactBranchFlowBranchFormulation()
},
node_type_to_formulations={Bus: ExactBranchFlowNodeFormulation()},
)
# ---------------------------------------------------------------------------
# Gas
# ---------------------------------------------------------------------------
GAS_CONVEX_MIQCQP_FORMULATION = NetworkFormulation(
branch_type_to_formulations={
GasPipe: RelaxedWeymouthBranchFormulation(),
},
node_type_to_formulations={(Junction, GasGrid): GasNodeFormulation()},
)
GAS_NONCONVEX_MIQCQP_FORMULATION = NetworkFormulation(
branch_type_to_formulations={
GasPipe: ExactWeymouthBranchFormulation(),
},
node_type_to_formulations={(Junction, GasGrid): GasNodeFormulation()},
)
[docs]
def make_gas_milp_pwl_formulation(n_breakpoints: int = 12) -> NetworkFormulation:
r"""Variable-friction Weymouth via per-pipe PWL of :math:`\varphi(m) = friction(Re(m)) \cdot m^2`.
Opt-in alternative to :data:`GAS_CONVEX_MIQCQP_FORMULATION` for networks
where laminar-regime accuracy matters (``Re < 2300`` on lightly-loaded
pipes). See
:class:`monee.model.formulation.milp.gas.PwlWeymouthBranchFormulation`
for details and trade-offs.
"""
return NetworkFormulation(
branch_type_to_formulations={
GasPipe: PwlWeymouthBranchFormulation(n_breakpoints=n_breakpoints),
},
node_type_to_formulations={(Junction, GasGrid): GasNodeFormulation()},
)
[docs]
def make_gas_nlp_formulation(
friction_model: str = "constant",
smoothing_eps: float = 1e-3,
n_breakpoints: int = 12,
) -> NetworkFormulation:
"""Pure-NLP Weymouth gas formulation for GEKKO IPOPT/APOPT.
Binary-free (no ``direction`` switch), numerically smooth signed pressure
drop. ``friction_model`` selects ``"constant"`` / ``"pwl"`` / ``"nonlinear"``
friction. Opt-in alternative to :data:`GAS_CONVEX_MIQCQP_FORMULATION` for
full-MES solves where the MISOCP-shaped default stalls IPOPT.
"""
return NetworkFormulation(
branch_type_to_formulations={
GasPipe: SmoothWeymouthBranchFormulation(
friction_model=friction_model,
smoothing_eps=smoothing_eps,
n_breakpoints=n_breakpoints,
),
},
node_type_to_formulations={(Junction, GasGrid): GasNodeFormulation()},
)
GAS_NLP_FORMULATION = make_gas_nlp_formulation()
# ---------------------------------------------------------------------------
# Water / heat
# ---------------------------------------------------------------------------
HEAT_NONCONVEX_MIQCQP_FORMULATION = NetworkFormulation(
branch_type_to_formulations={
WaterPipe: BilinearDarcyWeisbachBranchFormulation(),
HeatExchanger: FixedFlowHeatExchangerFormulation(),
PassiveHeatExchanger: BilinearPassiveHeatExchangerFormulation(),
},
node_type_to_formulations={(Junction, WaterGrid): WaterNodeFormulation()},
)
[docs]
def make_heat_nonconvex_pwl_formulation(
n_breakpoints: int = 12,
) -> NetworkFormulation:
r"""Variable-friction Darcy-Weisbach via per-pipe PWL of :math:`\varphi(m)`.
Opt-in alternative to :data:`HEAT_NONCONVEX_MIQCQP_FORMULATION` for
laminar-heavy networks (Re < 2300); the default's asymptotic shortcut
under-estimates pressure drop there. HeatExchanger formulations are
unaffected.
"""
return NetworkFormulation(
branch_type_to_formulations={
WaterPipe: PwlDarcyWeisbachBranchFormulation(n_breakpoints=n_breakpoints),
HeatExchanger: FixedFlowHeatExchangerFormulation(),
PassiveHeatExchanger: BilinearPassiveHeatExchangerFormulation(),
},
node_type_to_formulations={(Junction, WaterGrid): WaterNodeFormulation()},
)
[docs]
def make_heat_convex_milp_formulation(
num_partitions: int = 1,
include_heat_exchangers: bool = True,
) -> NetworkFormulation:
"""McCormick district-heating relaxation: LP for ``num_partitions == 1``,
piecewise MILP above. Raise the partition count only when
:func:`~monee.model.formulation.milp.heat.mccormick_dhs_gap_bound_k` shows
the LP corner is non-physical on the network at hand.
``include_heat_exchangers`` also maps the active/passive heat exchangers
to their convex variants so the sector has no non-convex fallback; disable
it to reproduce the legacy pipes-only McCormick apply.
"""
branch_map = {
WaterPipe: McCormickHeatBranchFormulation(num_partitions=num_partitions),
}
if include_heat_exchangers:
branch_map[HeatExchanger] = McCormickHeatExchangerFormulation()
branch_map[PassiveHeatExchanger] = McCormickPassiveHeatExchangerFormulation(
num_partitions=num_partitions
)
return NetworkFormulation(
branch_type_to_formulations=branch_map,
node_type_to_formulations={
(Junction, WaterGrid): McCormickHeatNodeFormulation(
num_partitions=num_partitions
),
},
)
HEAT_CONVEX_MILP_FORMULATION = make_heat_convex_milp_formulation(num_partitions=1)
[docs]
def make_heat_nlp_formulation(
friction_model: str = "constant",
smoothing_eps: float = 1e-3,
n_breakpoints: int = 12,
) -> NetworkFormulation:
"""Pure-NLP Darcy-Weisbach water/heat formulation for GEKKO IPOPT/APOPT.
Binary-free smooth signed flow, smooth temperature upwinding and an active /
passive heat-exchanger pair with their ``direction`` binaries removed.
``friction_model`` selects ``"constant"`` / ``"pwl"`` / ``"nonlinear"``.
Opt-in alternative to :data:`HEAT_NONCONVEX_MIQCQP_FORMULATION`.
"""
return NetworkFormulation(
branch_type_to_formulations={
WaterPipe: SmoothDarcyWeisbachBranchFormulation(
friction_model=friction_model,
smoothing_eps=smoothing_eps,
n_breakpoints=n_breakpoints,
),
HeatExchanger: SmoothHeatExchangerFormulation(),
PassiveHeatExchanger: SmoothPassiveHeatExchangerFormulation(
friction_model=friction_model,
smoothing_eps=smoothing_eps,
n_breakpoints=n_breakpoints,
),
},
node_type_to_formulations={(Junction, WaterGrid): WaterNodeFormulation()},
)
HEAT_NLP_FORMULATION = make_heat_nlp_formulation()
# ---------------------------------------------------------------------------
# Sector-complete bundles
# ---------------------------------------------------------------------------
[docs]
def make_smooth_nlp_formulation(friction_model: str = "constant"):
"""Combined polar-AC + smooth gas + smooth heat formulation in one apply.
Pure-NLP across all three carriers. Solve with ``run_energy_flow`` for a
fast square steady-state simulation (GEKKO IMODE=1, falls back to IMODE=3 if
not square), or pass an optimization problem for an IMODE=3 optimize. The
simulation squaring (phantom-var pinning, flow-limit drop, vm_pu_squared
demotion) is applied by the solver from its ``simulation`` flag - there is
no separate simulation formulation."""
return combine(
EL_NLP_FORMULATION,
make_gas_nlp_formulation(friction_model),
make_heat_nlp_formulation(friction_model),
)
SMOOTH_NLP_FORMULATION = make_smooth_nlp_formulation()
[docs]
def make_convex_miqcqp_formulation(num_partitions: int = 1) -> NetworkFormulation:
"""Convex MIQCQP across all three carriers: branch-flow MISOCP electricity,
epigraph-relaxed Weymouth gas and McCormick district heating (incl. the
convex heat-exchanger variants). Every constraint is certifiable by a
convex MIQCQP/MISOCP solver; solutions are relaxation optima - check the
documented gap bounds where exactness matters."""
return combine(
EL_MISOCP_FORMULATION,
GAS_CONVEX_MIQCQP_FORMULATION,
make_heat_convex_milp_formulation(num_partitions=num_partitions),
)
CONVEX_MIQCQP_FORMULATION = make_convex_miqcqp_formulation()
# Exact quadratic models across all three carriers - for global MIQCQP
# solvers (SCIP, Gurobi). No relaxation gap, no trigonometric terms.
NONCONVEX_MIQCQP_FORMULATION = combine(
EL_NONCONVEX_MIQCQP_FORMULATION,
GAS_NONCONVEX_MIQCQP_FORMULATION,
HEAT_NONCONVEX_MIQCQP_FORMULATION,
)
# The deliberate hybrid Network() applies by default: exact polar AC for
# electricity, the MISOCP-shaped relaxed Weymouth for gas and the bilinear
# Darcy-Weisbach for heat. Documented here so its mixed nature is explicit.
DEFAULT_SIMULATION_FORMULATION = combine(
EL_NLP_FORMULATION,
GAS_CONVEX_MIQCQP_FORMULATION,
HEAT_NONCONVEX_MIQCQP_FORMULATION,
)