epyt-flow 0.9.0__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.
@@ -0,0 +1,134 @@
1
+ """
2
+ Module provides a base class for custom control modules.
3
+ """
4
+ from abc import abstractmethod, ABC
5
+ import warnings
6
+ import numpy as np
7
+ import epyt
8
+
9
+ from . import ScadaData
10
+
11
+
12
+ class CustomControlModule(ABC):
13
+ """
14
+ Base class for a custom control module.
15
+
16
+ Attributes
17
+ ----------
18
+ epanet_api : `epyt.epanet <https://epanet-python-toolkit-epyt.readthedocs.io/en/latest/api.html#epyt.epanet.epanet>`_
19
+ API to EPANET and EPANET-MSX. Is set in :func:`init`.
20
+ """
21
+ def __init__(self, **kwds):
22
+ self._epanet_api = None
23
+
24
+ super().__init__(**kwds)
25
+
26
+ def init(self, epanet_api: epyt.epanet) -> None:
27
+ """
28
+ Initializes the control module.
29
+
30
+ Parameters
31
+ ----------
32
+ epanet_api : `epyt.epanet <https://epanet-python-toolkit-epyt.readthedocs.io/en/latest/api.html#epyt.epanet.epanet>`_
33
+ API to EPANET for implementing the control module.
34
+ """
35
+ if not isinstance(epanet_api, epyt.epanet):
36
+ raise TypeError("'epanet_api' must be an instance of 'epyt.epanet' but not of " +
37
+ f"'{type(epanet_api)}'")
38
+
39
+ self._epanet_api = epanet_api
40
+
41
+ def set_pump_status(self, pump_id: str, status: int) -> None:
42
+ """
43
+ Sets the status of a pump.
44
+
45
+ Parameters
46
+ ----------
47
+ pump_id : `str`
48
+ ID of the pump for which the status is set.
49
+ status : `int`
50
+ New status of the pump -- either active (i.e. open) or inactive (i.e. closed).
51
+
52
+ Must be one of the following constants defined in
53
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
54
+
55
+ - EN_CLOSED = 0
56
+ - EN_OPEN = 1
57
+ """
58
+ pump_idx = self._epanet_api.getLinkPumpNameID().index(pump_id)
59
+ pump_link_idx = self._epanet_api.getLinkPumpIndex(pump_idx + 1)
60
+ self._epanet_api.setLinkStatus(pump_link_idx, status)
61
+
62
+ def set_pump_speed(self, pump_id: str, speed: float) -> None:
63
+ """
64
+ Sets the speed of a pump.
65
+
66
+ Parameters
67
+ ----------
68
+ pump_id : `str`
69
+ ID of the pump for which the pump speed is set.
70
+ speed : `float`
71
+ New pump speed.
72
+ """
73
+ pump_idx = self._epanet_api.getLinkPumpNameID().index(pump_id)
74
+ pattern_idx = self._epanet_api.getLinkPumpPatternIndex(pump_idx + 1)
75
+
76
+ if pattern_idx == 0:
77
+ warnings.warn(f"No pattern for pump '{pump_id}' found -- a new pattern is created")
78
+ pattern_idx = self._epanet_api.addPattern(f"pump_speed_{pump_id}")
79
+ self._epanet_api.setLinkPumpPatternIndex(pattern_idx)
80
+
81
+ self._epanet_api.setPattern(pattern_idx, np.array([speed]))
82
+
83
+ def set_valve_status(self, valve_id: str, status: int) -> None:
84
+ """
85
+ Sets the status of a valve.
86
+
87
+ Parameters
88
+ ----------
89
+ valve_id : `str`
90
+ ID of the valve for which the status is set.
91
+ status : `int`
92
+ New status of the valve -- either open or closed.
93
+
94
+ Must be one of the following constants defined in
95
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
96
+
97
+ - EN_CLOSED = 0
98
+ - EN_OPEN = 1
99
+ """
100
+ valve_idx = self._epanet_api.getLinkValveNameID().index(valve_id)
101
+ valve_link_idx = self._epanet_api.getLinkValveIndex()[valve_idx]
102
+ self._epanet_api.setLinkStatus(valve_link_idx, status)
103
+
104
+ def set_node_quality_source_value(self, node_id: str, pattern_id: str,
105
+ qual_value: float) -> None:
106
+ """
107
+ Sets the quality source at a particular node to a specific value -- e.g.
108
+ setting the chlorine concentration injection to a specified value.
109
+
110
+ Parameters
111
+ ----------
112
+ node_id : `str`
113
+ ID of the node.
114
+ pattern_id : `str`
115
+ ID of the quality pattern at the specific node.
116
+ qual_value : `float`
117
+ New quality source value.
118
+ """
119
+ node_idx = self._epanet_api.getNodeIndex(node_id)
120
+ pattern_idx = self._epanet_api.getPatternIndex(pattern_id)
121
+ self._epanet_api.setNodeSourceQuality(node_idx, 1)
122
+ self._epanet_api.setPattern(pattern_idx, np.array([qual_value]))
123
+
124
+ @abstractmethod
125
+ def step(self, scada_data: ScadaData) -> None:
126
+ """
127
+ Implements the control algorithm -- i.e. mapping of sensor reading to actions.
128
+
129
+ Parameters
130
+ ----------
131
+ scada_data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
132
+ Sensor readings.
133
+ """
134
+ raise NotImplementedError()
@@ -1314,7 +1314,7 @@ class ScadaData(Serializable):
1314
1314
  """
1315
1315
  return deepcopy(self.__pumps_efficiency_data_raw)
1316
1316
 
1317
- def __map_sensor_to_idx(self, sensor_type: int, sensor_id: str) -> int:
1317
+ def __map_sensor_to_idx(self, sensor_type: int, sensor_id: Union[str, tuple[str, str]]) -> int:
1318
1318
  if sensor_type == SENSOR_TYPE_NODE_PRESSURE:
1319
1319
  return self.__sensor_config.get_index_of_reading(pressure_sensor=sensor_id)
1320
1320
  elif sensor_type == SENSOR_TYPE_NODE_QUALITY:
@@ -1351,8 +1351,25 @@ class ScadaData(Serializable):
1351
1351
 
1352
1352
  self.__apply_sensor_reading_events = []
1353
1353
  for sensor_event in self.__sensor_reading_events:
1354
- idx = self.__map_sensor_to_idx(sensor_event.sensor_type, sensor_event.sensor_id)
1355
- self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
1354
+ sensor_id = sensor_event.sensor_id
1355
+ if sensor_event.sensor_type == SENSOR_TYPE_NODE_BULK_SPECIES:
1356
+ for species_id, node_sensors_id in self.__sensor_config.bulk_species_node_sensors.items():
1357
+ if sensor_id in node_sensors_id:
1358
+ idx = self.__map_sensor_to_idx(sensor_event.sensor_type, (species_id, sensor_id))
1359
+ self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
1360
+ elif sensor_event.sensor_type == SENSOR_TYPE_LINK_BULK_SPECIES:
1361
+ for species_id, link_sensors_id in self.__sensor_config.bulk_species_link_sensors.items():
1362
+ if sensor_id in link_sensors_id:
1363
+ idx = self.__map_sensor_to_idx(sensor_event.sensor_type, (species_id, sensor_id))
1364
+ self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
1365
+ elif sensor_event.sensor_type == SENSOR_TYPE_SURFACE_SPECIES:
1366
+ for species_id, link_sensors_id in self.__sensor_config.surface_species_sensors.items():
1367
+ if sensor_id in link_sensors_id:
1368
+ idx = self.__map_sensor_to_idx(sensor_event.sensor_type, (species_id, sensor_id))
1369
+ self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
1370
+ else:
1371
+ idx = self.__map_sensor_to_idx(sensor_event.sensor_type, sensor_event.sensor_id)
1372
+ self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
1356
1373
 
1357
1374
  self.__sensor_readings = None
1358
1375
 
@@ -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)