Network aspects¶
A NetworkAspect is a solver-agnostic plug-in that adds variables and constraints to a network without modifying any existing model class. It works at the network level rather than the component level. Reach for one whenever a feature spans multiple components or needs coordination across the whole topology.
Binary connectivity variables and flow constraints that restore feasibility when the network splits into electrically isolated islands.
Thermal inertia in district heating junctions: slows temperature propagation between consecutive timesteps.
Stored gas mass in pipeline segments: enables the pipeline itself to act as a short-term storage buffer.
Subclass NetworkAspect, implement
the phases you need, and call network.add_extension().
Why not a formulation?¶
Formulations (BranchFormulation,
NodeFormulation, …) define how a single
component type behaves: the equations for a gas pipe, the variables for a bus.
A NetworkAspect is different:
It touches many components at once (for example, all water junctions).
It injects variables onto existing models that were not designed for the feature, such as adding a
linepack_kgvariable to everyGasPipethat carries linepack.It can activate or suppress behaviour conditionally. Gas linepack, for instance, pins its net mass change to zero in a steady-state solve and lets a dynamic balance govern it only when a timeseries solve is running.
The four phases¶
Every NetworkAspect participates in up to four phases of the solver pipeline:
flowchart LR
P0["Phase 0<br/>prepare"]
P1a["Phase 1a<br/>activate timeseries"]
P1b["Phase 1b<br/>equations"]
P1c["Phase 1c<br/>time coupling"]
P0 --> P1a --> P1b --> P1c
Only implement the phases you need; every phase has a no-op default.
Minimal example¶
A one-file aspect that adds a pressure-spread variable to every gas junction and bounds it against a reference junction:
import monee.model as mm
import monee.express as mx
from monee.model.extension.core import NetworkAspect
from monee.model.core import Var
class PressureSpreadAspect(NetworkAspect):
"""Adds a ``p_spread_pu`` Var bounded against a reference junction."""
def prepare(self, network) -> None:
for node in network.nodes:
if isinstance(node.model, mm.Junction):
node.model.p_spread_pu = Var(0.0, min=0.0, name="p_spread_pu")
def equations(self, network, ignored_nodes: set) -> list:
eqs = []
junctions = [n.model for n in network.nodes
if isinstance(n.model, mm.Junction)
and n.id not in ignored_nodes]
if len(junctions) >= 2:
ref = junctions[0]
for junc in junctions[1:]:
eqs.append(junc.p_spread_pu >= ref.pressure_squared_pu
- junc.pressure_squared_pu)
return eqs
# Register and use
net = mx.create_multi_energy_network()
net.add_extension(PressureSpreadAspect())
Temporal phases in detail¶
All three temporal methods share the same InterStepState interface but run
in different contexts:
Method |
Called by |
Typical use |
|---|---|---|
|
timeseries and multi-period |
Storage SoC, linepack balance, LTC: anything that should work in both |
|
timeseries only |
Logic that only makes sense step-by-step (e.g. clamp from a controller) |
|
multi-period only |
Look-ahead constraints (e.g. require non-decreasing SoC over horizon) |
The temporal_state argument implements
InterStepState:
# In a timeseries solve: temporal_state is StepState
# → .get() returns a plain float
#
# In a multi-period solve: temporal_state is PeriodState
# → .get() returns a live solver variable (GEKKO GKVariable / Pyomo Var)
#
# Both expose the same .get() and .dt_h API, so your code is identical.
def inter_temporal_equations(self, network, ignored_nodes, temporal_state):
eqs = []
dt_s = temporal_state.dt_h * 3600.0
for branch in network.branches:
if branch.id not in self._tracked:
continue
bm = branch.model
prev = temporal_state.get(branch.id, "stored_kg")
if prev is None:
prev = self._initial[branch.id]
eqs.append(bm.stored_kg == prev + dt_s * bm.net_inflow)
return eqs
Registering an aspect¶
Register aspects on the network before the first solve. They persist across all later single-step, timeseries, and multi-period solves:
import monee.model as mm
import monee.express as mx
from monee.model import LumpedThermalCapacitance, GasLinepack
net = mx.create_multi_energy_network()
# Water side
js = mx.create_water_junction(net)
jl = mx.create_water_junction(net)
mx.create_ext_hydr_grid(net, js)
mx.create_water_pipe(net, js, jl, diameter_m=0.3, length_m=500)
# Gas side
jg0 = mx.create_gas_junction(net)
jg1 = mx.create_gas_junction(net)
mx.create_gas_ext_grid(net, jg0)
pipe_id = mx.create_gas_pipe(net, jg0, jg1, diameter_m=0.3, length_m=10_000)
# Attach both aspects
net.add_extension(LumpedThermalCapacitance())
net.add_extension(GasLinepack())
print(len(net.extensions))
2
Multiple aspects compose without conflict; each operates on its own variable subset.
See also¶
Step-by-step walkthroughs for LumpedThermalCapacitance and
GasLinepack, including physics background and visualisation code.
The built-in islanding system is implemented as a NetworkAspect.
A detailed breakdown of its phases is in the islanding concept page.
How the solver pipeline calls temporal aspect methods and how
StepState bridges consecutive steps.
How PeriodState replaces StepState in a single-shot joint
solve, and why existing aspect code works unchanged.