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.
Files changed (113) hide show
  1. {monee-2.0.0/src/monee.egg-info → monee-2.2.0}/PKG-INFO +76 -41
  2. {monee-2.0.0 → monee-2.2.0}/README.md +75 -40
  3. {monee-2.0.0 → monee-2.2.0}/pyproject.toml +1 -1
  4. {monee-2.0.0 → monee-2.2.0}/src/monee/__init__.py +62 -14
  5. monee-2.2.0/src/monee/express/__init__.py +1473 -0
  6. monee-2.2.0/src/monee/express/structures.py +879 -0
  7. {monee-2.0.0 → monee-2.2.0}/src/monee/io/__init__.py +5 -4
  8. {monee-2.0.0 → monee-2.2.0}/src/monee/io/from_cim.py +1 -1
  9. monee-2.2.0/src/monee/io/from_pandapower.py +543 -0
  10. monee-2.2.0/src/monee/io/from_simbench.py +233 -0
  11. {monee-2.0.0 → monee-2.2.0}/src/monee/io/matpower.py +193 -137
  12. {monee-2.0.0 → monee-2.2.0}/src/monee/io/native.py +184 -43
  13. {monee-2.0.0 → monee-2.2.0}/src/monee/model/__init__.py +1 -0
  14. {monee-2.0.0 → monee-2.2.0}/src/monee/model/branch.py +36 -6
  15. {monee-2.0.0 → monee-2.2.0}/src/monee/model/child.py +118 -9
  16. {monee-2.0.0 → monee-2.2.0}/src/monee/model/core.py +287 -4
  17. {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/core.py +1 -1
  18. {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/islanding/core.py +172 -46
  19. monee-2.2.0/src/monee/model/extension/islanding/el.py +311 -0
  20. {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/islanding/gas.py +23 -3
  21. {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/ltc.py +106 -73
  22. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/__init__.py +1 -1
  23. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/bundles.py +19 -1
  24. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/common.py +29 -1
  25. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/core.py +14 -4
  26. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/milp/gas.py +1 -1
  27. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/milp/heat.py +222 -127
  28. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/convex/el.py +102 -14
  29. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/convex/gas.py +21 -1
  30. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/nonconvex/el.py +3 -3
  31. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/nonconvex/heat.py +5 -1
  32. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/nlp/gas.py +7 -2
  33. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/nlp/heat.py +6 -1
  34. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/registry.py +106 -1
  35. {monee-2.0.0 → monee-2.2.0}/src/monee/model/grid.py +13 -0
  36. {monee-2.0.0 → monee-2.2.0}/src/monee/model/multi.py +187 -24
  37. {monee-2.0.0 → monee-2.2.0}/src/monee/model/network.py +414 -41
  38. {monee-2.0.0 → monee-2.2.0}/src/monee/model/node.py +10 -2
  39. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/misoc/pf.py +5 -0
  40. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/nonlinear/ac.py +2 -2
  41. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/nonlinear/gf.py +5 -4
  42. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/nonlinear/smooth.py +21 -2
  43. {monee-2.0.0 → monee-2.2.0}/src/monee/model/storage.py +8 -4
  44. {monee-2.0.0 → monee-2.2.0}/src/monee/network/mes.py +224 -47
  45. {monee-2.0.0 → monee-2.2.0}/src/monee/network/res.py +9 -9
  46. {monee-2.0.0 → monee-2.2.0}/src/monee/problem/__init__.py +7 -0
  47. {monee-2.0.0 → monee-2.2.0}/src/monee/problem/core.py +426 -93
  48. monee-2.2.0/src/monee/problem/economic_dispatch.py +638 -0
  49. {monee-2.0.0 → monee-2.2.0}/src/monee/problem/metric.py +33 -1
  50. {monee-2.0.0 → monee-2.2.0}/src/monee/problem/min_load_shedding.py +355 -53
  51. monee-2.2.0/src/monee/problem/utils.py +314 -0
  52. {monee-2.0.0 → monee-2.2.0}/src/monee/simulation/core.py +14 -8
  53. {monee-2.0.0 → monee-2.2.0}/src/monee/simulation/multi_period.py +298 -140
  54. monee-2.2.0/src/monee/simulation/result_utils.py +219 -0
  55. {monee-2.0.0 → monee-2.2.0}/src/monee/simulation/stepper.py +195 -36
  56. {monee-2.0.0 → monee-2.2.0}/src/monee/simulation/timeseries.py +388 -30
  57. {monee-2.0.0 → monee-2.2.0}/src/monee/solver/__init__.py +12 -1
  58. {monee-2.0.0 → monee-2.2.0}/src/monee/solver/casadi.py +804 -46
  59. {monee-2.0.0 → monee-2.2.0}/src/monee/solver/core.py +1243 -121
  60. {monee-2.0.0 → monee-2.2.0}/src/monee/solver/dispatch.py +203 -2
  61. {monee-2.0.0 → monee-2.2.0}/src/monee/solver/gekko.py +183 -26
  62. {monee-2.0.0 → monee-2.2.0}/src/monee/solver/gurobipy.py +73 -44
  63. {monee-2.0.0 → monee-2.2.0}/src/monee/solver/infeasibility/__init__.py +2 -0
  64. {monee-2.0.0 → monee-2.2.0}/src/monee/solver/infeasibility/apm.py +4 -3
  65. monee-2.2.0/src/monee/solver/infeasibility/pyo.py +885 -0
  66. {monee-2.0.0 → monee-2.2.0}/src/monee/solver/pyo.py +95 -19
  67. {monee-2.0.0 → monee-2.2.0}/src/monee/visualization/result_visualization.py +74 -31
  68. {monee-2.0.0 → monee-2.2.0}/src/monee/visualization/visualization.py +7 -7
  69. {monee-2.0.0 → monee-2.2.0/src/monee.egg-info}/PKG-INFO +76 -41
  70. monee-2.0.0/src/monee/express/__init__.py +0 -1025
  71. monee-2.0.0/src/monee/express/structures.py +0 -483
  72. monee-2.0.0/src/monee/io/from_pandapower.py +0 -278
  73. monee-2.0.0/src/monee/io/from_simbench.py +0 -110
  74. monee-2.0.0/src/monee/model/extension/islanding/el.py +0 -82
  75. monee-2.0.0/src/monee/problem/economic_dispatch.py +0 -119
  76. monee-2.0.0/src/monee/problem/utils.py +0 -151
  77. monee-2.0.0/src/monee/simulation/result_utils.py +0 -137
  78. monee-2.0.0/src/monee/solver/infeasibility/pyo.py +0 -374
  79. {monee-2.0.0 → monee-2.2.0}/LICENSE +0 -0
  80. {monee-2.0.0 → monee-2.2.0}/setup.cfg +0 -0
  81. {monee-2.0.0 → monee-2.2.0}/src/monee/io/_utils.py +0 -0
  82. {monee-2.0.0 → monee-2.2.0}/src/monee/io/from_esdl.py +0 -0
  83. {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/__init__.py +0 -0
  84. {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/islanding/__init__.py +0 -0
  85. {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/islanding/water.py +0 -0
  86. {monee-2.0.0 → monee-2.2.0}/src/monee/model/extension/linepack.py +0 -0
  87. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/milp/__init__.py +0 -0
  88. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/__init__.py +0 -0
  89. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/convex/__init__.py +0 -0
  90. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/nonconvex/__init__.py +0 -0
  91. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/miqcqp/nonconvex/gas.py +0 -0
  92. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/nlp/__init__.py +0 -0
  93. {monee-2.0.0 → monee-2.2.0}/src/monee/model/formulation/nlp/el.py +0 -0
  94. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/__init__.py +0 -0
  95. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/core/__init__.py +0 -0
  96. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/core/hydraulics.py +0 -0
  97. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/core/properties.py +0 -0
  98. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/islanding.py +0 -0
  99. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/linear/__init__.py +0 -0
  100. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/misoc/__init__.py +0 -0
  101. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/nonlinear/__init__.py +0 -0
  102. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/nonlinear/hf.py +0 -0
  103. {monee-2.0.0 → monee-2.2.0}/src/monee/model/phys/nonlinear/wf.py +0 -0
  104. {monee-2.0.0 → monee-2.2.0}/src/monee/network/__init__.py +0 -0
  105. {monee-2.0.0 → monee-2.2.0}/src/monee/network/bench/__init__.py +0 -0
  106. {monee-2.0.0 → monee-2.2.0}/src/monee/network/bench/restoration.py +0 -0
  107. {monee-2.0.0 → monee-2.2.0}/src/monee/simulation/__init__.py +0 -0
  108. {monee-2.0.0 → monee-2.2.0}/src/monee/simulation/step_state.py +0 -0
  109. {monee-2.0.0 → monee-2.2.0}/src/monee/visualization/__init__.py +0 -0
  110. {monee-2.0.0 → monee-2.2.0}/src/monee.egg-info/SOURCES.txt +0 -0
  111. {monee-2.0.0 → monee-2.2.0}/src/monee.egg-info/dependency_links.txt +0 -0
  112. {monee-2.0.0 → monee-2.2.0}/src/monee.egg-info/requires.txt +0 -0
  113. {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.0.0
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 exposes the coupled system to three solver back-ends (CasADi, GEKKO, and Pyomo). The framework is designed for research workflows that require flexible problem formulation, reproducible benchmarks, and straightforward integration with external optimisers.
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 bundles everything needed for both simulation and the default
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 IPOPT, the default solver when present.
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**: for MILP/MIQCP problems such as MISOCP optimal power flow.
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 = mx.create_water_junction(net)
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(net, bus_1, j_mid, j_return,
156
- heat_energy_mw=0.1, diameter_m=0.1, efficiency=0.9)
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 = mx.create_bus(net)
185
- bus_b = mx.create_bus(net)
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) # cheap
193
- mx.create_power_generator(net, bus_b, p_mw=0.15, q_mvar=0.0, cost=40.0) # expensive
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, # include the slack so it can be bounded
197
- ext_grid_bounds=(-0.01, 0.01), # substation import/export limit (MW)
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(net, solver="gurobi", formulation="el_misocp", simulation=False)
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-3 * branch.length_m / branch.diameter_m**2)
271
+ branch.resistance = Const(1e-7 * branch.length_m / branch.diameter_m**2)
259
272
  branch.direction = Const(1)
260
- for unused in ("velocity_mps", "reynolds_scaled", "gas_density_kg_per_m3",
261
- "friction", "mass_flow_pos_kgs_squared",
262
- "mass_flow_neg_kgs_squared"):
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
- # the nodal balance consumes the positive / negative flow split
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
- net.apply_formulation(NetworkFormulation(
277
- branch_type_to_formulations={GasPipe: MyGasPipeFormulation()},
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 AttributeParameter, Constraints, Objectives, OptimizationProblem
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
- ("regulation", AttributeParameter(
301
- min=lambda attr, val: 0,
302
- max=lambda attr, val: 1,
303
- val=lambda attr, val: 1,
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 <= 0.6)
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 exposes the coupled system to three solver back-ends (CasADi, GEKKO, and Pyomo). The framework is designed for research workflows that require flexible problem formulation, reproducible benchmarks, and straightforward integration with external optimisers.
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 bundles everything needed for both simulation and the default
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 IPOPT, the default solver when present.
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**: for MILP/MIQCP problems such as MISOCP optimal power flow.
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 = mx.create_water_junction(net)
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(net, bus_1, j_mid, j_return,
105
- heat_energy_mw=0.1, diameter_m=0.1, efficiency=0.9)
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 = mx.create_bus(net)
134
- bus_b = mx.create_bus(net)
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) # cheap
142
- mx.create_power_generator(net, bus_b, p_mw=0.15, q_mvar=0.0, cost=40.0) # expensive
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, # include the slack so it can be bounded
146
- ext_grid_bounds=(-0.01, 0.01), # substation import/export limit (MW)
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(net, solver="gurobi", formulation="el_misocp", simulation=False)
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-3 * branch.length_m / branch.diameter_m**2)
220
+ branch.resistance = Const(1e-7 * branch.length_m / branch.diameter_m**2)
208
221
  branch.direction = Const(1)
209
- for unused in ("velocity_mps", "reynolds_scaled", "gas_density_kg_per_m3",
210
- "friction", "mass_flow_pos_kgs_squared",
211
- "mass_flow_neg_kgs_squared"):
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
- # the nodal balance consumes the positive / negative flow split
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
- net.apply_formulation(NetworkFormulation(
226
- branch_type_to_formulations={GasPipe: MyGasPipeFormulation()},
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 AttributeParameter, Constraints, Objectives, OptimizationProblem
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
- ("regulation", AttributeParameter(
250
- min=lambda attr, val: 0,
251
- max=lambda attr, val: 1,
252
- val=lambda attr, val: 1,
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 <= 0.6)
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)
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "monee"
7
- version = "2.0.0"
7
+ version = "2.2.0"
8
8
  authors = [
9
9
  { name="Rico Schrage", email="rico.schrage@uol.de" },
10
10
  ]
@@ -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 persist_solution, compute_bound_violations
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(net: mm.Network, solver=None, simulation: bool = True, **kwargs):
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
- **kwargs: Additional keyword arguments for solver configuration or analysis tuning. Refer to the solver's documentation for supported options.
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, optimization_problem: mp.OptimizationProblem, solver=None, **kwargs
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
- **kwargs: Additional keyword arguments for solver configuration or optimization tuning. Refer to the solver's documentation for supported options.
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( # NOSONAR
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 = (-3, 3),
208
- bounds_ext_gas: tuple = (-10, 10),
209
- bounds_ext_heat: tuple = (-10, 10),
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 (min, max).
246
- bounds_ext_gas (tuple): External gas-grid exchange bounds (min, max).
247
- bounds_ext_heat (tuple): External heat-grid exchange bounds (min, max).
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.