epyt-flow 0.9.0__py3-none-any.whl → 0.11.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.
- epyt_flow/VERSION +1 -1
- epyt_flow/data/networks.py +27 -14
- epyt_flow/gym/control_gyms.py +8 -0
- epyt_flow/gym/scenario_control_env.py +17 -4
- epyt_flow/metrics.py +5 -0
- epyt_flow/models/event_detector.py +5 -0
- epyt_flow/models/sensor_interpolation_detector.py +5 -0
- epyt_flow/serialization.py +5 -0
- epyt_flow/simulation/__init__.py +0 -1
- epyt_flow/simulation/events/actuator_events.py +7 -1
- epyt_flow/simulation/events/sensor_reading_attack.py +16 -3
- epyt_flow/simulation/events/sensor_reading_event.py +18 -3
- epyt_flow/simulation/scada/__init__.py +3 -1
- epyt_flow/simulation/scada/advanced_control.py +6 -2
- epyt_flow/simulation/scada/complex_control.py +625 -0
- epyt_flow/simulation/scada/custom_control.py +134 -0
- epyt_flow/simulation/scada/scada_data.py +547 -8
- epyt_flow/simulation/scada/simple_control.py +317 -0
- epyt_flow/simulation/scenario_config.py +87 -26
- epyt_flow/simulation/scenario_simulator.py +865 -51
- epyt_flow/simulation/sensor_config.py +34 -2
- epyt_flow/topology.py +16 -0
- epyt_flow/uncertainty/model_uncertainty.py +80 -62
- epyt_flow/uncertainty/sensor_noise.py +15 -4
- epyt_flow/uncertainty/uncertainties.py +71 -18
- epyt_flow/uncertainty/utils.py +40 -13
- epyt_flow/utils.py +15 -1
- epyt_flow/visualization/__init__.py +2 -0
- epyt_flow/{simulation → visualization}/scenario_visualizer.py +429 -586
- epyt_flow/visualization/visualization_utils.py +611 -0
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.11.0.dist-info}/LICENSE +1 -1
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.11.0.dist-info}/METADATA +18 -6
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.11.0.dist-info}/RECORD +35 -30
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.11.0.dist-info}/WHEEL +1 -1
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.11.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module contains a class for representing complex control rules as implemented in EPANET.
|
|
3
|
+
"""
|
|
4
|
+
from copy import deepcopy
|
|
5
|
+
from typing import Any
|
|
6
|
+
import numpy as np
|
|
7
|
+
from epyt.epanet import ToolkitConstants
|
|
8
|
+
|
|
9
|
+
from ...serialization import JsonSerializable, COMPLEX_CONTROL_ID, COMPLEX_CONTROL_CONDITION_ID, \
|
|
10
|
+
COMPLEX_CONTROL_ACTION_ID, serializable
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
EN_R_AND = 2
|
|
14
|
+
EN_R_OR = 3
|
|
15
|
+
|
|
16
|
+
EN_R_DEMAND = 1
|
|
17
|
+
EN_R_HEAD = 2
|
|
18
|
+
EN_R_LEVEL = 3
|
|
19
|
+
EN_R_PRESSURE = 4
|
|
20
|
+
EN_R_FLOW = 5
|
|
21
|
+
EN_R_STATUS = 6
|
|
22
|
+
EN_R_SETTING = 7
|
|
23
|
+
EN_R_POWER = 8
|
|
24
|
+
EN_R_TIME = 9
|
|
25
|
+
EN_R_CLOCKTIME = 10
|
|
26
|
+
EN_R_FILLTIME = 11
|
|
27
|
+
EN_R_DRAINTIME = 12
|
|
28
|
+
|
|
29
|
+
EN_R_EQ = 0
|
|
30
|
+
EN_R_NEQ = 1
|
|
31
|
+
EN_R_LEQ = 2
|
|
32
|
+
EN_R_GEQ = 3
|
|
33
|
+
EN_R_LESS = 4
|
|
34
|
+
EN_R_GREATER = 5
|
|
35
|
+
EN_R_IS = 6
|
|
36
|
+
EN_R_NOT = 7
|
|
37
|
+
EN_R_BELOW = 8
|
|
38
|
+
EN_R_ABOVE = 9
|
|
39
|
+
|
|
40
|
+
EN_R_ACTION_SETTING = -1
|
|
41
|
+
EN_R_ACTION_STATUS_OPEN = 1
|
|
42
|
+
EN_R_ACTION_STATUS_CLOSED = 2
|
|
43
|
+
EN_R_ACTION_STATUS_ACTIVE = 3
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@serializable(COMPLEX_CONTROL_CONDITION_ID, ".epytflow_complex_control_condition")
|
|
47
|
+
class RuleCondition(JsonSerializable):
|
|
48
|
+
"""
|
|
49
|
+
Class representing a rule condition.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
object_type_id : `int`
|
|
54
|
+
ID of the object type.
|
|
55
|
+
|
|
56
|
+
Must be one of the following EPANET constants:
|
|
57
|
+
|
|
58
|
+
- EN_R_NODE = 6
|
|
59
|
+
- EN_R_LINK = 7
|
|
60
|
+
- EN_R_SYSTEM = 8
|
|
61
|
+
object_id : `str`
|
|
62
|
+
ID of the object (i.e. junction, pipe, link, tank, etc.).
|
|
63
|
+
attribute_id : `int`
|
|
64
|
+
Type ID of the object's attribute that is checked.
|
|
65
|
+
|
|
66
|
+
Must be on of the following constants:
|
|
67
|
+
|
|
68
|
+
- EN_R_DEMAND = 1
|
|
69
|
+
- EN_R_HEAD = 2
|
|
70
|
+
- EN_R_LEVEL = 3
|
|
71
|
+
- EN_R_PRESSURE = 4
|
|
72
|
+
- EN_R_FLOW = 5
|
|
73
|
+
- EN_R_STATUS = 6
|
|
74
|
+
- EN_R_SETTING = 7
|
|
75
|
+
- EN_R_TIME = 9
|
|
76
|
+
- EN_R_CLOCKTIME = 10
|
|
77
|
+
- EN_R_FILLTIME = 11
|
|
78
|
+
- EN_R_DRAINTIME = 12
|
|
79
|
+
relation_type_id : `int`
|
|
80
|
+
ID of the type of comparison.
|
|
81
|
+
|
|
82
|
+
Must be one of the following constants:
|
|
83
|
+
|
|
84
|
+
- EN_R_EQ = 0
|
|
85
|
+
- EN_R_NEQ = 1
|
|
86
|
+
- EN_R_LEQ = 2
|
|
87
|
+
- EN_R_GEQ = 3
|
|
88
|
+
- EN_R_LESS = 4
|
|
89
|
+
- EN_R_GREATER = 5
|
|
90
|
+
- EN_R_IS = 6
|
|
91
|
+
- EN_R_NOT = 7
|
|
92
|
+
- EN_R_BELOW = 8
|
|
93
|
+
- EN_R_ABOVE = 9
|
|
94
|
+
value : `Any`
|
|
95
|
+
Value that is compared against.
|
|
96
|
+
"""
|
|
97
|
+
def __init__(self, object_type_id: int, object_id: str, attribute_id: int,
|
|
98
|
+
relation_type_id: int, value: Any, **kwds):
|
|
99
|
+
if not isinstance(object_type_id, int):
|
|
100
|
+
raise TypeError("'object_type_id' must be an instance of 'int' " +
|
|
101
|
+
f"but not of '{type(object_type_id)}'")
|
|
102
|
+
if object_type_id not in [ToolkitConstants.EN_R_NODE, ToolkitConstants.EN_R_LINK,
|
|
103
|
+
ToolkitConstants.EN_R_SYSTEM]:
|
|
104
|
+
raise ValueError(f"Invalid value '{object_type_id}' for 'object_type_id'")
|
|
105
|
+
if not isinstance(object_id, str):
|
|
106
|
+
raise TypeError("'object_id' must be an instance of 'str' " +
|
|
107
|
+
f"but not of '{type(object_id)}'")
|
|
108
|
+
if not isinstance(attribute_id, int):
|
|
109
|
+
raise TypeError("'attribute_id' must be an instance of 'int' " +
|
|
110
|
+
f"but not of '{type(attribute_id)}'")
|
|
111
|
+
if attribute_id not in [EN_R_DEMAND, EN_R_HEAD, EN_R_LEVEL, EN_R_PRESSURE,
|
|
112
|
+
EN_R_FLOW, EN_R_STATUS, EN_R_SETTING, EN_R_POWER, EN_R_TIME,
|
|
113
|
+
EN_R_CLOCKTIME, EN_R_FILLTIME, EN_R_DRAINTIME]:
|
|
114
|
+
raise ValueError(f"Invalid value '{attribute_id}' for 'attribute_id'")
|
|
115
|
+
if not isinstance(relation_type_id, int):
|
|
116
|
+
raise TypeError("'relation_type_id' must be an instance of 'int' " +
|
|
117
|
+
f"but not of '{type(relation_type_id)}'")
|
|
118
|
+
if relation_type_id not in [EN_R_EQ, EN_R_NEQ, EN_R_LEQ, EN_R_GEQ, EN_R_LESS, EN_R_GREATER,
|
|
119
|
+
EN_R_IS, EN_R_NOT, EN_R_BELOW, EN_R_ABOVE]:
|
|
120
|
+
raise ValueError(f"Invalid value '{relation_type_id}' for 'relation_type_id'")
|
|
121
|
+
|
|
122
|
+
self.__object_type_id = object_type_id
|
|
123
|
+
self.__object_id = object_id
|
|
124
|
+
self.__attribute_id = attribute_id
|
|
125
|
+
self.__relation_type_id = relation_type_id
|
|
126
|
+
self.__value = value
|
|
127
|
+
|
|
128
|
+
super().__init__(**kwds)
|
|
129
|
+
|
|
130
|
+
@property
|
|
131
|
+
def object_type_id(self) -> int:
|
|
132
|
+
"""
|
|
133
|
+
Returns the ID of the object type.
|
|
134
|
+
|
|
135
|
+
Will be one of the following EPANET constants:
|
|
136
|
+
|
|
137
|
+
- EN_R_NODE = 6
|
|
138
|
+
- EN_R_LINK = 7
|
|
139
|
+
- EN_R_SYSTEM = 8
|
|
140
|
+
|
|
141
|
+
Returns
|
|
142
|
+
-------
|
|
143
|
+
`int`
|
|
144
|
+
ID of the object type..
|
|
145
|
+
"""
|
|
146
|
+
return self.__object_type_id
|
|
147
|
+
|
|
148
|
+
@property
|
|
149
|
+
def object_id(self) -> str:
|
|
150
|
+
"""
|
|
151
|
+
Returns the ID of the object (i.e. junction, pipe, link, tank, etc.).
|
|
152
|
+
|
|
153
|
+
Returns
|
|
154
|
+
-------
|
|
155
|
+
`str`
|
|
156
|
+
ID of the object.
|
|
157
|
+
"""
|
|
158
|
+
return self.__object_id
|
|
159
|
+
|
|
160
|
+
@property
|
|
161
|
+
def attribute_id(self) -> int:
|
|
162
|
+
"""
|
|
163
|
+
Returns the type ID of the object's attribute that is checked.
|
|
164
|
+
|
|
165
|
+
Will be one of the following constants:
|
|
166
|
+
|
|
167
|
+
- EN_R_DEMAND = 1
|
|
168
|
+
- EN_R_HEAD = 2
|
|
169
|
+
- EN_R_LEVEL = 3
|
|
170
|
+
- EN_R_PRESSURE = 4
|
|
171
|
+
- EN_R_FLOW = 5
|
|
172
|
+
- EN_R_STATUS = 6
|
|
173
|
+
- EN_R_SETTING = 7
|
|
174
|
+
- EN_R_TIME = 9
|
|
175
|
+
- EN_R_CLOCKTIME = 10
|
|
176
|
+
- EN_R_FILLTIME = 11
|
|
177
|
+
- EN_R_DRAINTIME = 12
|
|
178
|
+
|
|
179
|
+
Returns
|
|
180
|
+
-------
|
|
181
|
+
`int`
|
|
182
|
+
Type ID of the object's attribute that is checked.
|
|
183
|
+
"""
|
|
184
|
+
return self.__attribute_id
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def relation_type_id(self) -> int:
|
|
188
|
+
"""
|
|
189
|
+
Returns the ID of the type of comparison.
|
|
190
|
+
|
|
191
|
+
Will be one of the following constants:
|
|
192
|
+
|
|
193
|
+
- EN_R_EQ = 0
|
|
194
|
+
- EN_R_NEQ = 1
|
|
195
|
+
- EN_R_LEQ = 2
|
|
196
|
+
- EN_R_GEQ = 3
|
|
197
|
+
- EN_R_LESS = 4
|
|
198
|
+
- EN_R_GREATER = 5
|
|
199
|
+
- EN_R_IS = 6
|
|
200
|
+
- EN_R_NOT = 7
|
|
201
|
+
- EN_R_BELOW = 8
|
|
202
|
+
- EN_R_ABOVE = 9
|
|
203
|
+
|
|
204
|
+
Returns
|
|
205
|
+
-------
|
|
206
|
+
`int`
|
|
207
|
+
ID of the type of comparison.
|
|
208
|
+
"""
|
|
209
|
+
return self.__relation_type_id
|
|
210
|
+
|
|
211
|
+
@property
|
|
212
|
+
def value(self) -> Any:
|
|
213
|
+
"""
|
|
214
|
+
Returns the value that is compared against.
|
|
215
|
+
|
|
216
|
+
Returns
|
|
217
|
+
-------
|
|
218
|
+
`Any`
|
|
219
|
+
Value that is compared against.
|
|
220
|
+
"""
|
|
221
|
+
return self.__value
|
|
222
|
+
|
|
223
|
+
def get_attributes(self) -> dict:
|
|
224
|
+
return super().get_attributes() | {"object_type_id": self.__object_type_id,
|
|
225
|
+
"object_id": self.__object_id,
|
|
226
|
+
"attribute_id": self.__attribute_id,
|
|
227
|
+
"relation_type_id": self.__relation_type_id,
|
|
228
|
+
"value": self.__value}
|
|
229
|
+
|
|
230
|
+
def __eq__(self, other) -> bool:
|
|
231
|
+
return self.__object_type_id == other.object_type_id and \
|
|
232
|
+
self.__object_id == other.object_id and self.__attribute_id == other.attribute_id and \
|
|
233
|
+
self.__relation_type_id == other.relation_type_id and self.__value == other.value
|
|
234
|
+
|
|
235
|
+
def __str__(self) -> str:
|
|
236
|
+
desc = ""
|
|
237
|
+
|
|
238
|
+
if self.__attribute_id == EN_R_DEMAND:
|
|
239
|
+
if self.__object_type_id == ToolkitConstants.EN_R_NODE:
|
|
240
|
+
desc += f"JUNCTION {self.__object_id} DEMAND "
|
|
241
|
+
elif self.__object_type_id == ToolkitConstants.EN_R_SYSTEM:
|
|
242
|
+
desc += "SYSTEM DEMAND "
|
|
243
|
+
elif self.__attribute_id == EN_R_HEAD:
|
|
244
|
+
desc += f"JUNCTION {self.__object_id} HEAD "
|
|
245
|
+
elif self.__attribute_id == EN_R_LEVEL:
|
|
246
|
+
desc += f"TANK {self.__object_id} LEVEL "
|
|
247
|
+
elif self.__attribute_id == EN_R_PRESSURE:
|
|
248
|
+
desc += f"JUNCTION {self.__object_id} PRESSURE "
|
|
249
|
+
elif self.__attribute_id == EN_R_FLOW:
|
|
250
|
+
desc += f"LINK {self.__object_id} FLOW "
|
|
251
|
+
elif self.__attribute_id == EN_R_STATUS:
|
|
252
|
+
desc += f"LINK {self.__object_id} STATUS "
|
|
253
|
+
elif self.__attribute_id == EN_R_SETTING:
|
|
254
|
+
desc += f"LINK {self.__object_id} SETTING "
|
|
255
|
+
elif self.__attribute_id == EN_R_TIME:
|
|
256
|
+
desc += "SYSTEM TIME "
|
|
257
|
+
elif self.__attribute_id == EN_R_CLOCKTIME:
|
|
258
|
+
desc += "SYSTEM CLOCKTIME "
|
|
259
|
+
elif self.__attribute_id == EN_R_FILLTIME:
|
|
260
|
+
desc += f"TANK {self.__object_id} FILLTIME "
|
|
261
|
+
elif self.__attribute_id == EN_R_DRAINTIME:
|
|
262
|
+
desc += f"TANK {self.__object_id} DRAINTIME "
|
|
263
|
+
|
|
264
|
+
if self.__relation_type_id == EN_R_EQ:
|
|
265
|
+
desc += "= "
|
|
266
|
+
elif self.__relation_type_id == EN_R_IS:
|
|
267
|
+
desc += "IS "
|
|
268
|
+
elif self.__relation_type_id == EN_R_NOT:
|
|
269
|
+
desc += "IS NOT "
|
|
270
|
+
elif self.__relation_type_id == EN_R_LEQ:
|
|
271
|
+
desc += "<= "
|
|
272
|
+
elif self.__relation_type_id == EN_R_GEQ:
|
|
273
|
+
desc += ">= "
|
|
274
|
+
elif self.__relation_type_id == EN_R_ABOVE:
|
|
275
|
+
desc += "ABOVE "
|
|
276
|
+
elif self.__relation_type_id == EN_R_BELOW:
|
|
277
|
+
desc += "BELOW "
|
|
278
|
+
elif self.__relation_type_id == EN_R_LESS:
|
|
279
|
+
desc += "< "
|
|
280
|
+
elif self.__relation_type_id == EN_R_GREATER:
|
|
281
|
+
desc += "> "
|
|
282
|
+
|
|
283
|
+
desc += str(self.__value)
|
|
284
|
+
|
|
285
|
+
return desc
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
@serializable(COMPLEX_CONTROL_ACTION_ID, ".epytflow_complex_control_action")
|
|
289
|
+
class RuleAction(JsonSerializable):
|
|
290
|
+
"""
|
|
291
|
+
Class representing a rule action.
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
----------
|
|
295
|
+
link_type_id : `int`
|
|
296
|
+
Link type ID.
|
|
297
|
+
|
|
298
|
+
Must be one of following EPANET constants:
|
|
299
|
+
|
|
300
|
+
- EN_CVPIPE = 0
|
|
301
|
+
- EN_PIPE = 1
|
|
302
|
+
- EN_PUMP = 2
|
|
303
|
+
- EN_PRV = 3
|
|
304
|
+
- EN_PSV = 4
|
|
305
|
+
- EN_PBV = 5
|
|
306
|
+
- EN_FCV = 6
|
|
307
|
+
- EN_TCV = 7
|
|
308
|
+
- EN_GPV = 8
|
|
309
|
+
link_id : `str`
|
|
310
|
+
Link ID.
|
|
311
|
+
action_type_id : `int`
|
|
312
|
+
Type ID of the action.
|
|
313
|
+
|
|
314
|
+
Must be one of the following constants:
|
|
315
|
+
|
|
316
|
+
- EN_R_ACTION_SETTING = -1
|
|
317
|
+
- EN_R_ACTION_STATUS_OPEN = 1
|
|
318
|
+
- EN_R_ACTION_STATUS_CLOSED = 2
|
|
319
|
+
- EN_R_ACTION_STATUS_ACTIVE = 3
|
|
320
|
+
action_value : `Any`
|
|
321
|
+
Value of the acton (e.g. pump speed).
|
|
322
|
+
Only relevant if action_type_id = EN_R_SETTING, will be ignored in all other cases.
|
|
323
|
+
"""
|
|
324
|
+
def __init__(self, link_type_id: int, link_id: str, action_type_id: int, action_value: Any,
|
|
325
|
+
**kwds):
|
|
326
|
+
if not isinstance(link_type_id, int):
|
|
327
|
+
raise TypeError("'link_type_id' must be an istanace of 'int' " +
|
|
328
|
+
f"but not of '{type(link_type_id)}'")
|
|
329
|
+
if link_type_id not in [ToolkitConstants.EN_CVPIPE, ToolkitConstants.EN_PIPE,
|
|
330
|
+
ToolkitConstants.EN_PUMP, ToolkitConstants.EN_PRV,
|
|
331
|
+
ToolkitConstants.EN_PSV, ToolkitConstants.EN_PBV,
|
|
332
|
+
ToolkitConstants.EN_FCV, ToolkitConstants.EN_TCV,
|
|
333
|
+
ToolkitConstants.EN_GPV]:
|
|
334
|
+
raise ValueError(f"Invalid value '{link_type_id}' for 'link_type_id'")
|
|
335
|
+
if not isinstance(link_id, str):
|
|
336
|
+
raise TypeError("'link_id' must be an instance of 'str' " +
|
|
337
|
+
f"but not of '{type(link_id)}'")
|
|
338
|
+
if not isinstance(action_type_id, int):
|
|
339
|
+
raise TypeError("'action_type_id' must be an instance of 'int' " +
|
|
340
|
+
f"but not of '{type(action_type_id)}'")
|
|
341
|
+
if action_type_id not in [EN_R_ACTION_SETTING, EN_R_ACTION_STATUS_OPEN,
|
|
342
|
+
EN_R_ACTION_STATUS_CLOSED, EN_R_ACTION_STATUS_ACTIVE]:
|
|
343
|
+
raise ValueError(f"Invalid value '{action_type_id}' for 'action_type_id'")
|
|
344
|
+
|
|
345
|
+
self.__link_type_id = link_type_id
|
|
346
|
+
self.__link_id = link_id
|
|
347
|
+
self.__action_type_id = action_type_id
|
|
348
|
+
self.__action_value = action_value
|
|
349
|
+
|
|
350
|
+
super().__init__(**kwds)
|
|
351
|
+
|
|
352
|
+
@property
|
|
353
|
+
def link_type_id(self) -> int:
|
|
354
|
+
"""
|
|
355
|
+
Returns the link type ID.
|
|
356
|
+
|
|
357
|
+
Will be one of the following EPANET constants:
|
|
358
|
+
|
|
359
|
+
- EN_CVPIPE = 0
|
|
360
|
+
- EN_PIPE = 1
|
|
361
|
+
- EN_PUMP = 2
|
|
362
|
+
- EN_PRV = 3
|
|
363
|
+
- EN_PSV = 4
|
|
364
|
+
- EN_PBV = 5
|
|
365
|
+
- EN_FCV = 6
|
|
366
|
+
- EN_TCV = 7
|
|
367
|
+
- EN_GPV = 8
|
|
368
|
+
|
|
369
|
+
Returns
|
|
370
|
+
-------
|
|
371
|
+
`int`
|
|
372
|
+
Link type ID.
|
|
373
|
+
"""
|
|
374
|
+
return self.__link_type_id
|
|
375
|
+
|
|
376
|
+
@property
|
|
377
|
+
def link_id(self) -> str:
|
|
378
|
+
"""
|
|
379
|
+
Returns the link ID.
|
|
380
|
+
|
|
381
|
+
Returns
|
|
382
|
+
-------
|
|
383
|
+
`str`
|
|
384
|
+
Link ID.
|
|
385
|
+
"""
|
|
386
|
+
return self.__link_id
|
|
387
|
+
|
|
388
|
+
@property
|
|
389
|
+
def action_type_id(self) -> int:
|
|
390
|
+
"""
|
|
391
|
+
Returns the type ID of the action.
|
|
392
|
+
|
|
393
|
+
Will be one of the following constants:
|
|
394
|
+
|
|
395
|
+
- EN_R_ACTION_SETTING = -1
|
|
396
|
+
- EN_R_ACTION_STATUS_OPEN = 1
|
|
397
|
+
- EN_R_ACTION_STATUS_CLOSED = 2
|
|
398
|
+
- EN_R_ACTION_STATUS_ACTIVE = 3
|
|
399
|
+
|
|
400
|
+
Returns
|
|
401
|
+
-------
|
|
402
|
+
`int`
|
|
403
|
+
Type ID of the action.
|
|
404
|
+
"""
|
|
405
|
+
return self.__action_type_id
|
|
406
|
+
|
|
407
|
+
@property
|
|
408
|
+
def action_value(self) -> Any:
|
|
409
|
+
"""
|
|
410
|
+
Returns the value of the acton (e.g. pump speed).
|
|
411
|
+
Only relevant if action_type_id = EN_R_SETTING.
|
|
412
|
+
|
|
413
|
+
Returns
|
|
414
|
+
-------
|
|
415
|
+
`Any`
|
|
416
|
+
Value of the action.
|
|
417
|
+
"""
|
|
418
|
+
return self.__action_value
|
|
419
|
+
|
|
420
|
+
def get_attributes(self) -> dict:
|
|
421
|
+
return super().get_attributes() | {"link_type_id": self.__link_type_id,
|
|
422
|
+
"link_id": self.__link_id,
|
|
423
|
+
"action_type_id": self.__action_type_id,
|
|
424
|
+
"action_value": self.__action_value}
|
|
425
|
+
|
|
426
|
+
def __eq__(self, other) -> bool:
|
|
427
|
+
return self.__link_type_id == other.link_type_id and \
|
|
428
|
+
self.__link_id == other.link_id and \
|
|
429
|
+
self.__action_type_id == other.action_type_id and \
|
|
430
|
+
self.__action_value == other.action_value
|
|
431
|
+
|
|
432
|
+
def __str__(self) -> str:
|
|
433
|
+
desc = ""
|
|
434
|
+
|
|
435
|
+
if self.__link_type_id in [ToolkitConstants.EN_CVPIPE, ToolkitConstants.EN_PIPE]:
|
|
436
|
+
desc += "PIPE "
|
|
437
|
+
elif self.__link_type_id == ToolkitConstants.EN_PUMP:
|
|
438
|
+
desc += "PUMP "
|
|
439
|
+
else:
|
|
440
|
+
desc += "VALVE "
|
|
441
|
+
|
|
442
|
+
desc += f"{self.__link_id} "
|
|
443
|
+
|
|
444
|
+
if self.__action_type_id == EN_R_ACTION_SETTING:
|
|
445
|
+
desc += f"SETTING IS {self.__action_value}"
|
|
446
|
+
elif self.__action_type_id == EN_R_ACTION_STATUS_OPEN:
|
|
447
|
+
desc += "STATUS IS OPEN"
|
|
448
|
+
elif self.__action_type_id == EN_R_ACTION_STATUS_CLOSED:
|
|
449
|
+
desc += "STATUS IS CLOSED"
|
|
450
|
+
elif self.action_type_id == EN_R_ACTION_STATUS_ACTIVE:
|
|
451
|
+
desc += "STATUS IS ACTIVE"
|
|
452
|
+
|
|
453
|
+
return desc
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
@serializable(COMPLEX_CONTROL_ID, ".epytflow_complex_control")
|
|
457
|
+
class ComplexControlModule(JsonSerializable):
|
|
458
|
+
"""
|
|
459
|
+
Class representing a complex control module (i.e. IF-THEN-ELSE rule) as implemented in EPANET.
|
|
460
|
+
|
|
461
|
+
Parameters
|
|
462
|
+
----------
|
|
463
|
+
rule_id : `str`
|
|
464
|
+
ID of the rule.
|
|
465
|
+
condition_1 : :class:`~epyt_flow.simulation.scada.complex_control.RuleCondition`
|
|
466
|
+
First condition of this rule.
|
|
467
|
+
additional_conditions : list[tuple[int, :class:`~epyt_flow.simulation.scada.complex_control.RuleCondition`]]
|
|
468
|
+
List of (optional) additional conditions incl. their conjunction operator
|
|
469
|
+
(must be either EN_R_AND = 2 or EN_R_OR = 3).
|
|
470
|
+
|
|
471
|
+
Empty list if there are no additional conditions.
|
|
472
|
+
actions : list[:class:`~epyt_flow.simulation.scada.complex_control.RuleAction`]
|
|
473
|
+
List of actions that are applied if the conditions are met.
|
|
474
|
+
Must contain at least one action.
|
|
475
|
+
else_actions : list[:class:`~epyt_flow.simulation.scada.complex_control.RuleAction`]
|
|
476
|
+
List of actions that are applied if the conditions are NOT met.
|
|
477
|
+
priority : `int`
|
|
478
|
+
Priority of this control rule.
|
|
479
|
+
"""
|
|
480
|
+
def __init__(self, rule_id: str, condition_1: RuleCondition,
|
|
481
|
+
additional_conditions: list[tuple[int, RuleCondition]], actions: list[RuleAction],
|
|
482
|
+
else_actions: list[RuleAction], priority: int, **kwds):
|
|
483
|
+
if not isinstance(rule_id, str):
|
|
484
|
+
raise TypeError(f"'rule_id' must be an instance of 'str' but not of '{type(rule_id)}'")
|
|
485
|
+
if not isinstance(condition_1, RuleCondition):
|
|
486
|
+
raise TypeError("'condition_1' must be an instance of " +
|
|
487
|
+
"'epyt_flow.simulation.scada.RuleCondition' " +
|
|
488
|
+
f"but not of '{type(condition_1)}'")
|
|
489
|
+
if not isinstance(additional_conditions, list) or \
|
|
490
|
+
any(not isinstance(condition, tuple) for condition in additional_conditions) or \
|
|
491
|
+
any(not isinstance(condition[0], int) or condition[0] not in [EN_R_AND, EN_R_OR] or
|
|
492
|
+
not isinstance(condition[1], RuleCondition)
|
|
493
|
+
for condition in additional_conditions):
|
|
494
|
+
raise TypeError("'additional_conditions' must be a list of " +
|
|
495
|
+
"'tuple[int, epyt_flow.simulation.scada.RuleCondition]' instances")
|
|
496
|
+
if not isinstance(actions, list) or any(not isinstance(action, RuleAction)
|
|
497
|
+
for action in actions):
|
|
498
|
+
raise TypeError("'actions' must be a list of " +
|
|
499
|
+
"'epyt_flow.simulation.scada.RuleAction' instances")
|
|
500
|
+
if len(actions) == 0:
|
|
501
|
+
raise ValueError("'actions' must contain at least one action")
|
|
502
|
+
if not isinstance(else_actions, list) or any(not isinstance(action, RuleAction)
|
|
503
|
+
for action in actions):
|
|
504
|
+
raise TypeError("'else_actions' must be a list of " +
|
|
505
|
+
"'epyt_flow.simulation.scada.RuleAction' instances")
|
|
506
|
+
if not isinstance(priority, int) or priority < 0:
|
|
507
|
+
raise TypeError("'priority' must be a non-negative integer")
|
|
508
|
+
|
|
509
|
+
self.__rule_id = rule_id
|
|
510
|
+
self.__condition_1 = condition_1
|
|
511
|
+
self.__additional_conditions = additional_conditions
|
|
512
|
+
self.__actions = actions
|
|
513
|
+
self.__else_actions = else_actions
|
|
514
|
+
self.__priority = priority
|
|
515
|
+
|
|
516
|
+
super().__init__(**kwds)
|
|
517
|
+
|
|
518
|
+
@property
|
|
519
|
+
def rule_id(self) -> str:
|
|
520
|
+
"""
|
|
521
|
+
Returns the ID of this control rule.
|
|
522
|
+
|
|
523
|
+
Returns
|
|
524
|
+
-------
|
|
525
|
+
`str`
|
|
526
|
+
ID of this control rule.
|
|
527
|
+
"""
|
|
528
|
+
return self.__rule_id
|
|
529
|
+
|
|
530
|
+
@property
|
|
531
|
+
def condition_1(self) -> RuleCondition:
|
|
532
|
+
"""
|
|
533
|
+
Returns the first condition of this rule.
|
|
534
|
+
|
|
535
|
+
Returns
|
|
536
|
+
-------
|
|
537
|
+
:class:`~epyt_flow.simulation.scada.complex_control.RuleCondition`
|
|
538
|
+
First condition of this rule.
|
|
539
|
+
"""
|
|
540
|
+
return deepcopy(self.__condition_1)
|
|
541
|
+
|
|
542
|
+
@property
|
|
543
|
+
def additional_conditions(self) -> list[tuple[int, RuleCondition]]:
|
|
544
|
+
"""
|
|
545
|
+
Returns the list of (optional) additional conditions incl. their conjunction operator.
|
|
546
|
+
Empty list if there are no additional conditions.
|
|
547
|
+
|
|
548
|
+
Returns
|
|
549
|
+
-------
|
|
550
|
+
list[tuple[int, :class:`~epyt_flow.simulation.scada.complex_control.RuleCondition`]]
|
|
551
|
+
List of (optional) additional conditions incl. their conjunction operator.
|
|
552
|
+
"""
|
|
553
|
+
return deepcopy(self.__additional_conditions)
|
|
554
|
+
|
|
555
|
+
@property
|
|
556
|
+
def actions(self) -> list[RuleAction]:
|
|
557
|
+
"""
|
|
558
|
+
Returns the list of actions that are applied if the conditions are met.
|
|
559
|
+
|
|
560
|
+
Returns
|
|
561
|
+
-------
|
|
562
|
+
list[:class:`~epyt_flow.simulation.scada.complex_control.RuleAction`]
|
|
563
|
+
List of actions that are applied if the conditions are met.
|
|
564
|
+
"""
|
|
565
|
+
return deepcopy(self.__actions)
|
|
566
|
+
|
|
567
|
+
@property
|
|
568
|
+
def else_actions(self) -> list[RuleAction]:
|
|
569
|
+
"""
|
|
570
|
+
Returns the list of actions that are applied if the conditions are NOT met.
|
|
571
|
+
|
|
572
|
+
Returns
|
|
573
|
+
-------
|
|
574
|
+
list[:class:`~epyt_flow.simulation.scada.complex_control.RuleAction`]
|
|
575
|
+
List of actions that are applied if the conditions are NOT met.
|
|
576
|
+
"""
|
|
577
|
+
return deepcopy(self.__else_actions)
|
|
578
|
+
|
|
579
|
+
@property
|
|
580
|
+
def priority(self) -> int:
|
|
581
|
+
"""
|
|
582
|
+
Returns the priority of this control rule.
|
|
583
|
+
|
|
584
|
+
Returns
|
|
585
|
+
-------
|
|
586
|
+
`int`
|
|
587
|
+
Priority of this control rule.
|
|
588
|
+
"""
|
|
589
|
+
return self.__priority
|
|
590
|
+
|
|
591
|
+
def get_attributes(self) -> dict:
|
|
592
|
+
return super().get_attributes() | {"rule_id": self.__rule_id,
|
|
593
|
+
"condition_1": self.__condition_1,
|
|
594
|
+
"additional_conditions": self.__additional_conditions,
|
|
595
|
+
"actions": self.__actions,
|
|
596
|
+
"else_actions": self.__else_actions,
|
|
597
|
+
"priority": self.__priority}
|
|
598
|
+
|
|
599
|
+
def __eq__(self, other) -> bool:
|
|
600
|
+
return super().__eq__(other) and self.__rule_id == other.rule_id and \
|
|
601
|
+
self.__priority == other.priority and self.__condition_1 == other.condition_1 and \
|
|
602
|
+
np.all(self.__additional_conditions == other.additional_conditions) and \
|
|
603
|
+
np.all(self.__actions == other.actions) and \
|
|
604
|
+
np.all(self.__else_actions == other.else_actions)
|
|
605
|
+
|
|
606
|
+
def __str__(self) -> str:
|
|
607
|
+
desc = ""
|
|
608
|
+
|
|
609
|
+
desc += f"RULE {self.__rule_id}\n"
|
|
610
|
+
desc += f"IF {self.__condition_1} "
|
|
611
|
+
for op, action in self.__additional_conditions:
|
|
612
|
+
if op == EN_R_AND:
|
|
613
|
+
desc += "\nAND "
|
|
614
|
+
elif op == EN_R_OR:
|
|
615
|
+
desc += "\nOR "
|
|
616
|
+
|
|
617
|
+
desc += f"{action} "
|
|
618
|
+
desc += "\nTHEN "
|
|
619
|
+
desc += "\nAND ".join(str(action) for action in self.__actions)
|
|
620
|
+
if len(self.__else_actions) != 0:
|
|
621
|
+
desc += "\nELSE " + "\nAND ".join(str(action) for action in self.__else_actions)
|
|
622
|
+
|
|
623
|
+
desc += f"\nPRIORITY {self.__priority}"
|
|
624
|
+
|
|
625
|
+
return desc
|