Metadata-Version: 2.4
Name: monee
Version: 2.2.0
Summary: Framework for calculating the steady-state energy flow and for solving optimization problems in coupled energy grids (gas, heat, electricity)
Author-email: Rico Schrage <rico.schrage@uol.de>
License-Expression: MIT
Project-URL: Homepage, https://monee.readthedocs.io
Project-URL: Repository, https://github.com/Digitalized-Energy-Systems/monee
Project-URL: Issues, https://github.com/Digitalized-Energy-Systems/monee/issues
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: Implementation :: CPython
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: scipy>=1.10.1
Requires-Dist: casadi>=3.6
Requires-Dist: gekko>=1.3.2
Requires-Dist: networkx>=3.0
Requires-Dist: numpy>=2.3.5
Requires-Dist: pandas>=2.3.3
Requires-Dist: geopy>=2.4.1
Requires-Dist: plotly>=6.3.0
Requires-Dist: pyscipopt>=5.0
Requires-Dist: pyomo>=6.7
Provides-Extra: plot
Requires-Dist: kaleido; extra == "plot"
Provides-Extra: simbench
Requires-Dist: simbench>=1.6.1; extra == "simbench"
Requires-Dist: pandapower>=3.3.3; extra == "simbench"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pytest-cov; extra == "test"
Requires-Dist: pre-commit; extra == "test"
Requires-Dist: gurobipy; extra == "test"
Provides-Extra: testpp
Requires-Dist: pytest; extra == "testpp"
Requires-Dist: simbench==1.6.1; extra == "testpp"
Requires-Dist: pandapower==3.3.3; extra == "testpp"
Requires-Dist: pandapipes==0.14.0; extra == "testpp"
Requires-Dist: numpy==2.3.5; extra == "testpp"
Requires-Dist: pytest-cov; extra == "testpp"
Requires-Dist: pre-commit; extra == "testpp"
Requires-Dist: pandas==2.3.3; extra == "testpp"
Requires-Dist: kaleido; extra == "testpp"
Requires-Dist: gurobipy; extra == "testpp"
Dynamic: license-file

<p align="center">
  <img src="docs/source/_static/monee-logo.drawio.svg" width="220" alt="monee"/>
</p>

<p align="center">
  <strong>Modular Network-based Energy Grid Optimization</strong><br>
  Steady-state simulation and optimal energy flow for coupled electricity, gas, and heat networks
</p>

<p align="center">
  <a href="https://pypi.org/project/monee/"><img src="https://img.shields.io/pypi/v/monee?color=blue" alt="PyPI"/></a>
  <a href="https://monee.readthedocs.io"><img src="https://img.shields.io/badge/docs-readthedocs-blue" alt="Docs"/></a>
  <img src="https://img.shields.io/badge/lifecycle-maturing-blue.svg" alt="lifecycle"/>
  <a href="https://github.com/Digitalized-Energy-Systems/monee/blob/development/LICENSE"><img src="https://img.shields.io/badge/license-MIT-green.svg" alt="MIT License"/></a>
  <a href="https://github.com/Digitalized-Energy-Systems/monee/actions/workflows/test-monee.yml"><img src="https://github.com/Digitalized-Energy-Systems/monee/actions/workflows/test-monee.yml/badge.svg" alt="Tests"/></a>
  <a href="https://codecov.io/gh/Digitalized-Energy-Systems/monee"><img src="https://codecov.io/gh/Digitalized-Energy-Systems/monee/graph/badge.svg?token=KSBSBQGNBZ" alt="Coverage"/></a>
  <a href="https://sonarcloud.io/summary/new_code?id=Digitalized-Energy-Systems_monee"><img src="https://sonarcloud.io/api/project_badges/measure?project=Digitalized-Energy-Systems_monee&metric=alert_status" alt="Quality Gate"/></a>
</p>

---

**monee** is a Python framework for steady-state simulation and optimal energy flow of **multi-energy systems (MES)**. It models electricity, gas, and district-heat networks in a single unified directed graph, couples them via standard conversion units (CHP, P2H, P2G, G2P, G2H), and hands the coupled system to third-party solvers through four back-ends (for CasADi, GEKKO, Pyomo, and gurobipy). The framework is designed for research workflows that require flexible problem formulation, reproducible benchmarks, and straightforward integration with external optimisers.

## Capabilities

**Physical modelling**
- AC power flow (full nonlinear or MISOCP relaxation)
- Weymouth gas flow (*p*² formulation, Swamee–Jain friction)
- Darcy–Weisbach hydraulic flow with piecewise-linear friction factor and temperature propagation
- Coupling components: CHP, P2H (heat pump / electric boiler), P2G, G2P, G2H

**Optimisation**
- Optimal energy flow via CasADi or GEKKO (IPOPT) or Pyomo (SCIP, GLPK, CBC, Gurobi)
- MISOCP relaxation for convex AC OPF
- Built-in minimum-curtailment (load shedding) formulation with per-carrier bounds
- Capacity limits on all external grid connections (`max_import` / `max_export`)
- Islanding-aware solves with grid-forming units per carrier (`monee.enable_islanding`)
- Structured infeasibility diagnostics on failed solves, for both back-ends
- Custom objectives and constraints through a composable problem API

**Simulation**
- Sequential multi-step timeseries simulation with time-varying profiles
- Ramp constraints and inter-step state tracking
- Per-step hooks for custom logic

**Interoperability**
- Import from MATPOWER, pandapower, and SimBench
- NetworkX as the internal graph structure
- Typed result DataFrames, one row per component

---

## Installation

Requires **Python 3.11+**.

```bash
pip install monee
```

The core install pulls in, as dependencies, the third-party packages needed for
both simulation and the default optimisation workflows:

- **CasADi**: runs the IPOPT solver in-process; monee uses it by default.
- **GEKKO**: bundles its own APOPT/BPOPT/IPOPT binaries; used as the IPOPT fallback if CasADi is unavailable.
- **Pyomo** and **pyscipopt**: hand MILP/MIQCP problems such as MISOCP optimal power flow to the SCIP solver, or to any other solver Pyomo supports.

Optional solvers and import formats are pulled in only when you need them:

```bash
pip install monee[simbench]   # SimBench / pandapower grid import
pip install gurobipy          # Gurobi - commercial, free academic licence available
```

---

## Quick start

Build a coupled electricity and district-heat network and run an energy-flow calculation:

```python
import monee.model as mm
from monee import mx, run_energy_flow

net = mx.create_multi_energy_network()

# Electricity grid
bus_0 = mx.create_bus(net)
bus_1 = mx.create_bus(net)
mx.create_line(net, bus_0, bus_1, length_m=100, r_ohm_per_m=7e-5, x_ohm_per_m=7e-5)
mx.create_ext_power_grid(net, bus_0)
mx.create_power_load(net, bus_1, p_mw=0.1, q_mvar=0.0)

# District heating loop
j_supply = mx.create_water_junction(net)
j_mid = mx.create_water_junction(net)
j_return = mx.create_water_junction(net)
mx.create_ext_hydr_grid(net, j_supply)
mx.create_water_pipe(net, j_supply, j_mid, diameter_m=0.12, length_m=100)
mx.create_sink(net, j_return, mass_flow_kgs=1.0)

# Coupling: bus_1 drives a heat pump that feeds the heating loop
mx.create_p2h(
    net, bus_1, j_mid, j_return, heat_energy_mw=0.1, diameter_m=0.1, efficiency=0.9
)

result = run_energy_flow(net)
print(result.get(mm.Bus)[["vm_pu", "va_degree"]])
print(result.get(mm.WaterPipe)[["mass_flow_kgs"]])
```

Solvers are selected by name - `ipopt` routes to CasADi when installed (otherwise GEKKO), `apopt` and `bpopt` route to GEKKO, and any other name (e.g. `scip`, `gurobi`) is forwarded to the matching Pyomo solver. Override the back-end explicitly with `backend=...` if needed. By default `run_energy_flow` runs in **simulation mode** (`simulation=True`), squaring the model for a fast steady-state solve; pass `simulation=False` (or an optimisation problem) to solve in optimisation mode. Check `result.mode_used` to see which path actually ran.

---

## Optimal energy flow

Swap `run_energy_flow` for `run_energy_flow_optimization` and pass a problem.
monee ships ready-made formulations - economic dispatch and minimum load
shedding - and you can also build your own (see *Extensibility* below).

Economic dispatch minimises the total generation cost `Σ cost · p_gen` while
respecting the network's physical limits. Give each generator a marginal
`cost`, and the optimiser dispatches the cheaper units first:

```python
import monee
import monee.model as mm
from monee import mx

net = mx.create_multi_energy_network()
bus_ext = mx.create_bus(net)
bus_a = mx.create_bus(net)
bus_b = mx.create_bus(net)
mx.create_line(net, bus_ext, bus_a, length_m=100, r_ohm_per_m=7e-5, x_ohm_per_m=7e-5)
mx.create_line(net, bus_ext, bus_b, length_m=100, r_ohm_per_m=7e-5, x_ohm_per_m=7e-5)
mx.create_ext_power_grid(net, bus_ext)
mx.create_power_load(net, bus_a, p_mw=0.1, q_mvar=0.0)

# Two dispatchable generators with different marginal costs (currency/MW).
mx.create_power_generator(net, bus_a, p_mw=0.15, q_mvar=0.0, cost=10.0)  # cheap
mx.create_power_generator(net, bus_b, p_mw=0.15, q_mvar=0.0, cost=40.0)  # expensive

problem = monee.create_economic_dispatch_problem(
    ext_grid_cost_default=0.0,  # include the slack so it can be bounded
    ext_grid_bounds=(-0.01, 0.01),  # substation import/export limit (MW)
)

result = monee.run_energy_flow_optimization(net, problem, solver="ipopt")

# The cheap generator carries the load; the expensive one stays near zero.
# (generation uses the load convention, so dispatched power is negative)
print(result.get(mm.PowerGenerator)[["p_mw"]])
#            p_mw
# 0 -9.000072e-02
# 1  6.969836e-09
```

The same problem dispatches gas and heat as well: pass `gas_cost_default` and
`heat_cost_default` (prices per MW of gas energy and per MW of heat) and give
gas sources, heat generators and external grids their own `cost`; coupling
units such as CHPs then follow the merit order too. See the
[economic dispatch](https://monee.readthedocs.io/en/latest/problems/economic_dispatch.html)
page.

For resilience studies, `create_min_load_shedding_problem` instead minimises
curtailment with per-carrier operational bounds, returning a `regulation` per
demand (`1.0` fully served, `0.0` completely shed). Both problems accept the
same solver and formulation options.

---

## Formulations

monee separates the **physical equations** from the **network topology** through a `NetworkFormulation` layer. A formulation maps component types to a set of equations - most cover a **single energy domain**. Calling `apply_formulation()` overwrites the equations for only the component types included in that formulation, leaving all other domains untouched.

Formulations are named by **optimization class** (`NLP`, `MILP`, convex/non-convex `MIQCQP`) and are a property of the **solve**, not the network: pass `formulation=` to any solve call (a registry key string, a `NetworkFormulation`, or a mix), or record a network-level choice with `apply_formulation()`. Components without a choice fall back to `DEFAULT_SIMULATION_FORMULATION` - a deliberate per-domain hybrid:

| Formulation constant | Domain | Equations | Applied |
|---|---|---|---|
| `EL_NLP_FORMULATION` | Electricity | Nonlinear polar AC power flow (voltage magnitude + angle) | default |
| `GAS_CONVEX_MIQCQP_FORMULATION` | Gas | Weymouth equation (*p*² formulation, convex epigraph relaxation) | default |
| `HEAT_NONCONVEX_MIQCQP_FORMULATION` | Water / Heat | Darcy–Weisbach + bilinear temperature propagation | default |
| `EL_MISOCP_FORMULATION` | Electricity | MISOCP relaxation (lifted voltages, SOC constraints) | opt-in |
| `HEAT_CONVEX_MILP_FORMULATION` | Water / Heat | McCormick district-heating relaxation (LP/MILP) | opt-in |
| `SMOOTH_NLP_FORMULATION` | All three | Binary-free smooth NLP (AC + smooth Weymouth + smooth Darcy–Weisbach) | opt-in bundle |
| `CONVEX_MIQCQP_FORMULATION` | All three | Certifiable convex relaxations (MISOCP + relaxed Weymouth + McCormick heat) | opt-in bundle |
| `NONCONVEX_MIQCQP_FORMULATION` | All three | Exact quadratic models for global solvers (exact branch flow + exact Weymouth + bilinear heat) | opt-in bundle |

The smooth bundle replaces all direction binaries with smooth approximations, making the full multi-energy system tractable for pure NLP solvers such as IPOPT; import it (or its factory `make_smooth_nlp_formulation()`) from `monee.model.formulation`.

To use the convex MISOCP relaxation for electricity optimal power flow, select it at solve time - gas and heat equations remain on their defaults:

```python
import monee

result = monee.run_energy_flow(
    net, solver="gurobi", formulation="el_misocp", simulation=False
)

# or record it as the network-level choice for all subsequent solves
net.apply_formulation(monee.EL_MISOCP_FORMULATION)
```

Custom formulations follow the same pattern - subclass the appropriate base class and register it for the target component types:

```python
from monee.model.core import Const, Var
from monee.model.formulation.core import BranchFormulation, NetworkFormulation
from monee.model.branch import GasPipe


class MyGasPipeFormulation(BranchFormulation):
    """Toy linear pipe: pressure drop proportional to the mass flow."""

    def ensure_var(self, branch, simulation=False, grid=None):
        # One signed mass-flow decision variable plus a geometric resistance the
        # equation references; neutralise the Vars this linear model does not use.
        branch.mass_flow_kgs = Var(0.1, name="mass_flow_kgs")
        branch.resistance = Const(1e-7 * branch.length_m / branch.diameter_m**2)
        branch.direction = Const(1)
        for unused in (
            "velocity_mps",
            "reynolds_scaled",
            "gas_density_kg_per_m3",
            "friction",
            "mass_flow_pos_kgs_squared",
            "mass_flow_neg_kgs_squared",
        ):
            setattr(branch, unused, Const(0.0))

    def equations(self, branch, grid, from_node_model, to_node_model, **kwargs):
        m = branch.mass_flow_kgs
        mag = kwargs["sqrt_impl"](m * m)  # |m|, so it works in either flow direction
        return [
            # from->to flow rides mass_flow_neg_kgs, i.e. m < 0, so the drop
            # needs the minus sign to fall along the flow.
            from_node_model.pressure_pu - to_node_model.pressure_pu
            == -branch.resistance * m,
            # The nodal balance consumes the positive / negative flow split, and
            # it fixes the reported sign: mass_flow_kgs = pos - neg is negative
            # when the fluid runs from the from-node to the to-node.
            branch.mass_flow_pos_kgs == 0.5 * (mag + m),
            branch.mass_flow_neg_kgs == 0.5 * (mag - m),
        ]


net.apply_formulation(
    NetworkFormulation(
        branch_type_to_formulations={GasPipe: MyGasPipeFormulation()},
    )
)
```

---

## Extensibility

Every layer of the framework is designed to be subclassed or replaced without modifying the library internals.

**Custom components** - subclass `NodeModel`, `BranchModel`, `ChildModel`, or `CompoundModel` and register the model with `NetworkFormulation`. The solver infrastructure (variable injection, result extraction, timeseries state tracking) handles the new type automatically.

**Custom objectives and constraints** - the optimisation problem is assembled with a composable builder API. `Objectives` and `Constraints` objects are attached to an `OptimizationProblem`; the solver evaluates them without any solver-specific glue:

```python
import monee.model as mm
from monee import run_energy_flow_optimization
from monee.problem import (
    AttributeParameter,
    Constraints,
    Objectives,
    OptimizationProblem,
)

problem = OptimizationProblem()

# Make regulation ∈ [0, 1] a solver decision variable for each demand
problem.controllable_demands(
    [
        (
            "regulation",
            AttributeParameter(
                min=lambda attr, val: 0,
                max=lambda attr, val: 1,
                val=lambda attr, val: 1,
            ),
        )
    ]
)

# Objective: minimise curtailment cost weighted by load priority
objectives = Objectives()
objectives.select(
    lambda model: isinstance(model, mm.PowerLoad) and hasattr(model, "_priority")
).data(lambda model: (1 - model.regulation) * model.p_mw * model._priority).calculate(
    lambda model_to_data: sum(model_to_data.values())
)
problem.objectives = objectives

# Constraint: cap the substation import at 0.6 MW.
# ExtPowerGrid.p_mw follows the load convention: import is negative, export
# positive, so an import cap is a *lower* bound.
constraints = Constraints()
constraints.select_types(mm.ExtPowerGrid).equation(lambda model: model.p_mw >= -0.6)
problem.constraints = constraints

result = run_energy_flow_optimization(net, problem)
```

**Network-level extensions** - implement `NetworkAspect` (with `prepare` and `equations` methods) and attach it via `network.add_extension(aspect)`. The extension participates in both variable injection and equation registration without any solver-specific glue code.

---

## Timeseries simulation

```python
import monee.model as mm
from monee import run_timeseries, TimeseriesData
import numpy as np

td = TimeseriesData()
td.add_child_series(load_id, "p_mw", np.linspace(0.5, 1.5, 96))

ts_result = run_timeseries(net, td)
# DataFrame indexed by timestep, columns by component ID
print(ts_result.get_result_for(mm.Bus, "vm_pu"))
```

---

## Documentation

Full documentation with tutorials, concept explanations, and API reference:
**[monee.readthedocs.io](https://monee.readthedocs.io)**

---

## License

MIT © [Digitalized Energy Systems](https://github.com/Digitalized-Energy-Systems)
