Source code for monee.model.formulation.milp.gas
r"""Piecewise-linear Weymouth gas formulation: a MILP (PWL of :math:`\varphi(m)` plus the
``direction`` binaries; the pressure equation is linear in :math:`p^2`-space)."""
import monee.model.phys.core.hydraulics as hydraulicsmodel
import monee.model.phys.nonlinear.gf as ogfmodel
from monee.model.core import Const, Var
from ..core import BranchFormulation
def _pwl_m_max(model, grid) -> float:
"""Per-pipe mass-flow cap: grid max_mass_flow_kgs tightened by the velocity_mps bound at
reference-pressure density."""
return min(
grid.max_mass_flow_kgs,
hydraulicsmodel.calc_max_mass_flow(
model.diameter_m,
ogfmodel.reference_gas_density(grid),
getattr(grid, "v_max_mps", 20.0),
),
)
[docs]
class PwlWeymouthBranchFormulation(BranchFormulation):
r"""Variable-friction Weymouth via per-pipe PWL of :math:`\varphi(m) = friction(Re(m)) \cdot m^2`.
Opt-in alternative to the relaxed (convex MIQCQP) Weymouth for networks
where laminar-regime accuracy matters (``Re < 2300`` on lightly-loaded
pipes); the constant-friction turbulent asymptote under-estimates the
pressure drop there.
"""
def __init__(self, n_breakpoints: int = 12):
self.n_breakpoints = n_breakpoints
[docs]
def ensure_var(self, model, simulation=False, grid=None):
model.phi_pwl_pos = Var(0, min=0, name="phi_pwl_pos")
model.phi_pwl_neg = Var(0, min=0, name="phi_pwl_neg")
model.mass_flow_pos_kgs_squared = Const(0.0)
model.mass_flow_neg_kgs_squared = Const(0.0)
model.reynolds_scaled = Const(0.0)
model.friction = Const(0.0)
if hasattr(grid, "max_mass_flow_kgs"):
# Pyomo Piecewise requires bounded x; 1.001x slack avoids endpoint
# tightness. Declared on the Var abstraction so every backend gets
# the bound natively - the previous Var.setub() in equations() was
# pyomo-only and crashed the GEKKO path.
m_ub = _pwl_m_max(model, grid) * 1.001
model.mass_flow_pos_kgs.max = m_ub
model.mass_flow_neg_kgs.max = m_ub
[docs]
def equations(self, branch, grid, from_node_model, to_node_model, **kwargs):
branch._pipe_area = hydraulicsmodel.calc_pipe_area(branch.diameter_m)
# Linearise sqrt(p) around nominal pressure.
p0 = grid.nominal_pressure_pu
x0 = p0**2
p_from = p0 + (1 / (2 * p0)) * (
from_node_model.vars["pressure_squared_pu"] - x0
)
p_to = p0 + (1 / (2 * p0)) * (to_node_model.vars["pressure_squared_pu"] - x0)
p_avg = 0.5 * (p_from + p_to)
m_max = _pwl_m_max(branch, grid)
# Two \varphi(m) PWLs; 0-anchor collapses the inactive side's \varphi to 0.
# (mass_flow_pos_kgs/neg upper bounds are declared in ensure_var.)
xs, ys = hydraulicsmodel.phi_pwl_breakpoints(
branch.diameter_m,
branch.roughness_m,
grid.dynamic_visc_pas,
branch._pipe_area,
m_max,
self.n_breakpoints,
)
kwargs["pwl_impl"].piecewise_eq(
y=branch.phi_pwl_pos,
x=branch.mass_flow_pos_kgs,
xs=xs,
ys=ys,
)
kwargs["pwl_impl"].piecewise_eq(
y=branch.phi_pwl_neg,
x=branch.mass_flow_neg_kgs,
xs=xs,
ys=ys,
)
c_sq = ogfmodel.calc_C_squared(
branch.diameter_m,
branch.length_m,
grid.t_k,
grid.compressibility,
grid.universal_gas_constant / grid.molar_mass,
)
p_amb = getattr(grid, "pressure_ambient_pa", 0.0)
# ABSOLUTE squared-pressure difference (per-unit): gauge psq difference
# plus the gauge->absolute term. In gauge mode the offset uses the
# LINEARIZED pressure p_from/p_to (affine in pressure_squared_pu, the same
# linearization used for the density) so the equation stays linear; a
# no-op when p_amb = 0.
abs_psq_diff = ogfmodel.abs_psq_diff_pu(
from_node_model.vars["pressure_squared_pu"],
to_node_model.vars["pressure_squared_pu"],
p_from,
p_to,
p_amb,
grid.pressure_ref_pa,
)
return [
# direction=0 - forward flow via m_neg.
branch.mass_flow_pos_kgs <= m_max * branch.direction,
branch.mass_flow_neg_kgs <= m_max * (1 - branch.direction),
branch.mass_flow_pos_kgs <= m_max * branch.on_off,
branch.mass_flow_neg_kgs <= m_max * branch.on_off,
#
abs_psq_diff * grid.pressure_ref_pa**2 * c_sq * branch.on_off
== branch.phi_pwl_neg - branch.phi_pwl_pos,
#
branch.gas_density_kg_per_m3
== (grid.pressure_ref_pa * p_avg + p_amb)
* grid.molar_mass
/ (grid.universal_gas_constant * grid.t_k),
]