Source code for monee.model.grid

from dataclasses import dataclass

from .core import model
from .phys.core.properties import (
    gas_compressibility,
    water_density_kg_per_m3,
    water_dynamic_viscosity_pas,
)

# Gas/heat energy-rate conversion: power[MW] = mass_flow[kg/s] \cdot HHV[kWh/kg] \cdot 3.6,
# since 1 kWh/s = 3600 kW = 3.6 MW (3600 s/h \div 1000 kW/MW). Used wherever a gas
# mass flow is turned into a power, so the bare 3.6 never appears inline.
KGPS_KWHPERKG_TO_MW = 3.6

# Standard atmosphere [Pa] (sea level). Set a gas grid's ``pressure_ambient_pa``
# to this to make its node pressures GAUGE (absolute = gauge + 1 atm), matching
# pandapipes and the conventional way operating/MAOP pressures are stated.
STANDARD_ATMOSPHERE_PA = 101325.0


[docs] @model @dataclass(unsafe_hash=True) class Grid: """Marker/base for a resource domain (electrical, gas, heat).""" name: str
[docs] @model @dataclass(unsafe_hash=True) class PowerGrid(Grid): """Electrical grid domain.""" sn_mva: float = 1 vm_pu_max: float = 1.5 # Lower bound applied to bus ``vm_pu`` at node creation. 0 is unphysical and # lets an NLP solver (IPOPT) drive the 1/vm current-equation Jacobian to # infinity during restoration; a small positive floor keeps it bounded. 0.5 # is low enough to preserve realistic stressed / load-shedding operating # points (e.g. ~0.68 pu) yet still bounds the Jacobian. vm_pu_min: float = 0.5
[docs] @model @dataclass(unsafe_hash=True) class WaterGrid(Grid): """Water/heat grid domain. ``fluid_density_kg_per_m3`` and ``dynamic_visc_pas`` default to ``None`` and are then derived from ``t_ref_k`` via the standard water correlations (see :mod:`monee.model.phys.core.properties`), so the transport properties always match the grid's declared operating temperature. Pass explicit values to override (e.g. to model a non-water fluid or a different reference).""" t_ref_k: float = 356 pressure_ref_pa: float = 1000000 max_mass_flow_kgs: float = 200 # v_max_mps caps per-pipe mass-flow via \pi/4 \cdot D^2 \cdot \rho \cdot v_max; combined with max_mass_flow_kgs # by min(...), so it can only tighten. 5 m/s is generous for DH water. v_max_mps: float = 5.0 fluid_density_kg_per_m3: float | None = None dynamic_visc_pas: float | None = None def __post_init__(self): if self.fluid_density_kg_per_m3 is None: self.fluid_density_kg_per_m3 = water_density_kg_per_m3(self.t_ref_k) if self.dynamic_visc_pas is None: self.dynamic_visc_pas = water_dynamic_viscosity_pas(self.t_ref_k)
# Conditions and bounds shared by every lgas variant; only the gas-identity # constants (molar mass, viscosity, heating value) differ between them. # ``compressibility`` is intentionally absent: it is derived from the reference # pressure/temperature/molar mass in ``GasGrid.__post_init__``. _LGAS_COMMON = { "universal_gas_constant": 8.314, "t_k": 300, "t_ref_k": 356, "pressure_ref_pa": 1000000, "nominal_pressure_pu": 1, "max_mass_flow_kgs": 20, "pressure_squared_pu_max": 1.3, "pressure_squared_pu_min": 0.7, } GAS_GRID_ATTRS = { # Realistic L-gas (N2/CO2-diluted natural gas): heavier and lower-calorific # than pure methane. Values match pandapipes' 'lgas' fluid library. "lgas": { **_LGAS_COMMON, "molar_mass": 0.0181138902, "dynamic_visc_pas": 1.2086239755175486e-05, "higher_heating_value_kwh_per_kg": 11.79011, }, # Methane / H-gas (pure natural gas): lighter and higher-calorific than # L-gas. The molar mass / HHV match monee's pre-2026 defaults (M=0.0165 # kg/mol, HHV=15.3 kWh/kg ~ methane). NOTE: compressibility is now DERIVED # (real-gas Papay Z~=0.978 at 10 bar), not the old hardcoded ideal-gas Z=1, # so this does NOT reproduce pre-2026 gas pressure drops byte-for-byte (the # Weymouth C^2 ~ 1/Z shifts ~2.3%). For exact historical (ideal-gas) numbers # construct GasGrid(..., compressibility=1) directly. "methane": { **_LGAS_COMMON, "molar_mass": 0.0165, "dynamic_visc_pas": 1.2190162697374919e-05, "higher_heating_value_kwh_per_kg": 15.3, }, } # Single source of truth for the default gas heating value (the lgas grid's HHV) # and its MJ/kg form. Network-sizing heuristics and HHV fallbacks reference these # instead of re-hardcoding the constant, so they always match the gas physics # (which reads ``grid.higher_heating_value_kwh_per_kg``). 1 kWh = 3.6 MJ. DEFAULT_GAS_HHV_KWH_PER_KG = GAS_GRID_ATTRS["lgas"]["higher_heating_value_kwh_per_kg"] DEFAULT_GAS_HHV_MJ_PER_KG = DEFAULT_GAS_HHV_KWH_PER_KG * 3.6
[docs] @model @dataclass(unsafe_hash=True) class GasGrid(Grid): """Gas grid domain. Construct via :func:`create_gas_grid` for defaults. ``compressibility`` defaults to ``None`` and is then derived from the reference pressure (``pressure_ref_pa``), temperature (``t_k``) and ``molar_mass`` via the Papay real-gas correlation (see :mod:`monee.model.phys.core.properties`), so Z reflects the grid's declared operating pressure rather than a fixed ideal-gas 1. Pass an explicit value to override (e.g. to force ideal gas with ``compressibility=1``).""" molar_mass: float dynamic_visc_pas: float higher_heating_value_kwh_per_kg: float universal_gas_constant: float t_k: float t_ref_k: float pressure_ref_pa: float nominal_pressure_pu: float max_mass_flow_kgs: float pressure_squared_pu_max: float pressure_squared_pu_min: float compressibility: float | None = None # Ambient pressure [Pa] added to node pressure to form the ABSOLUTE pressure # the Weymouth/density physics require. 0.0 (default) => node pressures are # absolute (monee's historical convention); STANDARD_ATMOSPHERE_PA => node # pressures are gauge (pandapipes/standard-engineering convention). pressure_ambient_pa: float = 0.0 def __post_init__(self): if self.compressibility is None: self.compressibility = gas_compressibility( self.pressure_ref_pa, self.t_k, self.molar_mass )
[docs] @model @dataclass(unsafe_hash=True) class NoGrid(Grid): """Marker for components not bound to any grid."""
NO_GRID = NoGrid("None")
[docs] def create_gas_grid(name, type="lgas", t_ref_k=None, pressure_ref_pa=None): """Return a :class:`GasGrid` populated from ``GAS_GRID_ATTRS[type]``. ``t_ref_k`` / ``pressure_ref_pa`` override the reference condition so the derived compressibility tracks the grid's actual operating pressure. Pass them here rather than mutating the grid afterwards, so ``__post_init__`` derives Z from the final values (a post-construction assignment leaves the already-derived Z stale).""" attrs = dict(GAS_GRID_ATTRS[type]) if t_ref_k is not None: attrs["t_ref_k"] = t_ref_k if pressure_ref_pa is not None: attrs["pressure_ref_pa"] = pressure_ref_pa return GasGrid(name, **attrs)
[docs] def create_water_grid( name, t_ref_k=WaterGrid.t_ref_k, pressure_ref_pa=WaterGrid.pressure_ref_pa ): """Return a :class:`WaterGrid` whose density/viscosity are derived from ``t_ref_k`` (see :class:`WaterGrid`). Pass ``t_ref_k`` here rather than mutating the grid afterwards, so the derived properties match the final reference temperature.""" return WaterGrid(name, t_ref_k=t_ref_k, pressure_ref_pa=pressure_ref_pa)
[docs] def create_power_grid(name, sn_mva=1): return PowerGrid(name, sn_mva=sn_mva)