monee 2.0.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {monee-2.0.0/src/monee.egg-info → monee-2.2.0}/PKG-INFO +76 -41
- {monee-2.0.0 → monee-2.2.0}/README.md +75 -40
- {monee-2.0.0 → monee-2.2.0}/pyproject.toml +1 -1
- {monee-2.0.0 → monee-2.2.0}/src/monee/__init__.py +62 -14
- monee-2.2.0/src/monee/express/__init__.py +1473 -0
- monee-2.2.0/src/monee/express/structures.py +879 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/io/__init__.py +5 -4
- {monee-2.0.0 → monee-2.2.0}/src/monee/io/from_cim.py +1 -1
- monee-2.2.0/src/monee/io/from_pandapower.py +543 -0
- monee-2.2.0/src/monee/io/from_simbench.py +233 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/io/matpower.py +193 -137
- {monee-2.0.0 → monee-2.2.0}/src/monee/io/native.py +184 -43
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/__init__.py +1 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/branch.py +36 -6
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/child.py +118 -9
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/core.py +287 -4
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/core.py +1 -1
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/islanding/core.py +172 -46
- monee-2.2.0/src/monee/model/extension/islanding/el.py +311 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/islanding/gas.py +23 -3
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/ltc.py +106 -73
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/__init__.py +1 -1
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/bundles.py +19 -1
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/common.py +29 -1
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/core.py +14 -4
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/milp/gas.py +1 -1
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/milp/heat.py +222 -127
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/convex/el.py +102 -14
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/convex/gas.py +21 -1
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/nonconvex/el.py +3 -3
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/nonconvex/heat.py +5 -1
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/nlp/gas.py +7 -2
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/nlp/heat.py +6 -1
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/registry.py +106 -1
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/grid.py +13 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/multi.py +187 -24
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/network.py +414 -41
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/node.py +10 -2
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/misoc/pf.py +5 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/nonlinear/ac.py +2 -2
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/nonlinear/gf.py +5 -4
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/nonlinear/smooth.py +21 -2
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/storage.py +8 -4
- {monee-2.0.0 → monee-2.2.0}/src/monee/network/mes.py +224 -47
- {monee-2.0.0 → monee-2.2.0}/src/monee/network/res.py +9 -9
- {monee-2.0.0 → monee-2.2.0}/src/monee/problem/__init__.py +7 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/problem/core.py +426 -93
- monee-2.2.0/src/monee/problem/economic_dispatch.py +638 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/problem/metric.py +33 -1
- {monee-2.0.0 → monee-2.2.0}/src/monee/problem/min_load_shedding.py +355 -53
- monee-2.2.0/src/monee/problem/utils.py +314 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/simulation/core.py +14 -8
- {monee-2.0.0 → monee-2.2.0}/src/monee/simulation/multi_period.py +298 -140
- monee-2.2.0/src/monee/simulation/result_utils.py +219 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/simulation/stepper.py +195 -36
- {monee-2.0.0 → monee-2.2.0}/src/monee/simulation/timeseries.py +388 -30
- {monee-2.0.0 → monee-2.2.0}/src/monee/solver/__init__.py +12 -1
- {monee-2.0.0 → monee-2.2.0}/src/monee/solver/casadi.py +804 -46
- {monee-2.0.0 → monee-2.2.0}/src/monee/solver/core.py +1243 -121
- {monee-2.0.0 → monee-2.2.0}/src/monee/solver/dispatch.py +203 -2
- {monee-2.0.0 → monee-2.2.0}/src/monee/solver/gekko.py +183 -26
- {monee-2.0.0 → monee-2.2.0}/src/monee/solver/gurobipy.py +73 -44
- {monee-2.0.0 → monee-2.2.0}/src/monee/solver/infeasibility/__init__.py +2 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/solver/infeasibility/apm.py +4 -3
- monee-2.2.0/src/monee/solver/infeasibility/pyo.py +885 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/solver/pyo.py +95 -19
- {monee-2.0.0 → monee-2.2.0}/src/monee/visualization/result_visualization.py +74 -31
- {monee-2.0.0 → monee-2.2.0}/src/monee/visualization/visualization.py +7 -7
- {monee-2.0.0 → monee-2.2.0/src/monee.egg-info}/PKG-INFO +76 -41
- monee-2.0.0/src/monee/express/__init__.py +0 -1025
- monee-2.0.0/src/monee/express/structures.py +0 -483
- monee-2.0.0/src/monee/io/from_pandapower.py +0 -278
- monee-2.0.0/src/monee/io/from_simbench.py +0 -110
- monee-2.0.0/src/monee/model/extension/islanding/el.py +0 -82
- monee-2.0.0/src/monee/problem/economic_dispatch.py +0 -119
- monee-2.0.0/src/monee/problem/utils.py +0 -151
- monee-2.0.0/src/monee/simulation/result_utils.py +0 -137
- monee-2.0.0/src/monee/solver/infeasibility/pyo.py +0 -374
- {monee-2.0.0 → monee-2.2.0}/LICENSE +0 -0
- {monee-2.0.0 → monee-2.2.0}/setup.cfg +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/io/_utils.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/io/from_esdl.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/islanding/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/islanding/water.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/linepack.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/milp/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/convex/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/nonconvex/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/nonconvex/gas.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/nlp/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/nlp/el.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/core/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/core/hydraulics.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/core/properties.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/islanding.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/linear/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/misoc/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/nonlinear/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/nonlinear/hf.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/nonlinear/wf.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/network/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/network/bench/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/network/bench/restoration.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/simulation/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/simulation/step_state.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee/visualization/__init__.py +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee.egg-info/SOURCES.txt +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee.egg-info/dependency_links.txt +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee.egg-info/requires.txt +0 -0
- {monee-2.0.0 → monee-2.2.0}/src/monee.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: monee
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Framework for calculating the steady-state energy flow and for solving optimization problems in coupled energy grids (gas, heat, electricity)
|
|
5
5
|
Author-email: Rico Schrage <rico.schrage@uol.de>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -70,7 +70,7 @@ Dynamic: license-file
|
|
|
70
70
|
|
|
71
71
|
---
|
|
72
72
|
|
|
73
|
-
**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
|
|
73
|
+
**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.
|
|
74
74
|
|
|
75
75
|
## Capabilities
|
|
76
76
|
|
|
@@ -109,12 +109,12 @@ Requires **Python 3.11+**.
|
|
|
109
109
|
pip install monee
|
|
110
110
|
```
|
|
111
111
|
|
|
112
|
-
The core install
|
|
113
|
-
optimisation workflows:
|
|
112
|
+
The core install pulls in, as dependencies, the third-party packages needed for
|
|
113
|
+
both simulation and the default optimisation workflows:
|
|
114
114
|
|
|
115
|
-
- **CasADi**: in-process
|
|
115
|
+
- **CasADi**: runs the IPOPT solver in-process; monee uses it by default.
|
|
116
116
|
- **GEKKO**: bundles its own APOPT/BPOPT/IPOPT binaries; used as the IPOPT fallback if CasADi is unavailable.
|
|
117
|
-
- **Pyomo**:
|
|
117
|
+
- **Pyomo** and **pyscipopt**: hand MILP/MIQCP problems such as MISOCP optimal power flow to the SCIP solver, or to any other solver Pyomo supports.
|
|
118
118
|
|
|
119
119
|
Optional solvers and import formats are pulled in only when you need them:
|
|
120
120
|
|
|
@@ -138,22 +138,22 @@ net = mx.create_multi_energy_network()
|
|
|
138
138
|
# Electricity grid
|
|
139
139
|
bus_0 = mx.create_bus(net)
|
|
140
140
|
bus_1 = mx.create_bus(net)
|
|
141
|
-
mx.create_line(net, bus_0, bus_1, length_m=100,
|
|
142
|
-
r_ohm_per_m=7e-5, x_ohm_per_m=7e-5)
|
|
141
|
+
mx.create_line(net, bus_0, bus_1, length_m=100, r_ohm_per_m=7e-5, x_ohm_per_m=7e-5)
|
|
143
142
|
mx.create_ext_power_grid(net, bus_0)
|
|
144
143
|
mx.create_power_load(net, bus_1, p_mw=0.1, q_mvar=0.0)
|
|
145
144
|
|
|
146
145
|
# District heating loop
|
|
147
146
|
j_supply = mx.create_water_junction(net)
|
|
148
|
-
j_mid
|
|
147
|
+
j_mid = mx.create_water_junction(net)
|
|
149
148
|
j_return = mx.create_water_junction(net)
|
|
150
149
|
mx.create_ext_hydr_grid(net, j_supply)
|
|
151
150
|
mx.create_water_pipe(net, j_supply, j_mid, diameter_m=0.12, length_m=100)
|
|
152
151
|
mx.create_sink(net, j_return, mass_flow_kgs=1.0)
|
|
153
152
|
|
|
154
153
|
# Coupling: bus_1 drives a heat pump that feeds the heating loop
|
|
155
|
-
mx.create_p2h(
|
|
156
|
-
|
|
154
|
+
mx.create_p2h(
|
|
155
|
+
net, bus_1, j_mid, j_return, heat_energy_mw=0.1, diameter_m=0.1, efficiency=0.9
|
|
156
|
+
)
|
|
157
157
|
|
|
158
158
|
result = run_energy_flow(net)
|
|
159
159
|
print(result.get(mm.Bus)[["vm_pu", "va_degree"]])
|
|
@@ -181,20 +181,20 @@ from monee import mx
|
|
|
181
181
|
|
|
182
182
|
net = mx.create_multi_energy_network()
|
|
183
183
|
bus_ext = mx.create_bus(net)
|
|
184
|
-
bus_a
|
|
185
|
-
bus_b
|
|
184
|
+
bus_a = mx.create_bus(net)
|
|
185
|
+
bus_b = mx.create_bus(net)
|
|
186
186
|
mx.create_line(net, bus_ext, bus_a, length_m=100, r_ohm_per_m=7e-5, x_ohm_per_m=7e-5)
|
|
187
187
|
mx.create_line(net, bus_ext, bus_b, length_m=100, r_ohm_per_m=7e-5, x_ohm_per_m=7e-5)
|
|
188
188
|
mx.create_ext_power_grid(net, bus_ext)
|
|
189
189
|
mx.create_power_load(net, bus_a, p_mw=0.1, q_mvar=0.0)
|
|
190
190
|
|
|
191
|
-
# Two dispatchable generators with different marginal costs (currency/MW)
|
|
192
|
-
mx.create_power_generator(net, bus_a, p_mw=0.15, q_mvar=0.0, cost=10.0)
|
|
193
|
-
mx.create_power_generator(net, bus_b, p_mw=0.15, q_mvar=0.0, cost=40.0)
|
|
191
|
+
# Two dispatchable generators with different marginal costs (currency/MW).
|
|
192
|
+
mx.create_power_generator(net, bus_a, p_mw=0.15, q_mvar=0.0, cost=10.0) # cheap
|
|
193
|
+
mx.create_power_generator(net, bus_b, p_mw=0.15, q_mvar=0.0, cost=40.0) # expensive
|
|
194
194
|
|
|
195
195
|
problem = monee.create_economic_dispatch_problem(
|
|
196
|
-
ext_grid_cost_default=0.0,
|
|
197
|
-
ext_grid_bounds=(-0.01, 0.01),
|
|
196
|
+
ext_grid_cost_default=0.0, # include the slack so it can be bounded
|
|
197
|
+
ext_grid_bounds=(-0.01, 0.01), # substation import/export limit (MW)
|
|
198
198
|
)
|
|
199
199
|
|
|
200
200
|
result = monee.run_energy_flow_optimization(net, problem, solver="ipopt")
|
|
@@ -202,8 +202,18 @@ result = monee.run_energy_flow_optimization(net, problem, solver="ipopt")
|
|
|
202
202
|
# The cheap generator carries the load; the expensive one stays near zero.
|
|
203
203
|
# (generation uses the load convention, so dispatched power is negative)
|
|
204
204
|
print(result.get(mm.PowerGenerator)[["p_mw"]])
|
|
205
|
+
# p_mw
|
|
206
|
+
# 0 -9.000072e-02
|
|
207
|
+
# 1 6.969836e-09
|
|
205
208
|
```
|
|
206
209
|
|
|
210
|
+
The same problem dispatches gas and heat as well: pass `gas_cost_default` and
|
|
211
|
+
`heat_cost_default` (prices per MW of gas energy and per MW of heat) and give
|
|
212
|
+
gas sources, heat generators and external grids their own `cost`; coupling
|
|
213
|
+
units such as CHPs then follow the merit order too. See the
|
|
214
|
+
[economic dispatch](https://monee.readthedocs.io/en/latest/problems/economic_dispatch.html)
|
|
215
|
+
page.
|
|
216
|
+
|
|
207
217
|
For resilience studies, `create_min_load_shedding_problem` instead minimises
|
|
208
218
|
curtailment with per-carrier operational bounds, returning a `regulation` per
|
|
209
219
|
demand (`1.0` fully served, `0.0` completely shed). Both problems accept the
|
|
@@ -235,7 +245,9 @@ To use the convex MISOCP relaxation for electricity optimal power flow, select i
|
|
|
235
245
|
```python
|
|
236
246
|
import monee
|
|
237
247
|
|
|
238
|
-
result = monee.run_energy_flow(
|
|
248
|
+
result = monee.run_energy_flow(
|
|
249
|
+
net, solver="gurobi", formulation="el_misocp", simulation=False
|
|
250
|
+
)
|
|
239
251
|
|
|
240
252
|
# or record it as the network-level choice for all subsequent solves
|
|
241
253
|
net.apply_formulation(monee.EL_MISOCP_FORMULATION)
|
|
@@ -248,6 +260,7 @@ from monee.model.core import Const, Var
|
|
|
248
260
|
from monee.model.formulation.core import BranchFormulation, NetworkFormulation
|
|
249
261
|
from monee.model.branch import GasPipe
|
|
250
262
|
|
|
263
|
+
|
|
251
264
|
class MyGasPipeFormulation(BranchFormulation):
|
|
252
265
|
"""Toy linear pipe: pressure drop proportional to the mass flow."""
|
|
253
266
|
|
|
@@ -255,27 +268,39 @@ class MyGasPipeFormulation(BranchFormulation):
|
|
|
255
268
|
# One signed mass-flow decision variable plus a geometric resistance the
|
|
256
269
|
# equation references; neutralise the Vars this linear model does not use.
|
|
257
270
|
branch.mass_flow_kgs = Var(0.1, name="mass_flow_kgs")
|
|
258
|
-
branch.resistance = Const(1e-
|
|
271
|
+
branch.resistance = Const(1e-7 * branch.length_m / branch.diameter_m**2)
|
|
259
272
|
branch.direction = Const(1)
|
|
260
|
-
for unused in (
|
|
261
|
-
|
|
262
|
-
|
|
273
|
+
for unused in (
|
|
274
|
+
"velocity_mps",
|
|
275
|
+
"reynolds_scaled",
|
|
276
|
+
"gas_density_kg_per_m3",
|
|
277
|
+
"friction",
|
|
278
|
+
"mass_flow_pos_kgs_squared",
|
|
279
|
+
"mass_flow_neg_kgs_squared",
|
|
280
|
+
):
|
|
263
281
|
setattr(branch, unused, Const(0.0))
|
|
264
282
|
|
|
265
283
|
def equations(self, branch, grid, from_node_model, to_node_model, **kwargs):
|
|
266
284
|
m = branch.mass_flow_kgs
|
|
267
285
|
mag = kwargs["sqrt_impl"](m * m) # |m|, so it works in either flow direction
|
|
268
286
|
return [
|
|
287
|
+
# from->to flow rides mass_flow_neg_kgs, i.e. m < 0, so the drop
|
|
288
|
+
# needs the minus sign to fall along the flow.
|
|
269
289
|
from_node_model.pressure_pu - to_node_model.pressure_pu
|
|
270
|
-
== branch.resistance * m,
|
|
271
|
-
#
|
|
290
|
+
== -branch.resistance * m,
|
|
291
|
+
# The nodal balance consumes the positive / negative flow split, and
|
|
292
|
+
# it fixes the reported sign: mass_flow_kgs = pos - neg is negative
|
|
293
|
+
# when the fluid runs from the from-node to the to-node.
|
|
272
294
|
branch.mass_flow_pos_kgs == 0.5 * (mag + m),
|
|
273
295
|
branch.mass_flow_neg_kgs == 0.5 * (mag - m),
|
|
274
296
|
]
|
|
275
297
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
298
|
+
|
|
299
|
+
net.apply_formulation(
|
|
300
|
+
NetworkFormulation(
|
|
301
|
+
branch_type_to_formulations={GasPipe: MyGasPipeFormulation()},
|
|
302
|
+
)
|
|
303
|
+
)
|
|
279
304
|
```
|
|
280
305
|
|
|
281
306
|
---
|
|
@@ -291,33 +316,43 @@ Every layer of the framework is designed to be subclassed or replaced without mo
|
|
|
291
316
|
```python
|
|
292
317
|
import monee.model as mm
|
|
293
318
|
from monee import run_energy_flow_optimization
|
|
294
|
-
from monee.problem import
|
|
319
|
+
from monee.problem import (
|
|
320
|
+
AttributeParameter,
|
|
321
|
+
Constraints,
|
|
322
|
+
Objectives,
|
|
323
|
+
OptimizationProblem,
|
|
324
|
+
)
|
|
295
325
|
|
|
296
326
|
problem = OptimizationProblem()
|
|
297
327
|
|
|
298
328
|
# Make regulation ∈ [0, 1] a solver decision variable for each demand
|
|
299
|
-
problem.controllable_demands(
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
329
|
+
problem.controllable_demands(
|
|
330
|
+
[
|
|
331
|
+
(
|
|
332
|
+
"regulation",
|
|
333
|
+
AttributeParameter(
|
|
334
|
+
min=lambda attr, val: 0,
|
|
335
|
+
max=lambda attr, val: 1,
|
|
336
|
+
val=lambda attr, val: 1,
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
]
|
|
340
|
+
)
|
|
306
341
|
|
|
307
342
|
# Objective: minimise curtailment cost weighted by load priority
|
|
308
343
|
objectives = Objectives()
|
|
309
344
|
objectives.select(
|
|
310
345
|
lambda model: isinstance(model, mm.PowerLoad) and hasattr(model, "_priority")
|
|
311
|
-
).data(
|
|
312
|
-
lambda model: (1 - model.regulation) * model.p_mw * model._priority
|
|
313
|
-
).calculate(
|
|
346
|
+
).data(lambda model: (1 - model.regulation) * model.p_mw * model._priority).calculate(
|
|
314
347
|
lambda model_to_data: sum(model_to_data.values())
|
|
315
348
|
)
|
|
316
349
|
problem.objectives = objectives
|
|
317
350
|
|
|
318
|
-
# Constraint: cap the substation import
|
|
351
|
+
# Constraint: cap the substation import at 0.6 MW.
|
|
352
|
+
# ExtPowerGrid.p_mw follows the load convention: import is negative, export
|
|
353
|
+
# positive, so an import cap is a *lower* bound.
|
|
319
354
|
constraints = Constraints()
|
|
320
|
-
constraints.select_types(mm.ExtPowerGrid).equation(lambda model: model.p_mw
|
|
355
|
+
constraints.select_types(mm.ExtPowerGrid).equation(lambda model: model.p_mw >= -0.6)
|
|
321
356
|
problem.constraints = constraints
|
|
322
357
|
|
|
323
358
|
result = run_energy_flow_optimization(net, problem)
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
---
|
|
21
21
|
|
|
22
|
-
**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
|
|
22
|
+
**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.
|
|
23
23
|
|
|
24
24
|
## Capabilities
|
|
25
25
|
|
|
@@ -58,12 +58,12 @@ Requires **Python 3.11+**.
|
|
|
58
58
|
pip install monee
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
The core install
|
|
62
|
-
optimisation workflows:
|
|
61
|
+
The core install pulls in, as dependencies, the third-party packages needed for
|
|
62
|
+
both simulation and the default optimisation workflows:
|
|
63
63
|
|
|
64
|
-
- **CasADi**: in-process
|
|
64
|
+
- **CasADi**: runs the IPOPT solver in-process; monee uses it by default.
|
|
65
65
|
- **GEKKO**: bundles its own APOPT/BPOPT/IPOPT binaries; used as the IPOPT fallback if CasADi is unavailable.
|
|
66
|
-
- **Pyomo**:
|
|
66
|
+
- **Pyomo** and **pyscipopt**: hand MILP/MIQCP problems such as MISOCP optimal power flow to the SCIP solver, or to any other solver Pyomo supports.
|
|
67
67
|
|
|
68
68
|
Optional solvers and import formats are pulled in only when you need them:
|
|
69
69
|
|
|
@@ -87,22 +87,22 @@ net = mx.create_multi_energy_network()
|
|
|
87
87
|
# Electricity grid
|
|
88
88
|
bus_0 = mx.create_bus(net)
|
|
89
89
|
bus_1 = mx.create_bus(net)
|
|
90
|
-
mx.create_line(net, bus_0, bus_1, length_m=100,
|
|
91
|
-
r_ohm_per_m=7e-5, x_ohm_per_m=7e-5)
|
|
90
|
+
mx.create_line(net, bus_0, bus_1, length_m=100, r_ohm_per_m=7e-5, x_ohm_per_m=7e-5)
|
|
92
91
|
mx.create_ext_power_grid(net, bus_0)
|
|
93
92
|
mx.create_power_load(net, bus_1, p_mw=0.1, q_mvar=0.0)
|
|
94
93
|
|
|
95
94
|
# District heating loop
|
|
96
95
|
j_supply = mx.create_water_junction(net)
|
|
97
|
-
j_mid
|
|
96
|
+
j_mid = mx.create_water_junction(net)
|
|
98
97
|
j_return = mx.create_water_junction(net)
|
|
99
98
|
mx.create_ext_hydr_grid(net, j_supply)
|
|
100
99
|
mx.create_water_pipe(net, j_supply, j_mid, diameter_m=0.12, length_m=100)
|
|
101
100
|
mx.create_sink(net, j_return, mass_flow_kgs=1.0)
|
|
102
101
|
|
|
103
102
|
# Coupling: bus_1 drives a heat pump that feeds the heating loop
|
|
104
|
-
mx.create_p2h(
|
|
105
|
-
|
|
103
|
+
mx.create_p2h(
|
|
104
|
+
net, bus_1, j_mid, j_return, heat_energy_mw=0.1, diameter_m=0.1, efficiency=0.9
|
|
105
|
+
)
|
|
106
106
|
|
|
107
107
|
result = run_energy_flow(net)
|
|
108
108
|
print(result.get(mm.Bus)[["vm_pu", "va_degree"]])
|
|
@@ -130,20 +130,20 @@ from monee import mx
|
|
|
130
130
|
|
|
131
131
|
net = mx.create_multi_energy_network()
|
|
132
132
|
bus_ext = mx.create_bus(net)
|
|
133
|
-
bus_a
|
|
134
|
-
bus_b
|
|
133
|
+
bus_a = mx.create_bus(net)
|
|
134
|
+
bus_b = mx.create_bus(net)
|
|
135
135
|
mx.create_line(net, bus_ext, bus_a, length_m=100, r_ohm_per_m=7e-5, x_ohm_per_m=7e-5)
|
|
136
136
|
mx.create_line(net, bus_ext, bus_b, length_m=100, r_ohm_per_m=7e-5, x_ohm_per_m=7e-5)
|
|
137
137
|
mx.create_ext_power_grid(net, bus_ext)
|
|
138
138
|
mx.create_power_load(net, bus_a, p_mw=0.1, q_mvar=0.0)
|
|
139
139
|
|
|
140
|
-
# Two dispatchable generators with different marginal costs (currency/MW)
|
|
141
|
-
mx.create_power_generator(net, bus_a, p_mw=0.15, q_mvar=0.0, cost=10.0)
|
|
142
|
-
mx.create_power_generator(net, bus_b, p_mw=0.15, q_mvar=0.0, cost=40.0)
|
|
140
|
+
# Two dispatchable generators with different marginal costs (currency/MW).
|
|
141
|
+
mx.create_power_generator(net, bus_a, p_mw=0.15, q_mvar=0.0, cost=10.0) # cheap
|
|
142
|
+
mx.create_power_generator(net, bus_b, p_mw=0.15, q_mvar=0.0, cost=40.0) # expensive
|
|
143
143
|
|
|
144
144
|
problem = monee.create_economic_dispatch_problem(
|
|
145
|
-
ext_grid_cost_default=0.0,
|
|
146
|
-
ext_grid_bounds=(-0.01, 0.01),
|
|
145
|
+
ext_grid_cost_default=0.0, # include the slack so it can be bounded
|
|
146
|
+
ext_grid_bounds=(-0.01, 0.01), # substation import/export limit (MW)
|
|
147
147
|
)
|
|
148
148
|
|
|
149
149
|
result = monee.run_energy_flow_optimization(net, problem, solver="ipopt")
|
|
@@ -151,8 +151,18 @@ result = monee.run_energy_flow_optimization(net, problem, solver="ipopt")
|
|
|
151
151
|
# The cheap generator carries the load; the expensive one stays near zero.
|
|
152
152
|
# (generation uses the load convention, so dispatched power is negative)
|
|
153
153
|
print(result.get(mm.PowerGenerator)[["p_mw"]])
|
|
154
|
+
# p_mw
|
|
155
|
+
# 0 -9.000072e-02
|
|
156
|
+
# 1 6.969836e-09
|
|
154
157
|
```
|
|
155
158
|
|
|
159
|
+
The same problem dispatches gas and heat as well: pass `gas_cost_default` and
|
|
160
|
+
`heat_cost_default` (prices per MW of gas energy and per MW of heat) and give
|
|
161
|
+
gas sources, heat generators and external grids their own `cost`; coupling
|
|
162
|
+
units such as CHPs then follow the merit order too. See the
|
|
163
|
+
[economic dispatch](https://monee.readthedocs.io/en/latest/problems/economic_dispatch.html)
|
|
164
|
+
page.
|
|
165
|
+
|
|
156
166
|
For resilience studies, `create_min_load_shedding_problem` instead minimises
|
|
157
167
|
curtailment with per-carrier operational bounds, returning a `regulation` per
|
|
158
168
|
demand (`1.0` fully served, `0.0` completely shed). Both problems accept the
|
|
@@ -184,7 +194,9 @@ To use the convex MISOCP relaxation for electricity optimal power flow, select i
|
|
|
184
194
|
```python
|
|
185
195
|
import monee
|
|
186
196
|
|
|
187
|
-
result = monee.run_energy_flow(
|
|
197
|
+
result = monee.run_energy_flow(
|
|
198
|
+
net, solver="gurobi", formulation="el_misocp", simulation=False
|
|
199
|
+
)
|
|
188
200
|
|
|
189
201
|
# or record it as the network-level choice for all subsequent solves
|
|
190
202
|
net.apply_formulation(monee.EL_MISOCP_FORMULATION)
|
|
@@ -197,6 +209,7 @@ from monee.model.core import Const, Var
|
|
|
197
209
|
from monee.model.formulation.core import BranchFormulation, NetworkFormulation
|
|
198
210
|
from monee.model.branch import GasPipe
|
|
199
211
|
|
|
212
|
+
|
|
200
213
|
class MyGasPipeFormulation(BranchFormulation):
|
|
201
214
|
"""Toy linear pipe: pressure drop proportional to the mass flow."""
|
|
202
215
|
|
|
@@ -204,27 +217,39 @@ class MyGasPipeFormulation(BranchFormulation):
|
|
|
204
217
|
# One signed mass-flow decision variable plus a geometric resistance the
|
|
205
218
|
# equation references; neutralise the Vars this linear model does not use.
|
|
206
219
|
branch.mass_flow_kgs = Var(0.1, name="mass_flow_kgs")
|
|
207
|
-
branch.resistance = Const(1e-
|
|
220
|
+
branch.resistance = Const(1e-7 * branch.length_m / branch.diameter_m**2)
|
|
208
221
|
branch.direction = Const(1)
|
|
209
|
-
for unused in (
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
for unused in (
|
|
223
|
+
"velocity_mps",
|
|
224
|
+
"reynolds_scaled",
|
|
225
|
+
"gas_density_kg_per_m3",
|
|
226
|
+
"friction",
|
|
227
|
+
"mass_flow_pos_kgs_squared",
|
|
228
|
+
"mass_flow_neg_kgs_squared",
|
|
229
|
+
):
|
|
212
230
|
setattr(branch, unused, Const(0.0))
|
|
213
231
|
|
|
214
232
|
def equations(self, branch, grid, from_node_model, to_node_model, **kwargs):
|
|
215
233
|
m = branch.mass_flow_kgs
|
|
216
234
|
mag = kwargs["sqrt_impl"](m * m) # |m|, so it works in either flow direction
|
|
217
235
|
return [
|
|
236
|
+
# from->to flow rides mass_flow_neg_kgs, i.e. m < 0, so the drop
|
|
237
|
+
# needs the minus sign to fall along the flow.
|
|
218
238
|
from_node_model.pressure_pu - to_node_model.pressure_pu
|
|
219
|
-
== branch.resistance * m,
|
|
220
|
-
#
|
|
239
|
+
== -branch.resistance * m,
|
|
240
|
+
# The nodal balance consumes the positive / negative flow split, and
|
|
241
|
+
# it fixes the reported sign: mass_flow_kgs = pos - neg is negative
|
|
242
|
+
# when the fluid runs from the from-node to the to-node.
|
|
221
243
|
branch.mass_flow_pos_kgs == 0.5 * (mag + m),
|
|
222
244
|
branch.mass_flow_neg_kgs == 0.5 * (mag - m),
|
|
223
245
|
]
|
|
224
246
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
247
|
+
|
|
248
|
+
net.apply_formulation(
|
|
249
|
+
NetworkFormulation(
|
|
250
|
+
branch_type_to_formulations={GasPipe: MyGasPipeFormulation()},
|
|
251
|
+
)
|
|
252
|
+
)
|
|
228
253
|
```
|
|
229
254
|
|
|
230
255
|
---
|
|
@@ -240,33 +265,43 @@ Every layer of the framework is designed to be subclassed or replaced without mo
|
|
|
240
265
|
```python
|
|
241
266
|
import monee.model as mm
|
|
242
267
|
from monee import run_energy_flow_optimization
|
|
243
|
-
from monee.problem import
|
|
268
|
+
from monee.problem import (
|
|
269
|
+
AttributeParameter,
|
|
270
|
+
Constraints,
|
|
271
|
+
Objectives,
|
|
272
|
+
OptimizationProblem,
|
|
273
|
+
)
|
|
244
274
|
|
|
245
275
|
problem = OptimizationProblem()
|
|
246
276
|
|
|
247
277
|
# Make regulation ∈ [0, 1] a solver decision variable for each demand
|
|
248
|
-
problem.controllable_demands(
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
278
|
+
problem.controllable_demands(
|
|
279
|
+
[
|
|
280
|
+
(
|
|
281
|
+
"regulation",
|
|
282
|
+
AttributeParameter(
|
|
283
|
+
min=lambda attr, val: 0,
|
|
284
|
+
max=lambda attr, val: 1,
|
|
285
|
+
val=lambda attr, val: 1,
|
|
286
|
+
),
|
|
287
|
+
)
|
|
288
|
+
]
|
|
289
|
+
)
|
|
255
290
|
|
|
256
291
|
# Objective: minimise curtailment cost weighted by load priority
|
|
257
292
|
objectives = Objectives()
|
|
258
293
|
objectives.select(
|
|
259
294
|
lambda model: isinstance(model, mm.PowerLoad) and hasattr(model, "_priority")
|
|
260
|
-
).data(
|
|
261
|
-
lambda model: (1 - model.regulation) * model.p_mw * model._priority
|
|
262
|
-
).calculate(
|
|
295
|
+
).data(lambda model: (1 - model.regulation) * model.p_mw * model._priority).calculate(
|
|
263
296
|
lambda model_to_data: sum(model_to_data.values())
|
|
264
297
|
)
|
|
265
298
|
problem.objectives = objectives
|
|
266
299
|
|
|
267
|
-
# Constraint: cap the substation import
|
|
300
|
+
# Constraint: cap the substation import at 0.6 MW.
|
|
301
|
+
# ExtPowerGrid.p_mw follows the load convention: import is negative, export
|
|
302
|
+
# positive, so an import cap is a *lower* bound.
|
|
268
303
|
constraints = Constraints()
|
|
269
|
-
constraints.select_types(mm.ExtPowerGrid).equation(lambda model: model.p_mw
|
|
304
|
+
constraints.select_types(mm.ExtPowerGrid).equation(lambda model: model.p_mw >= -0.6)
|
|
270
305
|
problem.constraints = constraints
|
|
271
306
|
|
|
272
307
|
result = run_energy_flow_optimization(net, problem)
|
|
@@ -89,7 +89,12 @@ from monee.simulation import (
|
|
|
89
89
|
NetworkChange,
|
|
90
90
|
)
|
|
91
91
|
from monee.solver import GEKKOSolver, PyomoSolver
|
|
92
|
-
from monee.solver.core import
|
|
92
|
+
from monee.solver.core import (
|
|
93
|
+
persist_solution,
|
|
94
|
+
compute_bound_violations,
|
|
95
|
+
ResultWarning,
|
|
96
|
+
ValidationError,
|
|
97
|
+
)
|
|
93
98
|
from monee.visualization import plot_network, plot_result
|
|
94
99
|
|
|
95
100
|
|
|
@@ -124,17 +129,35 @@ def enable_islanding(
|
|
|
124
129
|
return config
|
|
125
130
|
|
|
126
131
|
|
|
127
|
-
def run_energy_flow(
|
|
132
|
+
def run_energy_flow(
|
|
133
|
+
net: mm.Network,
|
|
134
|
+
solver=None,
|
|
135
|
+
simulation: bool = True,
|
|
136
|
+
copy: bool = False,
|
|
137
|
+
**kwargs,
|
|
138
|
+
):
|
|
128
139
|
"""
|
|
129
140
|
Performs a basic energy flow analysis on a network without applying optimization constraints.
|
|
130
141
|
|
|
131
142
|
This function provides a straightforward assessment of energy flows within a network, making it useful for initial feasibility checks, diagnostics, or scenarios where optimization is not required. Use this function when you need quick insights into network behavior or as a baseline before introducing optimization-based analyses. It fits into workflows involving network validation, troubleshooting, or preliminary studies. Internally, the function delegates to `run_energy_flow_optimization` with the optimization problem set to `None`, utilizing the same solver infrastructure but bypassing optimization logic.
|
|
132
143
|
|
|
144
|
+
Warning:
|
|
145
|
+
By default this MUTATES the input network: the solved values are
|
|
146
|
+
written back into ``net``'s Vars after the solve, so the input doubles
|
|
147
|
+
as the warm start for the next solve (:class:`monee.Stepper` and
|
|
148
|
+
:func:`monee.run_timeseries` rely on this). Pass ``copy=True`` to
|
|
149
|
+
solve an internal ``net.copy()`` instead and leave the input
|
|
150
|
+
untouched; the solved values are then only available on
|
|
151
|
+
``result.network``. See the concepts/solvers docs page.
|
|
152
|
+
|
|
133
153
|
Args:
|
|
134
154
|
net (mm.Network): The network to analyze, represented as an `mm.Network` instance. The network must be fully defined, including all necessary nodes and parameters.
|
|
135
155
|
solver (optional): The solver to use for the energy flow computation. If not specified, a default compatible solver is chosen. The solver must support the network's structure.
|
|
136
156
|
simulation (bool): When ``True`` (default), solve as a square steady-state simulation (GEKKO IMODE=1, falling back to IMODE=3 if the model is not square). Pass ``False`` to force the optimize-the-feasibility-problem path. Ignored by backends without a simulation mode (e.g. Pyomo). Check ``result.mode_used`` to see which path actually ran.
|
|
137
|
-
|
|
157
|
+
copy (bool): When ``False`` (default), solved values are written back
|
|
158
|
+
into ``net`` in place (warm-start contract above). ``True`` solves
|
|
159
|
+
a copy and leaves ``net`` untouched.
|
|
160
|
+
**kwargs: Additional keyword arguments for solver configuration or analysis tuning, forwarded to the backend ``solve``. Notably ``strict=True`` raises :class:`monee.ValidationError` when the post-solve result validation attaches warnings (the permissive default only records them on ``result.warnings``); see the how-to/diagnose_infeasibility docs page.
|
|
138
161
|
|
|
139
162
|
Returns:
|
|
140
163
|
Any: The result of the energy flow analysis, typically including calculated flow values and status information. The exact structure depends on the solver and network configuration.
|
|
@@ -155,23 +178,39 @@ def run_energy_flow(net: mm.Network, solver=None, simulation: bool = True, **kwa
|
|
|
155
178
|
result = run_energy_flow(my_network, max_iter=500, tol=1e-5)
|
|
156
179
|
"""
|
|
157
180
|
return run_energy_flow_optimization(
|
|
158
|
-
net, None, solver=solver, simulation=simulation, **kwargs
|
|
181
|
+
net, None, solver=solver, simulation=simulation, copy=copy, **kwargs
|
|
159
182
|
)
|
|
160
183
|
|
|
161
184
|
|
|
162
185
|
def run_energy_flow_optimization(
|
|
163
|
-
net: mm.Network,
|
|
186
|
+
net: mm.Network,
|
|
187
|
+
optimization_problem: mp.OptimizationProblem,
|
|
188
|
+
solver=None,
|
|
189
|
+
copy: bool = False,
|
|
190
|
+
**kwargs,
|
|
164
191
|
):
|
|
165
192
|
"""
|
|
166
193
|
Executes an energy flow optimization on a given network using a specified optimization problem and solver.
|
|
167
194
|
|
|
168
195
|
This function determines the optimal distribution of energy flows within a network, subject to constraints and objectives defined by the provided optimization problem. Use this function when you need to solve power grid management, load balancing, or energy distribution planning tasks. It is typically integrated into simulation, planning, or real-time control workflows for energy systems. The function delegates the optimization process to a solver, which processes the network and problem definition to compute the optimal solution.
|
|
169
196
|
|
|
197
|
+
Warning:
|
|
198
|
+
By default this MUTATES the input network: the solved values are
|
|
199
|
+
written back into ``net``'s Vars after the solve, so the input doubles
|
|
200
|
+
as the warm start for the next solve (:class:`monee.Stepper` and
|
|
201
|
+
:func:`monee.run_timeseries` rely on this). Pass ``copy=True`` to
|
|
202
|
+
solve an internal ``net.copy()`` instead and leave the input
|
|
203
|
+
untouched; the solved values are then only available on
|
|
204
|
+
``result.network``. See the concepts/solvers docs page.
|
|
205
|
+
|
|
170
206
|
Args:
|
|
171
207
|
net (mm.Network): The network to optimize. Must be a fully specified `mm.Network` object with all nodes, edges, and parameters defined.
|
|
172
208
|
optimization_problem (mp.OptimizationProblem): The optimization problem instance, specifying constraints and objectives for the energy flow.
|
|
173
209
|
solver (optional): The solver to use for optimization. If None, a default compatible solver is selected. Must support the problem's formulation.
|
|
174
|
-
|
|
210
|
+
copy (bool): When ``False`` (default), solved values are written back
|
|
211
|
+
into ``net`` in place (warm-start contract above). ``True`` solves
|
|
212
|
+
a copy and leaves ``net`` untouched.
|
|
213
|
+
**kwargs: Additional keyword arguments for solver configuration or optimization tuning, forwarded to the backend ``solve``. Notably ``strict=True`` raises :class:`monee.ValidationError` when the post-solve result validation attaches warnings (the permissive default only records them on ``result.warnings``); see the how-to/diagnose_infeasibility docs page.
|
|
175
214
|
|
|
176
215
|
Returns:
|
|
177
216
|
Any: The optimization result, which may include optimal energy flows, solution status, and additional diagnostic information. The exact structure depends on the solver and problem definition.
|
|
@@ -191,11 +230,13 @@ def run_energy_flow_optimization(
|
|
|
191
230
|
Use the default solver with additional options:
|
|
192
231
|
result = run_energy_flow_optimization(my_network, my_problem, max_iter=1000, tol=1e-6)
|
|
193
232
|
"""
|
|
233
|
+
if copy:
|
|
234
|
+
net = net.copy()
|
|
194
235
|
return solve(net, optimization_problem, solver, **kwargs)
|
|
195
236
|
|
|
196
237
|
|
|
197
|
-
def solve_load_shedding_problem(
|
|
198
|
-
network: Network,
|
|
238
|
+
def solve_load_shedding_problem(
|
|
239
|
+
network: Network, # NOSONAR
|
|
199
240
|
*,
|
|
200
241
|
demand_weight=WEIGHT_DEMAND,
|
|
201
242
|
generator_weight=WEIGHT_GENERATOR,
|
|
@@ -204,9 +245,9 @@ def solve_load_shedding_problem( # NOSONAR
|
|
|
204
245
|
bounds_pressure: tuple = (0.9, 1.1),
|
|
205
246
|
bounds_t: tuple = (0.9, 1.1),
|
|
206
247
|
max_line_loading=1.5,
|
|
207
|
-
bounds_ext_el: tuple =
|
|
208
|
-
bounds_ext_gas: tuple =
|
|
209
|
-
bounds_ext_heat: tuple =
|
|
248
|
+
bounds_ext_el: tuple | None = None,
|
|
249
|
+
bounds_ext_gas: tuple | None = None,
|
|
250
|
+
bounds_ext_heat: tuple | None = None,
|
|
210
251
|
regulation_ramp_limit=None,
|
|
211
252
|
include_storages=False,
|
|
212
253
|
include_ext_grids=True,
|
|
@@ -242,9 +283,16 @@ def solve_load_shedding_problem( # NOSONAR
|
|
|
242
283
|
bounds_pressure (tuple): Per-unit pressure bounds (min, max) for the gas subsystem.
|
|
243
284
|
bounds_t (tuple): Per-unit temperature bounds (min, max) for the thermal subsystem.
|
|
244
285
|
max_line_loading: Line-loading cap enforced when ``check_lp`` is True.
|
|
245
|
-
bounds_ext_el (tuple): External electrical-grid exchange bounds
|
|
246
|
-
|
|
247
|
-
|
|
286
|
+
bounds_ext_el (tuple | None): External electrical-grid exchange bounds
|
|
287
|
+
(min, max) on ``ExtPowerGrid.p_mw``, load convention: import is
|
|
288
|
+
negative, export positive, so an import cap of X MW is ``(-X, 0)``.
|
|
289
|
+
Defaults to ``None`` (unconstrained).
|
|
290
|
+
bounds_ext_gas (tuple | None): External gas-grid exchange bounds
|
|
291
|
+
(min, max) on ``ExtHydrGrid.mass_flow_kgs``, load convention as
|
|
292
|
+
above. Defaults to ``None`` (unconstrained).
|
|
293
|
+
bounds_ext_heat (tuple | None): External heat-grid exchange bounds
|
|
294
|
+
(min, max) on ``ExtHydrGrid.mass_flow_kgs``, load convention as
|
|
295
|
+
above. Defaults to ``None`` (unconstrained).
|
|
248
296
|
regulation_ramp_limit: Optional per-step regulation ramp limit.
|
|
249
297
|
include_storages (bool): Make storages controllable. Defaults to False.
|
|
250
298
|
include_ext_grids (bool): Constrain external-grid exchange. Defaults to True.
|