epyt-flow 0.8.1__py3-none-any.whl → 0.10.0__py3-none-any.whl

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 (50) hide show
  1. epyt_flow/VERSION +1 -1
  2. epyt_flow/__init__.py +1 -0
  3. epyt_flow/data/benchmarks/batadal.py +1 -1
  4. epyt_flow/data/benchmarks/battledim.py +4 -3
  5. epyt_flow/data/benchmarks/gecco_water_quality.py +4 -4
  6. epyt_flow/data/benchmarks/leakdb.py +7 -7
  7. epyt_flow/data/benchmarks/water_usage.py +2 -2
  8. epyt_flow/data/networks.py +1 -1
  9. epyt_flow/gym/control_gyms.py +2 -2
  10. epyt_flow/gym/scenario_control_env.py +9 -1
  11. epyt_flow/metrics.py +28 -28
  12. epyt_flow/models/sensor_interpolation_detector.py +3 -3
  13. epyt_flow/rest_api/base_handler.py +4 -4
  14. epyt_flow/rest_api/scada_data/data_handlers.py +11 -11
  15. epyt_flow/rest_api/scada_data/export_handlers.py +2 -2
  16. epyt_flow/rest_api/scada_data/handlers.py +9 -9
  17. epyt_flow/rest_api/scenario/event_handlers.py +6 -6
  18. epyt_flow/rest_api/scenario/handlers.py +15 -15
  19. epyt_flow/rest_api/scenario/simulation_handlers.py +7 -7
  20. epyt_flow/rest_api/scenario/uncertainty_handlers.py +6 -6
  21. epyt_flow/serialization.py +8 -2
  22. epyt_flow/simulation/events/actuator_events.py +1 -1
  23. epyt_flow/simulation/events/leakages.py +1 -1
  24. epyt_flow/simulation/events/quality_events.py +16 -5
  25. epyt_flow/simulation/events/sensor_reading_attack.py +17 -4
  26. epyt_flow/simulation/events/sensor_reading_event.py +21 -6
  27. epyt_flow/simulation/events/system_event.py +1 -1
  28. epyt_flow/simulation/parallel_simulation.py +1 -1
  29. epyt_flow/simulation/scada/__init__.py +3 -1
  30. epyt_flow/simulation/scada/advanced_control.py +8 -4
  31. epyt_flow/simulation/scada/complex_control.py +625 -0
  32. epyt_flow/simulation/scada/custom_control.py +134 -0
  33. epyt_flow/simulation/scada/scada_data.py +133 -130
  34. epyt_flow/simulation/scada/scada_data_export.py +1 -1
  35. epyt_flow/simulation/scada/simple_control.py +317 -0
  36. epyt_flow/simulation/scenario_config.py +124 -24
  37. epyt_flow/simulation/scenario_simulator.py +514 -49
  38. epyt_flow/simulation/scenario_visualizer.py +9 -9
  39. epyt_flow/simulation/sensor_config.py +38 -28
  40. epyt_flow/topology.py +2 -2
  41. epyt_flow/uncertainty/model_uncertainty.py +624 -147
  42. epyt_flow/uncertainty/sensor_noise.py +94 -19
  43. epyt_flow/uncertainty/uncertainties.py +4 -4
  44. epyt_flow/uncertainty/utils.py +7 -7
  45. epyt_flow/utils.py +9 -8
  46. {epyt_flow-0.8.1.dist-info → epyt_flow-0.10.0.dist-info}/LICENSE +1 -1
  47. {epyt_flow-0.8.1.dist-info → epyt_flow-0.10.0.dist-info}/METADATA +7 -6
  48. {epyt_flow-0.8.1.dist-info → epyt_flow-0.10.0.dist-info}/RECORD +50 -47
  49. {epyt_flow-0.8.1.dist-info → epyt_flow-0.10.0.dist-info}/WHEEL +1 -1
  50. {epyt_flow-0.8.1.dist-info → epyt_flow-0.10.0.dist-info}/top_level.txt +0 -0
@@ -102,7 +102,7 @@ class ScadaDataExport():
102
102
 
103
103
  Returns
104
104
  -------
105
- `numpy.ndarray`
105
+ `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
106
106
  3-dimensional array describing all columns of the sensor readings:
107
107
  The first dimension describes the sensor type, the second dimension
108
108
  describes the sensor location, and the third one describes the measurement units.
@@ -0,0 +1,317 @@
1
+ """
2
+ The module contains classes for representing simple control rules as used in EPANET.
3
+ """
4
+ from typing import Union
5
+ from epyt.epanet import ToolkitConstants
6
+
7
+ from ..events import ActuatorConstants
8
+ from ...serialization import JsonSerializable, SIMPLE_CONTROL_ID, serializable
9
+
10
+
11
+ @serializable(SIMPLE_CONTROL_ID, ".epytflow_simple_control")
12
+ class SimpleControlModule(JsonSerializable):
13
+ """
14
+ A class for representing a simple EPANET control rule.
15
+
16
+ Parameters
17
+ ----------
18
+ link_id : `str`
19
+ Link ID.
20
+ link_status : `int` or `float`
21
+ Status of the link that is set when the condition is fullfilled.
22
+
23
+ Instance of `float` if the link constitutes a pump -- in this case,
24
+ the argument corresponds to the pump speed.
25
+
26
+ Instance of `int` if the link constitutes a valve -- in this case,
27
+ must be one of the followig costants defined in
28
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
29
+
30
+ - EN_CLOSED = 0
31
+ - EN_OPEN = 1
32
+ cond_type : `int`
33
+ Condition/Rule type.
34
+
35
+ Must be one of the following EPANET constants:
36
+
37
+ - EN_LOWLEVEL = 0
38
+ - EN_HILEVEL = 1
39
+ - EN_TIMER = 2
40
+ - EN_TIMEOFDAY = 3
41
+ cond_var_value : `str` or `int`
42
+ Condition/Rule variable value.
43
+
44
+ Node ID in the cases of EN_LOWLEVEL or EN_HILEVEL.
45
+ Time of the day (in AM/PM format) in the case of EN_TIMEOFDAY.
46
+ Number of hours (as an integer) since simulation start in the case of EN_TIMER.
47
+ cond_comp_value : `float`
48
+ The condition/rule comparison value at which this control rule is triggered.
49
+
50
+ Lower or upper value on the pressure (or tank level) in the cases of
51
+ EN_LOWLEVEL and EN_HILEVEL.
52
+
53
+ Will be ignored in all other cases -- i.e. should be set to None.
54
+ """
55
+ def __init__(self, link_id: str, link_status: Union[int, float], cond_type: int,
56
+ cond_var_value: Union[str, int], cond_comp_value: float,
57
+ **kwds):
58
+ if not isinstance(link_id, str):
59
+ raise TypeError(f"'link_id' must be an instance of 'str' but not of '{type(link_id)}'")
60
+ if isinstance(link_status, int):
61
+ if link_status not in [ActuatorConstants.EN_OPEN, ActuatorConstants.EN_CLOSED]:
62
+ raise ValueError(f"Invalid link status {link_status} in 'link_status'")
63
+ elif isinstance(link_status, float):
64
+ if link_status < 0:
65
+ raise TypeError("'link_status' can not be negative")
66
+ else:
67
+ raise TypeError("'link_status' must be an instance of 'int' or 'float' but not " +
68
+ f"of '{type(link_status)}'")
69
+ if cond_type not in [ToolkitConstants.EN_TIMEOFDAY, ToolkitConstants.EN_TIMER,
70
+ ToolkitConstants.EN_LOWLEVEL, ToolkitConstants.EN_HILEVEL]:
71
+ raise ValueError(f"Invalid control type '{cond_type}' in 'cond_type'")
72
+
73
+ if cond_type == ToolkitConstants.EN_TIMEOFDAY:
74
+ if not isinstance(cond_var_value, str):
75
+ raise TypeError("EN_TIMEOFDAY requires that 'cond_var_value' must be an instance " +
76
+ f"of 'str' but not of '{type(cond_var_value)}'")
77
+ if not cond_var_value.endswith("AM") and not cond_var_value.endswith("PM"):
78
+ raise ValueError(f"Invalid time of day format '{cond_var_value}' in " +
79
+ "'cond_var_value'")
80
+ elif cond_type == ToolkitConstants.EN_TIMER:
81
+ if not isinstance(cond_var_value, int):
82
+ raise TypeError("EN_TIMER requires that 'cond_var_value' must be an instance " +
83
+ f"of 'int' but not of '{type(cond_var_value)}'")
84
+ if cond_var_value < 0:
85
+ raise ValueError("'cond_var_value' can not be negative")
86
+ else:
87
+ if not isinstance(cond_var_value, str):
88
+ raise TypeError("'cond_var_value' must be an instance of 'str' but " +
89
+ f"not of '{type(cond_var_value)}'")
90
+ if not isinstance(cond_comp_value, float):
91
+ raise TypeError("'cond_comp_value' must be an instance of 'float' " +
92
+ f"but not of '{type(cond_comp_value)}'")
93
+ if cond_comp_value < 0:
94
+ raise ValueError("'cond_comp_value' can not be negative")
95
+
96
+ self.__link_id = link_id
97
+ self.__link_status = link_status
98
+ self.__cond_type = cond_type
99
+ self.__cond_var_value = cond_var_value
100
+ self.__cond_comp_value = cond_comp_value
101
+
102
+ super().__init__(**kwds)
103
+
104
+ @property
105
+ def link_id(self) -> str:
106
+ """
107
+ Returns the link ID.
108
+
109
+ Returns
110
+ -------
111
+ `str`
112
+ Link ID.
113
+ """
114
+ return self.__link_id
115
+
116
+ @property
117
+ def link_status(self) -> Union[int, float]:
118
+ """
119
+ Returns the link status that is set when the condition is fullfilled.
120
+
121
+ Returns
122
+ -------
123
+ `int` or `float`
124
+ Pump speed if the link is a pump, otherwise one of the followig costants defined in
125
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
126
+
127
+ - EN_CLOSED = 0
128
+ - EN_OPEN = 1
129
+ """
130
+ return self.__link_status
131
+
132
+ @property
133
+ def cond_type(self) -> int:
134
+ """
135
+ Returns the condition/rule type.
136
+
137
+ Returns
138
+ -------
139
+ `int`
140
+ Condition/Rule type -- will be one of the following EPANET constants:
141
+
142
+ - EN_LOWLEVEL = 0
143
+ - EN_HILEVEL = 1
144
+ - EN_TIMER = 2
145
+ - EN_TIMEOFDAY = 3
146
+ """
147
+ return self.__cond_type
148
+
149
+ @property
150
+ def cond_var_value(self) -> Union[str, int]:
151
+ """
152
+ Return the condition/rule variable value.
153
+
154
+ Node ID in the cases of EN_LOWLEVEL or EN_HILEVEL.
155
+ Time of the day (in AM/PM format) in the case of EN_TIMEOFDAY.
156
+ Number of hours (as an integer) since simulation start in the case of EN_TIMER.
157
+
158
+ Returns
159
+ -------
160
+ `str` or `int`
161
+ Condition/rule variable value.
162
+ """
163
+ return self.__cond_var_value
164
+
165
+ @property
166
+ def cond_comp_value(self) -> float:
167
+ """
168
+ Returns the condition/rule comparison value -- might be None if not needed.
169
+
170
+ Lower or upper value on the pressure (or tank level) in the cases of
171
+ EN_LOWLEVEL and EN_HILEVEL.
172
+
173
+ Returns
174
+ -------
175
+ `float`
176
+ Condition/Rule comparison value.
177
+ """
178
+ return self.__cond_comp_value
179
+
180
+ def get_attributes(self) -> dict:
181
+ return super().get_attributes() | {"link_id": self.__link_id,
182
+ "link_status": self.__link_status,
183
+ "cond_type": self.__cond_type,
184
+ "cond_var_value": self.__cond_var_value,
185
+ "cond_comp_value": self.__cond_comp_value}
186
+
187
+ def __eq__(self, other) -> bool:
188
+ return super().__eq__(other) and self.__link_id == other.link_id and \
189
+ self.__link_status == other.link_status and self.__cond_type == other.cond_type and \
190
+ self.__cond_var_value == other.cond_var_value and \
191
+ self.__cond_comp_value == other.cond_comp_value
192
+
193
+ def __str__(self) -> str:
194
+ control_rule_str = f"LINK {self.__link_id} "
195
+ if isinstance(self.__link_status, int):
196
+ control_rule_str += "OPEN " if self.__link_status == ActuatorConstants.EN_OPEN \
197
+ else "CLOSED "
198
+ else:
199
+ control_rule_str += f"{self.__link_status} "
200
+
201
+ if self.__cond_type == ToolkitConstants.EN_TIMER:
202
+ control_rule_str += f"AT TIME {self.__cond_var_value}"
203
+ elif self.__cond_type == ToolkitConstants.EN_TIMEOFDAY:
204
+ control_rule_str += f"AT CLOCKTIME {self.__cond_var_value}"
205
+ elif self.__cond_type == ToolkitConstants.EN_LOWLEVEL:
206
+ control_rule_str += f"IF NODE {self.__cond_var_value} BELOW {self.__cond_comp_value}"
207
+ elif self.__cond_type == ToolkitConstants.EN_HILEVEL:
208
+ control_rule_str += f"IF NODE {self.__cond_var_value} ABOVE {self.__cond_comp_value}"
209
+
210
+ return control_rule_str
211
+
212
+
213
+ class SimplePumpSpeedTimeControl(SimpleControlModule):
214
+ """
215
+ A class for representing a simple control rule for setting the pump speed at some point in time.
216
+
217
+ Parameters
218
+ ----------
219
+ pump_id : `str`
220
+ Pump ID.
221
+ pump_speed : `float`
222
+ Pump speed.
223
+ time : `str` or `int`
224
+ Time of the day (in AM/PM format) in the case or
225
+ number of hours (as an integer) since simulation start.
226
+ """
227
+ def __init__(self, pump_id: str, pump_speed: float, time: Union[str, int]):
228
+ super().__init__(link_id=pump_id, link_status=pump_speed,
229
+ cond_type=ToolkitConstants.EN_TIMER if isinstance(time, int)
230
+ else ToolkitConstants.EN_TIMEOFDAY,
231
+ cond_var_value=time, cond_comp_value=None)
232
+
233
+
234
+ class SimplePumpSpeedConditionControl(SimpleControlModule):
235
+ """
236
+ A class for representing a simple IF-THEN control rule for setting the pump speed.
237
+
238
+ Parameters
239
+ ----------
240
+ Parameters
241
+ ----------
242
+ pump_id : `str`
243
+ Pump ID.
244
+ pump_speed : `float`
245
+ Pump speed.
246
+ node_id : `str`
247
+ Node ID.
248
+ comp_type : `int`
249
+ Comparison type -- must be one of the following EPANET constants:
250
+
251
+ - EN_LOWLEVEL = 0
252
+ - EN_HILEVEL = 1
253
+ comp_value : `float`:
254
+ Lower or upper value on the pressure (or tank level) at which this
255
+ control rule is triggered.
256
+ """
257
+ def __init__(self, pump_id: str, pump_speed: float, node_id: str, comp_type: int,
258
+ comp_value: float):
259
+ super().__init__(link_id=pump_id, link_status=pump_speed, cond_type=comp_type,
260
+ cond_var_value=node_id, cond_comp_value=comp_value)
261
+
262
+
263
+ class SimpleValveTimeControl(SimpleControlModule):
264
+ """
265
+ A class for representing a simple control rule for setting the valve status
266
+ at some point in time.
267
+
268
+ Parameters
269
+ ----------
270
+ valve_id : `str`
271
+ valve ID.
272
+ valve_status : `int`
273
+ Valve status -- must be one of the followig costants defined in
274
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
275
+
276
+ - EN_CLOSED = 0
277
+ - EN_OPEN = 1
278
+ time : `str` or `int`
279
+ Time of the day (in AM/PM format) in the case or
280
+ number of hours (as an integer) since simulation start.
281
+ """
282
+ def __init__(self, valve_id: str, valve_status: int, time: Union[str, int]):
283
+ super().__init__(link_id=valve_id, link_status=valve_status,
284
+ cond_type=ToolkitConstants.EN_TIMER if isinstance(time, int)
285
+ else ToolkitConstants.EN_TIMEOFDAY,
286
+ cond_var_value=time, cond_comp_value=None)
287
+
288
+
289
+ class SimpleValveConditionControl(SimpleControlModule):
290
+ """
291
+ A class for representing a simple IF-THEN control rule for setting the valve status.
292
+
293
+ Parameters
294
+ ----------
295
+ valve_id : `str`
296
+ valve ID.
297
+ valve_status : `int`
298
+ Valve status -- must be one of the followig costants defined in
299
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
300
+
301
+ - EN_CLOSED = 0
302
+ - EN_OPEN = 1
303
+ node_id : `str`
304
+ Node ID.
305
+ comp_type : `int`
306
+ Comparison type -- must be one of the following EPANET constants:
307
+
308
+ - EN_LOWLEVEL = 0
309
+ - EN_HILEVEL = 1
310
+ comp_value : `float`:
311
+ Lower or upper value on the pressure (or tank level) at which this
312
+ control rule is triggered.
313
+ """
314
+ def __init__(self, valve_id: str, valve_status: int, node_id: str, comp_type: int,
315
+ comp_value: float):
316
+ super().__init__(link_id=valve_id, link_status=valve_status, cond_type=comp_type,
317
+ cond_var_value=node_id, cond_comp_value=comp_value)
@@ -3,16 +3,17 @@ Module provides a class for specifying scenario configurations.
3
3
  """
4
4
  from typing import Any
5
5
  from copy import deepcopy
6
+ import warnings
6
7
  import os
7
8
  import json
8
- import numpy as np
9
9
  from pathlib import Path
10
+ import numpy as np
10
11
 
11
12
  from ..uncertainty import AbsoluteGaussianUncertainty, RelativeGaussianUncertainty, \
12
13
  AbsoluteUniformUncertainty, RelativeUniformUncertainty, ModelUncertainty, \
13
14
  SensorNoise, Uncertainty
14
15
  from .sensor_config import SensorConfig
15
- from .scada import AdvancedControlModule
16
+ from .scada import CustomControlModule, SimpleControlModule, ComplexControlModule
16
17
  from .events import SystemEvent, SensorReadingEvent
17
18
  from .events.sensor_faults import SensorFaultConstant, SensorFaultDrift, SensorFaultGaussian, \
18
19
  SensorFaultPercentage, SensorFaultStuckZero
@@ -63,8 +64,16 @@ class ScenarioConfig(Serializable):
63
64
  Speciation of sensor noise -- i.e. noise/uncertainty affecting the sensor readings.
64
65
 
65
66
  The default is None
66
- controls : list[:class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`], optional
67
- List of control modules that are active during the simulation.
67
+ csutom_controls : list[:class:`~epyt_flow.simulation.scada.custom_control.CustomControlModule`], optional
68
+ List of custom control modules that are active during the simulation.
69
+
70
+ The default is an empty list.
71
+ simple_controls : list[:class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`], optional
72
+ List of EPANET control rules that are active during the simulation.
73
+
74
+ The default is an empty list.
75
+ complex_controls : list[:class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`], optional
76
+ List of complex (i.e. IF-THEN-ELSE) EPANET control rules that are active during the simulation.
68
77
 
69
78
  The default is an empty list.
70
79
  model_uncertainty : :class:`~epyt_flow.uncertainty.model_uncertainty.ModelUncertainty`, optional
@@ -82,11 +91,22 @@ class ScenarioConfig(Serializable):
82
91
  def __init__(self, scenario_config: Any = None, f_inp_in: str = None, f_msx_in: str = None,
83
92
  general_params: dict = None, sensor_config: SensorConfig = None,
84
93
  memory_consumption_estimate: float = None,
85
- controls: list[AdvancedControlModule] = [],
94
+ controls: list = None,
95
+ advanced_controls: list = None,
96
+ custom_controls: list[CustomControlModule] = [],
97
+ simple_controls: list[SimpleControlModule] = [],
98
+ complex_controls: list[ComplexControlModule] = [],
86
99
  sensor_noise: SensorNoise = None,
87
100
  model_uncertainty: ModelUncertainty = None,
88
101
  system_events: list[SystemEvent] = [],
89
102
  sensor_reading_events: list[SensorReadingEvent] = [], **kwds):
103
+ if controls is not None:
104
+ warnings.warn("'controls' is deprecated and will be removed in future releases")
105
+ advanced_controls = controls
106
+ if advanced_controls is not None:
107
+ warnings.warn("'advanced_controls' is deprecated and will be removed in " +
108
+ "future releases -- use 'custom_controls' instead")
109
+
90
110
  if f_inp_in is None and scenario_config is None:
91
111
  raise ValueError("Either 'f_inp_in' or 'scenario_config' must be given")
92
112
  if scenario_config is not None:
@@ -115,14 +135,32 @@ class ScenarioConfig(Serializable):
115
135
  if not isinstance(memory_consumption_estimate, float) or \
116
136
  memory_consumption_estimate <= 0:
117
137
  raise ValueError("'memory_consumption_estimate' must be a positive integer")
118
- if not isinstance(controls, list):
119
- raise TypeError("'controls' must be an instance of " +
120
- "'list[epyt_flow.simulation.scada.AdvancedControlModule]' but no of " +
121
- f"'{type(controls)}'")
122
- if len(controls) != 0:
123
- if any(not isinstance(c, AdvancedControlModule) for c in controls):
124
- raise TypeError("Each item in 'controls' must be an instance of " +
138
+ if advanced_controls is not None:
139
+ if not isinstance(advanced_controls, list):
140
+ raise TypeError("'advanced_controls' must be an instance of " +
141
+ "'list[epyt_flow.simulation.scada.AdvancedControlModule]' but no of " +
142
+ f"'{type(advanced_controls)}'")
143
+
144
+ from .scada.advanced_control import AdvancedControlModule
145
+ if any(not isinstance(c, AdvancedControlModule) for c in advanced_controls):
146
+ raise TypeError("Each item in 'advanced_controls' must be an instance of " +
125
147
  "'epyt_flow.simulation.scada.AdvancedControlModule'")
148
+ if len(custom_controls) != 0:
149
+ if any(not isinstance(c, CustomControlModule) for c in custom_controls):
150
+ raise TypeError("Each item in 'custom_controls' must be an instance of " +
151
+ "'epyt_flow.simulation.scada.CustomControlModule'")
152
+ if not isinstance(simple_controls, list):
153
+ raise TypeError("'simple_controls' must be an instance of " +
154
+ "'list[epyt_flow.simulation.scada.SimpleControlModule]' but no of " +
155
+ f"'{type(simple_controls)}'")
156
+ if len(simple_controls) != 0:
157
+ if any(not isinstance(c, SimpleControlModule) for c in simple_controls):
158
+ raise TypeError("Each item in 'simple_controls' must be an instance of " +
159
+ "'epyt_flow.simulation.scada.SimppleControlModule'")
160
+ if len(complex_controls) != 0:
161
+ if any(not isinstance(c, ComplexControlModule) for c in complex_controls):
162
+ raise TypeError("Each item in 'complex_controls' must be an instance of " +
163
+ "'epyt_flow.simulation.scada.ComplexControlModule'")
126
164
  if sensor_noise is not None:
127
165
  if not isinstance(sensor_noise, SensorNoise):
128
166
  raise TypeError("'sensor_noise' must be an instance of " +
@@ -169,10 +207,25 @@ class ScenarioConfig(Serializable):
169
207
  else:
170
208
  self.__memory_consumption_estimate = memory_consumption_estimate
171
209
 
172
- if len(controls) == 0:
173
- self.__controls = scenario_config.controls
210
+ self.__advanced_controls = advanced_controls
211
+ if advanced_controls is not None:
212
+ if len(advanced_controls) == 0:
213
+ self.__advanced_controls = scenario_config.advanced_controls
214
+
215
+ if len(custom_controls) == 0:
216
+ self.__custom_controls = scenario_config.custom_controls
174
217
  else:
175
- self.__controls = controls
218
+ self.__custom_controls = custom_controls
219
+
220
+ if len(simple_controls) == 0:
221
+ self.__simple_controls = scenario_config.simple_controls
222
+ else:
223
+ self.__simple_controls = simple_controls
224
+
225
+ if len(complex_controls) == 0:
226
+ self.__complex_controls = scenario_config.complex_controls
227
+ else:
228
+ self.__complex_controls = complex_controls
176
229
 
177
230
  if sensor_noise is None:
178
231
  self.__sensor_noise = scenario_config.sensor_noise
@@ -199,7 +252,10 @@ class ScenarioConfig(Serializable):
199
252
  self.__general_params = general_params
200
253
  self.__sensor_config = sensor_config
201
254
  self.__memory_consumption_estimate = memory_consumption_estimate
202
- self.__controls = controls
255
+ self.__advanced_controls = advanced_controls
256
+ self.__custom_controls = custom_controls
257
+ self.__simple_controls = simple_controls
258
+ self.__complex_controls = complex_controls
203
259
  self.__sensor_noise = sensor_noise
204
260
  self.__system_events = system_events
205
261
  self.__sensor_reading_events = sensor_reading_events
@@ -286,16 +342,53 @@ class ScenarioConfig(Serializable):
286
342
  return self.__memory_consumption_estimate
287
343
 
288
344
  @property
289
- def controls(self) -> list[AdvancedControlModule]:
345
+ def advanced_controls(self) -> list:
290
346
  """
291
- Gets the list of all control modules that are active during the simulation.
347
+ Gets the list of all (advanced) control modules that are active during the simulation.
292
348
 
293
349
  Returns
294
350
  -------
295
351
  list[:class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`]
296
352
  List of all control modules that are active during the simulation.
297
353
  """
298
- return deepcopy(self.__controls)
354
+ return deepcopy(self.__advanced_controls)
355
+
356
+ @property
357
+ def custom_controls(self) -> list[CustomControlModule]:
358
+ """
359
+ Returns the list of all custom control modules that are active during the simulation.
360
+
361
+ Returns
362
+ -------
363
+ list[:class:`~epyt_flow.simulation.scada.custom_control.CustomControlModule`]
364
+ List of all custom control modules that are active during the simulation.
365
+ """
366
+ return deepcopy(self.__custom_controls)
367
+
368
+ @property
369
+ def simple_controls(self) -> list[SimpleControlModule]:
370
+ """
371
+ Gets the list of all EPANET control rules that are active during the simulation.
372
+
373
+ Returns
374
+ -------
375
+ list[:class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`]
376
+ List of all EPANET control rules that are active during the simulation.
377
+ """
378
+ return deepcopy(self.__simple_controls)
379
+
380
+ @property
381
+ def complex_controls(self) -> list[ComplexControlModule]:
382
+ """
383
+ Gets the list of all complex (i.e. IF-THEN-ELSE) EPANET control rules
384
+ that are active during the simulation.
385
+
386
+ Returns
387
+ -------
388
+ list[:class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`]
389
+ List of all complex EPANET control rules that are active during the simulation.
390
+ """
391
+ return deepcopy(self.__complex_controls)
299
392
 
300
393
  @property
301
394
  def sensor_noise(self) -> SensorNoise:
@@ -350,7 +443,9 @@ class ScenarioConfig(Serializable):
350
443
  "general_params": self.__general_params,
351
444
  "sensor_config": self.__sensor_config,
352
445
  "memory_consumption_estimate": self.__memory_consumption_estimate,
353
- "controls": self.__controls,
446
+ "custom_controls": self.__custom_controls,
447
+ "simple_controls": self.__simple_controls,
448
+ "complex_controls": self.__complex_controls,
354
449
  "sensor_noise": self.__sensor_noise,
355
450
  "model_uncertainty": self.__model_uncertainty,
356
451
  "system_events": self.__system_events,
@@ -367,7 +462,10 @@ class ScenarioConfig(Serializable):
367
462
  and self.__general_params == other.general_params \
368
463
  and self.__memory_consumption_estimate == other.memory_consumption_estimate \
369
464
  and self.__sensor_config == other.sensor_config \
370
- and np.all(self.__controls == other.controls) \
465
+ and np.all(self.__advanced_controls == other.advanced_controls) \
466
+ and np.all(self.__custom_controls == other.custom_controls) \
467
+ and np.all(self.__simple_controls == other.simple_controls) \
468
+ and np.all(self.__complex_controls == other.complex_controls) \
371
469
  and self.__model_uncertainty == other.model_uncertainty \
372
470
  and np.all(self.__system_events == other.system_events) \
373
471
  and np.all(self.__sensor_reading_events == other.sensor_reading_events)
@@ -376,8 +474,10 @@ class ScenarioConfig(Serializable):
376
474
  return f"f_inp_in: {self.f_inp_in} f_msx_in: {self.f_msx_in} " + \
377
475
  f"general_params: {self.general_params} sensor_config: {self.sensor_config} " + \
378
476
  f"memory_consumption_estimate: {self.memory_consumption_estimate} " + \
379
- f"controls: {self.controls} sensor_noise: {self.sensor_noise} " + \
380
- f"model_uncertainty: {self.model_uncertainty} " + \
477
+ f"advanced_controls: {self.advanced_controls} simple_controls: {self.simple_controls} " + \
478
+ f"complex_controls: {self.__complex_controls} " + \
479
+ f"custom_controls: {self.__custom_controls}" + \
480
+ f"sensor_noise: {self.sensor_noise} model_uncertainty: {self.model_uncertainty} " + \
381
481
  f"system_events: {','.join(map(str, self.system_events))} " + \
382
482
  f"sensor_reading_events: {','.join(map(str, self.sensor_reading_events))}"
383
483
 
@@ -614,6 +714,6 @@ class ScenarioConfig(Serializable):
614
714
 
615
715
  # Create final scenario configuration
616
716
  return ScenarioConfig(f_inp_in=f_inp_in, f_msx_in=f_msx_in, general_params=general_params,
617
- sensor_config=sensor_config, controls=[], sensor_noise=sensor_noise,
717
+ sensor_config=sensor_config, sensor_noise=sensor_noise,
618
718
  model_uncertainty=model_uncertainty, system_events=leakages,
619
719
  sensor_reading_events=sensor_faults)