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.
- epyt_flow/VERSION +1 -1
- epyt_flow/gym/scenario_control_env.py +9 -1
- epyt_flow/serialization.py +4 -0
- 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 +20 -3
- epyt_flow/simulation/scada/simple_control.py +317 -0
- epyt_flow/simulation/scenario_config.py +123 -23
- epyt_flow/simulation/scenario_simulator.py +394 -23
- epyt_flow/simulation/sensor_config.py +16 -0
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.10.0.dist-info}/LICENSE +1 -1
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.10.0.dist-info}/METADATA +7 -6
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.10.0.dist-info}/RECORD +19 -16
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.10.0.dist-info}/WHEEL +1 -1
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.10.0.dist-info}/top_level.txt +0 -0
|
@@ -5,6 +5,8 @@ import sys
|
|
|
5
5
|
import os
|
|
6
6
|
import pathlib
|
|
7
7
|
import time
|
|
8
|
+
from datetime import timedelta
|
|
9
|
+
from datetime import datetime
|
|
8
10
|
from typing import Generator, Union
|
|
9
11
|
from copy import deepcopy
|
|
10
12
|
import shutil
|
|
@@ -28,7 +30,8 @@ from .sensor_config import SensorConfig, areaunit_to_id, massunit_to_id, quality
|
|
|
28
30
|
from ..uncertainty import ModelUncertainty, SensorNoise
|
|
29
31
|
from .events import SystemEvent, Leakage, ActuatorEvent, SensorFault, SensorReadingAttack, \
|
|
30
32
|
SensorReadingEvent
|
|
31
|
-
from .scada import ScadaData,
|
|
33
|
+
from .scada import ScadaData, CustomControlModule, SimpleControlModule, ComplexControlModule, \
|
|
34
|
+
RuleCondition, RuleAction, ActuatorConstants, EN_R_ACTION_SETTING
|
|
32
35
|
from ..topology import NetworkTopology, UNITS_SIMETRIC, UNITS_USCUSTOM
|
|
33
36
|
from ..utils import get_temp_folder
|
|
34
37
|
|
|
@@ -67,8 +70,12 @@ class ScenarioSimulator():
|
|
|
67
70
|
Sensor noise.
|
|
68
71
|
_sensor_config : :class:`~epyt_flow.simulation.sensor_config.SensorConfig`, protected
|
|
69
72
|
Sensor configuration.
|
|
70
|
-
|
|
73
|
+
_custom_controls : list[:class:`~epyt_flow.simulation.scada.custom_control.CustomControlModule`], protected
|
|
71
74
|
List of custom control modules.
|
|
75
|
+
_simple_controls : list[:class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`], protected
|
|
76
|
+
List of simle EPANET control rules.
|
|
77
|
+
_complex_controls : list[:class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`], protected
|
|
78
|
+
List of complex (IF-THEN-ELSE) EPANET control rules.
|
|
72
79
|
_system_events : list[:class:`~epyt_flow.simulation.events.system_event.SystemEvent`], protected
|
|
73
80
|
Lsit of system events such as leakages.
|
|
74
81
|
_sensor_reading_events : list[:class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`], protected
|
|
@@ -105,7 +112,10 @@ class ScenarioSimulator():
|
|
|
105
112
|
self._model_uncertainty = ModelUncertainty()
|
|
106
113
|
self._sensor_noise = None
|
|
107
114
|
self._sensor_config = None
|
|
108
|
-
self.
|
|
115
|
+
self._advanced_controls = []
|
|
116
|
+
self._custom_controls = []
|
|
117
|
+
self._simple_controls = []
|
|
118
|
+
self._complex_controls = []
|
|
109
119
|
self._system_events = []
|
|
110
120
|
self._sensor_reading_events = []
|
|
111
121
|
self.__running_simulation = False
|
|
@@ -168,6 +178,9 @@ class ScenarioSimulator():
|
|
|
168
178
|
if self.__f_msx_in is not None:
|
|
169
179
|
self.epanet_api.loadMSXFile(my_f_msx_in, customMSXlib=custom_epanetmsx_lib)
|
|
170
180
|
|
|
181
|
+
self._simple_controls = self._parse_simple_control_rules()
|
|
182
|
+
self._complex_controls = self._parse_complex_control_rules()
|
|
183
|
+
|
|
171
184
|
self._sensor_config = self._get_empty_sensor_config()
|
|
172
185
|
if scenario_config is not None:
|
|
173
186
|
if scenario_config.general_params is not None:
|
|
@@ -177,8 +190,15 @@ class ScenarioSimulator():
|
|
|
177
190
|
self._sensor_noise = scenario_config.sensor_noise
|
|
178
191
|
self._sensor_config = scenario_config.sensor_config
|
|
179
192
|
|
|
180
|
-
|
|
181
|
-
|
|
193
|
+
if scenario_config.advanced_controls is not None:
|
|
194
|
+
for control in scenario_config.advanced_controls:
|
|
195
|
+
self.add_advanced_control(control)
|
|
196
|
+
for control in scenario_config.custom_controls:
|
|
197
|
+
self.add_custom_control(control)
|
|
198
|
+
for control in scenario_config.simple_controls:
|
|
199
|
+
self.add_simple_control(control)
|
|
200
|
+
for control in scenario_config.complex_controls:
|
|
201
|
+
self.add_complex_control(control)
|
|
182
202
|
for event in scenario_config.system_events:
|
|
183
203
|
self.add_system_event(event)
|
|
184
204
|
for event in scenario_config.sensor_reading_events:
|
|
@@ -323,18 +343,62 @@ class ScenarioSimulator():
|
|
|
323
343
|
self._sensor_config = sensor_config
|
|
324
344
|
|
|
325
345
|
@property
|
|
326
|
-
def
|
|
346
|
+
def advanced_controls(self) -> list:
|
|
327
347
|
"""
|
|
328
|
-
|
|
348
|
+
Returns all advanced control modules.
|
|
329
349
|
|
|
330
350
|
Returns
|
|
331
351
|
-------
|
|
332
352
|
list[:class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`]
|
|
333
|
-
All control modules.
|
|
353
|
+
All advanced control modules.
|
|
354
|
+
"""
|
|
355
|
+
warnings.warn("'AdvancedControlModule' is deprecated and will be removed in a " +
|
|
356
|
+
"future release -- use 'CustomControlModule' instead")
|
|
357
|
+
self._adapt_to_network_changes()
|
|
358
|
+
|
|
359
|
+
return deepcopy(self._advanced_controls)
|
|
360
|
+
|
|
361
|
+
@property
|
|
362
|
+
def custom_controls(self) -> list[CustomControlModule]:
|
|
363
|
+
"""
|
|
364
|
+
Returns all custom control modules.
|
|
365
|
+
|
|
366
|
+
Returns
|
|
367
|
+
-------
|
|
368
|
+
list[:class:`~epyt_flow.simulation.scada.custom_control.CustomControlModule`]
|
|
369
|
+
All custom control modules.
|
|
370
|
+
"""
|
|
371
|
+
self._adapt_to_network_changes()
|
|
372
|
+
|
|
373
|
+
return deepcopy(self._custom_controls)
|
|
374
|
+
|
|
375
|
+
@property
|
|
376
|
+
def simple_controls(self) -> list[SimpleControlModule]:
|
|
377
|
+
"""
|
|
378
|
+
Gets all simple EPANET control rules.
|
|
379
|
+
|
|
380
|
+
Returns
|
|
381
|
+
-------
|
|
382
|
+
list[:class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`]
|
|
383
|
+
All simple EPANET control rules.
|
|
334
384
|
"""
|
|
335
385
|
self._adapt_to_network_changes()
|
|
336
386
|
|
|
337
|
-
return deepcopy(self.
|
|
387
|
+
return deepcopy(self._simple_controls)
|
|
388
|
+
|
|
389
|
+
@property
|
|
390
|
+
def complex_controls(self) -> list[SimpleControlModule]:
|
|
391
|
+
"""
|
|
392
|
+
Gets all complex (IF-THEN-ELSE) EPANET control rules.
|
|
393
|
+
|
|
394
|
+
Returns
|
|
395
|
+
-------
|
|
396
|
+
list[:class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`]
|
|
397
|
+
All complex EPANET control rules.
|
|
398
|
+
"""
|
|
399
|
+
self._adapt_to_network_changes()
|
|
400
|
+
|
|
401
|
+
return deepcopy(self._complex_controls)
|
|
338
402
|
|
|
339
403
|
@property
|
|
340
404
|
def leakages(self) -> list[Leakage]:
|
|
@@ -422,6 +486,132 @@ class ScenarioSimulator():
|
|
|
422
486
|
|
|
423
487
|
return deepcopy(self._sensor_reading_events)
|
|
424
488
|
|
|
489
|
+
def _parse_simple_control_rules(self) -> list[SimpleControlModule]:
|
|
490
|
+
controls = []
|
|
491
|
+
|
|
492
|
+
for idx in self.epanet_api.getControls():
|
|
493
|
+
control = self.epanet_api.getControls(idx)
|
|
494
|
+
|
|
495
|
+
if control.Setting == "OPEN":
|
|
496
|
+
link_status = ActuatorConstants.EN_OPEN
|
|
497
|
+
else:
|
|
498
|
+
link_status = ActuatorConstants.EN_CLOSED
|
|
499
|
+
|
|
500
|
+
if control.Type == "LOWLEVEL":
|
|
501
|
+
cond_type = ToolkitConstants.EN_LOWLEVEL
|
|
502
|
+
elif control.Type == "HIGHLEVEL":
|
|
503
|
+
cond_type = ToolkitConstants.EN_HILEVEL
|
|
504
|
+
elif control.Type == "TIMER":
|
|
505
|
+
cond_type = ToolkitConstants.EN_TIMER
|
|
506
|
+
elif control.Type == "TIMEOFDAY":
|
|
507
|
+
cond_type = ToolkitConstants.EN_TIMEOFDAY
|
|
508
|
+
|
|
509
|
+
if control.NodeID is not None:
|
|
510
|
+
cond_var_value = control.NodeID
|
|
511
|
+
cond_comp_value = control.Value
|
|
512
|
+
else:
|
|
513
|
+
if cond_type == ToolkitConstants.EN_TIMER:
|
|
514
|
+
cond_var_value = int(control.Value / 3600)
|
|
515
|
+
elif cond_type == ToolkitConstants.EN_TIMEOFDAY:
|
|
516
|
+
sec = control.Value
|
|
517
|
+
if sec <= 43200:
|
|
518
|
+
cond_var_value = \
|
|
519
|
+
f"{':'.join(str(timedelta(seconds=sec)).split(':')[:2])} AM"
|
|
520
|
+
else:
|
|
521
|
+
sec -= 43200
|
|
522
|
+
cond_var_value = \
|
|
523
|
+
f"{':'.join(str(timedelta(seconds=sec)).split(':')[:2])} PM"
|
|
524
|
+
cond_comp_value = None
|
|
525
|
+
|
|
526
|
+
controls.append(SimpleControlModule(link_id=control.LinkID,
|
|
527
|
+
link_status=link_status,
|
|
528
|
+
cond_type=cond_type,
|
|
529
|
+
cond_var_value=cond_var_value,
|
|
530
|
+
cond_comp_value=cond_comp_value))
|
|
531
|
+
|
|
532
|
+
return controls
|
|
533
|
+
|
|
534
|
+
def _parse_complex_control_rules(self) -> list[ComplexControlModule]:
|
|
535
|
+
controls = []
|
|
536
|
+
|
|
537
|
+
rules = self.epanet_api.getRules()
|
|
538
|
+
for rule_idx, rule in rules.items():
|
|
539
|
+
rule_info = self.epanet_api.getRuleInfo(rule_idx)
|
|
540
|
+
|
|
541
|
+
rule_id = rule["Rule_ID"]
|
|
542
|
+
rule_priority, *_ = rule_info.Priority
|
|
543
|
+
|
|
544
|
+
# Parse conditions
|
|
545
|
+
n_rule_premises, *_ = rule_info.Premises
|
|
546
|
+
|
|
547
|
+
condition_1 = None
|
|
548
|
+
additional_conditions = []
|
|
549
|
+
for j in range(1, n_rule_premises + 1):
|
|
550
|
+
[logop, object_type_id, obj_idx, variable_type_id, relop, status, value_premise] = \
|
|
551
|
+
self.epanet_api.api.ENgetpremise(rule_idx, j)
|
|
552
|
+
|
|
553
|
+
object_id = None
|
|
554
|
+
if object_type_id == ToolkitConstants.EN_R_NODE:
|
|
555
|
+
object_id = self.epanet_api.getNodeNameID(obj_idx)
|
|
556
|
+
elif object_type_id == ToolkitConstants.EN_R_LINK:
|
|
557
|
+
object_id = self.epanet_api.getLinkNameID(obj_idx)
|
|
558
|
+
elif object_type_id == ToolkitConstants.EN_R_SYSTEM:
|
|
559
|
+
object_id = ""
|
|
560
|
+
|
|
561
|
+
if variable_type_id >= ToolkitConstants.EN_R_TIME:
|
|
562
|
+
value_premise = datetime.fromtimestamp(value_premise)\
|
|
563
|
+
.strftime("%I:%M %p")
|
|
564
|
+
if status != 0:
|
|
565
|
+
value_premise = self.epanet_api.RULESTATUS[status - 1]
|
|
566
|
+
|
|
567
|
+
condition = RuleCondition(object_type_id, object_id, variable_type_id,
|
|
568
|
+
relop, value_premise)
|
|
569
|
+
if condition_1 is None:
|
|
570
|
+
condition_1 = condition
|
|
571
|
+
else:
|
|
572
|
+
additional_conditions.append((logop, condition))
|
|
573
|
+
|
|
574
|
+
# Parse actions
|
|
575
|
+
n_rule_then_actions, *_ = rule_info.ThenActions
|
|
576
|
+
actions = []
|
|
577
|
+
for j in range(1, n_rule_then_actions + 1):
|
|
578
|
+
[link_idx, link_status, link_setting] = \
|
|
579
|
+
self.epanet_api.api.ENgetthenaction(rule_idx, j)
|
|
580
|
+
|
|
581
|
+
link_type_id = self.epanet_api.getLinkTypeIndex(link_idx)
|
|
582
|
+
link_id = self.epanet_api.getLinkNameID(link_idx)
|
|
583
|
+
if link_status >= 0:
|
|
584
|
+
action_type_id = link_status
|
|
585
|
+
action_value = link_status
|
|
586
|
+
else:
|
|
587
|
+
action_type_id = EN_R_ACTION_SETTING
|
|
588
|
+
action_value = link_setting
|
|
589
|
+
|
|
590
|
+
actions.append(RuleAction(link_type_id, link_id, action_type_id, action_value))
|
|
591
|
+
|
|
592
|
+
n_rule_else_actions, *_ = rule_info.ElseActions
|
|
593
|
+
else_actions = []
|
|
594
|
+
for j in range(1, n_rule_else_actions + 1):
|
|
595
|
+
[link_idx, link_status, link_setting] = \
|
|
596
|
+
self.epanet_api.api.ENgetelseaction(rule_idx, j)
|
|
597
|
+
|
|
598
|
+
link_type_id = self.epanet_api.getLinkType(link_idx)
|
|
599
|
+
link_id = self.epanet_api.getLinkNameID(link_idx)
|
|
600
|
+
if link_status <= 3:
|
|
601
|
+
action_type_id = link_status
|
|
602
|
+
action_value = link_status
|
|
603
|
+
else:
|
|
604
|
+
action_type_id = EN_R_ACTION_SETTING
|
|
605
|
+
action_value = link_setting
|
|
606
|
+
|
|
607
|
+
else_actions.append(RuleAction(link_type_id, link_id, action_type_id, action_value))
|
|
608
|
+
|
|
609
|
+
# Create and add control module
|
|
610
|
+
controls.append(ComplexControlModule(rule_id, condition_1, additional_conditions,
|
|
611
|
+
actions, else_actions, int(rule_priority)))
|
|
612
|
+
|
|
613
|
+
return controls
|
|
614
|
+
|
|
425
615
|
def _adapt_to_network_changes(self):
|
|
426
616
|
nodes = self.epanet_api.getNodeNameID()
|
|
427
617
|
links = self.epanet_api.getLinkNameID()
|
|
@@ -800,7 +990,11 @@ class ScenarioSimulator():
|
|
|
800
990
|
return ScenarioConfig(f_inp_in=self.__f_inp_in, f_msx_in=self.__f_msx_in,
|
|
801
991
|
general_params=general_params, sensor_config=self.sensor_config,
|
|
802
992
|
memory_consumption_estimate=self.estimate_memory_consumption(),
|
|
803
|
-
|
|
993
|
+
advanced_controls=None if len(self._advanced_controls) == 0 else self.advanced_controls,
|
|
994
|
+
custom_controls=self.custom_controls,
|
|
995
|
+
simple_controls=self.simple_controls,
|
|
996
|
+
complex_controls=self.complex_controls,
|
|
997
|
+
sensor_noise=self.sensor_noise,
|
|
804
998
|
model_uncertainty=self.model_uncertainty,
|
|
805
999
|
system_events=self.system_events,
|
|
806
1000
|
sensor_reading_events=self.sensor_reading_events)
|
|
@@ -1063,23 +1257,147 @@ class ScenarioSimulator():
|
|
|
1063
1257
|
self.epanet_api.setNodeJunctionData(node_idx, self.epanet_api.getNodeElevations(node_idx),
|
|
1064
1258
|
base_demand, demand_pattern_id)
|
|
1065
1259
|
|
|
1066
|
-
def
|
|
1260
|
+
def add_advanced_control(self, control) -> None:
|
|
1067
1261
|
"""
|
|
1068
|
-
Adds
|
|
1262
|
+
Adds an advanced control module to the scenario simulation.
|
|
1069
1263
|
|
|
1070
1264
|
Parameters
|
|
1071
1265
|
----------
|
|
1072
1266
|
control : :class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`
|
|
1073
|
-
|
|
1267
|
+
Advanced control module.
|
|
1074
1268
|
"""
|
|
1075
1269
|
self._adapt_to_network_changes()
|
|
1076
1270
|
|
|
1271
|
+
from .scada.advanced_control import AdvancedControlModule
|
|
1077
1272
|
if not isinstance(control, AdvancedControlModule):
|
|
1078
1273
|
raise TypeError("'control' must be an instance of " +
|
|
1079
1274
|
"'epyt_flow.simulation.scada.AdvancedControlModule' not of " +
|
|
1080
1275
|
f"'{type(control)}'")
|
|
1081
1276
|
|
|
1082
|
-
self.
|
|
1277
|
+
self._advanced_controls.append(control)
|
|
1278
|
+
|
|
1279
|
+
def add_custom_control(self, control: CustomControlModule) -> None:
|
|
1280
|
+
"""
|
|
1281
|
+
Adds a custom control module to the scenario simulation.
|
|
1282
|
+
|
|
1283
|
+
Parameters
|
|
1284
|
+
----------
|
|
1285
|
+
control : :class:`~epyt_flow.simulation.scada.custom_control.CustomControlModule`
|
|
1286
|
+
Custom control module.
|
|
1287
|
+
"""
|
|
1288
|
+
self._adapt_to_network_changes()
|
|
1289
|
+
|
|
1290
|
+
if not isinstance(control, CustomControlModule):
|
|
1291
|
+
raise TypeError("'control' must be an instance of " +
|
|
1292
|
+
"'epyt_flow.simulation.scada.CustomControlModule' not of " +
|
|
1293
|
+
f"'{type(control)}'")
|
|
1294
|
+
|
|
1295
|
+
self._custom_controls.append(control)
|
|
1296
|
+
|
|
1297
|
+
def add_simple_control(self, control: SimpleControlModule) -> None:
|
|
1298
|
+
"""
|
|
1299
|
+
Adds a simple EPANET control rule to the scenario simulation.
|
|
1300
|
+
|
|
1301
|
+
Parameters
|
|
1302
|
+
----------
|
|
1303
|
+
control : :class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`
|
|
1304
|
+
Simple EPANET control module.
|
|
1305
|
+
"""
|
|
1306
|
+
self._adapt_to_network_changes()
|
|
1307
|
+
|
|
1308
|
+
if not isinstance(control, SimpleControlModule):
|
|
1309
|
+
raise TypeError("'control' must be an instance of " +
|
|
1310
|
+
"'epyt_flow.simulation.scada.SimpleControlModule' not of " +
|
|
1311
|
+
f"'{type(control)}'")
|
|
1312
|
+
|
|
1313
|
+
if not any(c == control for c in self._simple_controls):
|
|
1314
|
+
self._simple_controls.append(control)
|
|
1315
|
+
self.epanet_api.addControls(str(control))
|
|
1316
|
+
|
|
1317
|
+
def remove_all_simple_controls(self) -> None:
|
|
1318
|
+
"""
|
|
1319
|
+
Removes all simple EPANET controls from the scenario.
|
|
1320
|
+
"""
|
|
1321
|
+
self.epanet_api.deleteControls()
|
|
1322
|
+
self._simple_controls = []
|
|
1323
|
+
|
|
1324
|
+
def remove_simple_control(self, control: SimpleControlModule) -> None:
|
|
1325
|
+
"""
|
|
1326
|
+
Removes a given simple EPANET control rule from the scenario.
|
|
1327
|
+
|
|
1328
|
+
Parameters
|
|
1329
|
+
----------
|
|
1330
|
+
control : :class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`
|
|
1331
|
+
Simple EPANET control module to be removed.
|
|
1332
|
+
"""
|
|
1333
|
+
self._adapt_to_network_changes()
|
|
1334
|
+
|
|
1335
|
+
if not isinstance(control, SimpleControlModule):
|
|
1336
|
+
raise TypeError("'control' must be an instance of " +
|
|
1337
|
+
"'epyt_flow.simulation.scada.SimpleControlModule' not of " +
|
|
1338
|
+
f"'{type(control)}'")
|
|
1339
|
+
|
|
1340
|
+
control_idx = None
|
|
1341
|
+
for idx, c in enumerate(self._simple_controls):
|
|
1342
|
+
if c == control:
|
|
1343
|
+
control_idx = idx + 1
|
|
1344
|
+
break
|
|
1345
|
+
if control_idx is None:
|
|
1346
|
+
raise ValueError("Invalid/Unknown control module.")
|
|
1347
|
+
|
|
1348
|
+
self.epanet_api.deleteControls(control_idx)
|
|
1349
|
+
self._simple_controls.remove(control)
|
|
1350
|
+
|
|
1351
|
+
def add_complex_control(self, control: ComplexControlModule) -> None:
|
|
1352
|
+
"""
|
|
1353
|
+
Adds an complex (IF-THEN-ELSE) EPANET control rule to the scenario simulation.
|
|
1354
|
+
|
|
1355
|
+
Parameters
|
|
1356
|
+
----------
|
|
1357
|
+
control : :class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`
|
|
1358
|
+
Complex EPANET control module.
|
|
1359
|
+
"""
|
|
1360
|
+
self._adapt_to_network_changes()
|
|
1361
|
+
|
|
1362
|
+
if not isinstance(control, ComplexControlModule):
|
|
1363
|
+
raise TypeError("'control' must be an instance of " +
|
|
1364
|
+
"'epyt_flow.simulation.scada.ComplexControlModule' not of " +
|
|
1365
|
+
f"'{type(control)}'")
|
|
1366
|
+
|
|
1367
|
+
if not any(c == control for c in self._complex_controls):
|
|
1368
|
+
self._complex_controls.append(control)
|
|
1369
|
+
self.epanet_api.addRules(str(control))
|
|
1370
|
+
|
|
1371
|
+
def remove_all_complex_controls(self) -> None:
|
|
1372
|
+
"""
|
|
1373
|
+
Removes all complex EPANET controls from the scenario.
|
|
1374
|
+
"""
|
|
1375
|
+
self.epanet_api.deleteRules()
|
|
1376
|
+
self._complex_controls = []
|
|
1377
|
+
|
|
1378
|
+
def remove_complex_control(self, control: ComplexControlModule) -> None:
|
|
1379
|
+
"""
|
|
1380
|
+
Removes a given complex (IF-THEN-ELSE) EPANET control rule from the scenario.
|
|
1381
|
+
|
|
1382
|
+
Parameters
|
|
1383
|
+
----------
|
|
1384
|
+
control : :class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`
|
|
1385
|
+
Complex EPANET control module to be removed.
|
|
1386
|
+
"""
|
|
1387
|
+
self._adapt_to_network_changes()
|
|
1388
|
+
|
|
1389
|
+
if not isinstance(control, ComplexControlModule):
|
|
1390
|
+
raise TypeError("'control' must be an instance of " +
|
|
1391
|
+
"'epyt_flow.simulation.scada.ComplexControlModule' not of " +
|
|
1392
|
+
f"'{type(control)}'")
|
|
1393
|
+
|
|
1394
|
+
if control.rule_id not in self.epanet_api.getRuleID():
|
|
1395
|
+
raise ValueError("Invalid/Unknown control module. " +
|
|
1396
|
+
f"Can not find rule ID '{control.rule_id}'")
|
|
1397
|
+
|
|
1398
|
+
rule_idx = self.epanet_api.getRuleID().index(control.rule_id) + 1
|
|
1399
|
+
self.epanet_api.deleteRules(rule_idx)
|
|
1400
|
+
self._complex_controls.remove(control)
|
|
1083
1401
|
|
|
1084
1402
|
def add_leakage(self, leakage_event: Leakage) -> None:
|
|
1085
1403
|
"""
|
|
@@ -1616,12 +1934,17 @@ class ScenarioSimulator():
|
|
|
1616
1934
|
for event in self._system_events:
|
|
1617
1935
|
event.reset()
|
|
1618
1936
|
|
|
1619
|
-
if self.
|
|
1620
|
-
for c in self.
|
|
1937
|
+
if self._advanced_controls is not None:
|
|
1938
|
+
for c in self._advanced_controls:
|
|
1621
1939
|
c.init(self.epanet_api)
|
|
1940
|
+
if self._custom_controls is not None:
|
|
1941
|
+
for control in self._custom_controls:
|
|
1942
|
+
control.init(self.epanet_api)
|
|
1622
1943
|
|
|
1623
1944
|
def run_advanced_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
|
|
1624
|
-
frozen_sensor_config: bool = False
|
|
1945
|
+
frozen_sensor_config: bool = False,
|
|
1946
|
+
use_quality_time_step_as_reporting_time_step: bool = False
|
|
1947
|
+
) -> ScadaData:
|
|
1625
1948
|
"""
|
|
1626
1949
|
Runs an advanced quality analysis using EPANET-MSX.
|
|
1627
1950
|
|
|
@@ -1638,6 +1961,13 @@ class ScenarioSimulator():
|
|
|
1638
1961
|
If True, the sensor config can not be changed and only the required sensor nodes/links
|
|
1639
1962
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
1640
1963
|
|
|
1964
|
+
The default is False.
|
|
1965
|
+
use_quality_time_step_as_reporting_time_step : `bool`, optional
|
|
1966
|
+
If True, the water quality time step will be used as the reporting time step.
|
|
1967
|
+
|
|
1968
|
+
As a consequence, the simualtion results can not be merged
|
|
1969
|
+
with the hydraulic simulation.
|
|
1970
|
+
|
|
1641
1971
|
The default is False.
|
|
1642
1972
|
|
|
1643
1973
|
Returns
|
|
@@ -1657,7 +1987,9 @@ class ScenarioSimulator():
|
|
|
1657
1987
|
for scada_data in gen(hyd_file_in=hyd_file_in,
|
|
1658
1988
|
verbose=verbose,
|
|
1659
1989
|
return_as_dict=True,
|
|
1660
|
-
frozen_sensor_config=frozen_sensor_config
|
|
1990
|
+
frozen_sensor_config=frozen_sensor_config,
|
|
1991
|
+
use_quality_time_step_as_reporting_time_step=
|
|
1992
|
+
use_quality_time_step_as_reporting_time_step):
|
|
1661
1993
|
if result is None:
|
|
1662
1994
|
result = {}
|
|
1663
1995
|
for data_type, data in scada_data.items():
|
|
@@ -1682,7 +2014,8 @@ class ScenarioSimulator():
|
|
|
1682
2014
|
def run_advanced_quality_simulation_as_generator(self, hyd_file_in: str, verbose: bool = False,
|
|
1683
2015
|
support_abort: bool = False,
|
|
1684
2016
|
return_as_dict: bool = False,
|
|
1685
|
-
frozen_sensor_config: bool = False
|
|
2017
|
+
frozen_sensor_config: bool = False,
|
|
2018
|
+
use_quality_time_step_as_reporting_time_step: bool = False,
|
|
1686
2019
|
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
1687
2020
|
"""
|
|
1688
2021
|
Runs an advanced quality analysis using EPANET-MSX.
|
|
@@ -1704,6 +2037,13 @@ class ScenarioSimulator():
|
|
|
1704
2037
|
If True, the sensor config can not be changed and only the required sensor nodes/links
|
|
1705
2038
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
1706
2039
|
|
|
2040
|
+
The default is False.
|
|
2041
|
+
use_quality_time_step_as_reporting_time_step : `bool`, optional
|
|
2042
|
+
If True, the water quality time step will be used as the reporting time step.
|
|
2043
|
+
|
|
2044
|
+
As a consequence, the simualtion results can not be merged
|
|
2045
|
+
with the hydraulic simulation.
|
|
2046
|
+
|
|
1707
2047
|
The default is False.
|
|
1708
2048
|
|
|
1709
2049
|
Returns
|
|
@@ -1729,6 +2069,11 @@ class ScenarioSimulator():
|
|
|
1729
2069
|
reporting_time_step = self.epanet_api.getTimeReportingStep()
|
|
1730
2070
|
hyd_time_step = self.epanet_api.getTimeHydraulicStep()
|
|
1731
2071
|
|
|
2072
|
+
if use_quality_time_step_as_reporting_time_step is True:
|
|
2073
|
+
quality_time_step = self.epanet_api.getMSXTimeStep()
|
|
2074
|
+
reporting_time_step = quality_time_step
|
|
2075
|
+
hyd_time_step = quality_time_step
|
|
2076
|
+
|
|
1732
2077
|
self.epanet_api.initializeMSXQualityAnalysis(ToolkitConstants.EN_NOSAVE)
|
|
1733
2078
|
|
|
1734
2079
|
self.__running_simulation = True
|
|
@@ -1880,7 +2225,9 @@ class ScenarioSimulator():
|
|
|
1880
2225
|
self.__running_simulation = False
|
|
1881
2226
|
|
|
1882
2227
|
def run_basic_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
|
|
1883
|
-
frozen_sensor_config: bool = False
|
|
2228
|
+
frozen_sensor_config: bool = False,
|
|
2229
|
+
use_quality_time_step_as_reporting_time_step: bool = False
|
|
2230
|
+
) -> ScadaData:
|
|
1884
2231
|
"""
|
|
1885
2232
|
Runs a basic quality analysis using EPANET.
|
|
1886
2233
|
|
|
@@ -1897,6 +2244,13 @@ class ScenarioSimulator():
|
|
|
1897
2244
|
If True, the sensor config can not be changed and only the required sensor nodes/links
|
|
1898
2245
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
1899
2246
|
|
|
2247
|
+
The default is False.
|
|
2248
|
+
use_quality_time_step_as_reporting_time_step : `bool`, optional
|
|
2249
|
+
If True, the water quality time step will be used as the reporting time step.
|
|
2250
|
+
|
|
2251
|
+
As a consequence, the simualtion results can not be merged
|
|
2252
|
+
with the hydraulic simulation.
|
|
2253
|
+
|
|
1900
2254
|
The default is False.
|
|
1901
2255
|
|
|
1902
2256
|
Returns
|
|
@@ -1914,7 +2268,9 @@ class ScenarioSimulator():
|
|
|
1914
2268
|
for scada_data in gen(hyd_file_in=hyd_file_in,
|
|
1915
2269
|
verbose=verbose,
|
|
1916
2270
|
return_as_dict=True,
|
|
1917
|
-
frozen_sensor_config=frozen_sensor_config
|
|
2271
|
+
frozen_sensor_config=frozen_sensor_config,
|
|
2272
|
+
use_quality_time_step_as_reporting_time_step=
|
|
2273
|
+
use_quality_time_step_as_reporting_time_step):
|
|
1918
2274
|
if result is None:
|
|
1919
2275
|
result = {}
|
|
1920
2276
|
for data_type, data in scada_data.items():
|
|
@@ -1937,6 +2293,7 @@ class ScenarioSimulator():
|
|
|
1937
2293
|
support_abort: bool = False,
|
|
1938
2294
|
return_as_dict: bool = False,
|
|
1939
2295
|
frozen_sensor_config: bool = False,
|
|
2296
|
+
use_quality_time_step_as_reporting_time_step: bool = False
|
|
1940
2297
|
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
1941
2298
|
"""
|
|
1942
2299
|
Runs a basic quality analysis using EPANET.
|
|
@@ -1960,6 +2317,13 @@ class ScenarioSimulator():
|
|
|
1960
2317
|
If True, the sensor config can not be changed and only the required sensor nodes/links
|
|
1961
2318
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
1962
2319
|
|
|
2320
|
+
The default is False.
|
|
2321
|
+
use_quality_time_step_as_reporting_time_step : `bool`, optional
|
|
2322
|
+
If True, the water quality time step will be used as the reporting time step.
|
|
2323
|
+
|
|
2324
|
+
As a consequence, the simualtion results can not be merged
|
|
2325
|
+
with the hydraulic simulation.
|
|
2326
|
+
|
|
1963
2327
|
The default is False.
|
|
1964
2328
|
|
|
1965
2329
|
Returns
|
|
@@ -1974,6 +2338,11 @@ class ScenarioSimulator():
|
|
|
1974
2338
|
reporting_time_start = self.epanet_api.getTimeReportingStart()
|
|
1975
2339
|
reporting_time_step = self.epanet_api.getTimeReportingStep()
|
|
1976
2340
|
|
|
2341
|
+
if use_quality_time_step_as_reporting_time_step is True:
|
|
2342
|
+
quality_time_step = self.epanet_api.getTimeQualityStep()
|
|
2343
|
+
requested_time_step = quality_time_step
|
|
2344
|
+
reporting_time_step = quality_time_step
|
|
2345
|
+
|
|
1977
2346
|
self.epanet_api.useHydraulicFile(hyd_file_in)
|
|
1978
2347
|
|
|
1979
2348
|
self.epanet_api.openQualityAnalysis()
|
|
@@ -2032,7 +2401,7 @@ class ScenarioSimulator():
|
|
|
2032
2401
|
yield data
|
|
2033
2402
|
|
|
2034
2403
|
# Next
|
|
2035
|
-
tstep = self.epanet_api.
|
|
2404
|
+
tstep = self.epanet_api.stepQualityAnalysisTimeLeft()
|
|
2036
2405
|
|
|
2037
2406
|
self.epanet_api.closeHydraulicAnalysis()
|
|
2038
2407
|
|
|
@@ -2254,7 +2623,9 @@ class ScenarioSimulator():
|
|
|
2254
2623
|
yield data
|
|
2255
2624
|
|
|
2256
2625
|
# Apply control modules
|
|
2257
|
-
for control in self.
|
|
2626
|
+
for control in self._advanced_controls:
|
|
2627
|
+
control.step(scada_data)
|
|
2628
|
+
for control in self._custom_controls:
|
|
2258
2629
|
control.step(scada_data)
|
|
2259
2630
|
|
|
2260
2631
|
# Next
|
|
@@ -1276,6 +1276,22 @@ class SensorConfig(JsonSerializable):
|
|
|
1276
1276
|
"""
|
|
1277
1277
|
return self.__links.copy()
|
|
1278
1278
|
|
|
1279
|
+
@property
|
|
1280
|
+
def junctions(self) -> list[str]:
|
|
1281
|
+
"""
|
|
1282
|
+
Returns all junction IDs.
|
|
1283
|
+
|
|
1284
|
+
Returns
|
|
1285
|
+
-------
|
|
1286
|
+
`list[str]`
|
|
1287
|
+
All juncitons IDs.
|
|
1288
|
+
"""
|
|
1289
|
+
junctions = self.nodes
|
|
1290
|
+
for tank_id in self.tanks:
|
|
1291
|
+
junctions.remove(tank_id)
|
|
1292
|
+
|
|
1293
|
+
return junctions
|
|
1294
|
+
|
|
1279
1295
|
@property
|
|
1280
1296
|
def valves(self) -> list[str]:
|
|
1281
1297
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.2
|
|
2
2
|
Name: epyt-flow
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.10.0
|
|
4
4
|
Summary: EPyT-Flow -- EPANET Python Toolkit - Flow
|
|
5
5
|
Author-email: André Artelt <aartelt@techfak.uni-bielefeld.de>, "Marios S. Kyriakou" <kiriakou.marios@ucy.ac.cy>, "Stelios G. Vrachimis" <vrachimis.stelios@ucy.ac.cy>
|
|
6
6
|
License: MIT License
|
|
@@ -17,10 +17,11 @@ Classifier: Programming Language :: Python :: 3.9
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
18
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
21
|
Requires-Python: >=3.9
|
|
21
22
|
Description-Content-Type: text/markdown
|
|
22
23
|
License-File: LICENSE
|
|
23
|
-
Requires-Dist: epyt>=1.2.
|
|
24
|
+
Requires-Dist: epyt>=1.2.1
|
|
24
25
|
Requires-Dist: requests>=2.31.0
|
|
25
26
|
Requires-Dist: scipy>=1.11.4
|
|
26
27
|
Requires-Dist: u-msgpack-python>=2.8.0
|
|
@@ -66,8 +67,8 @@ Unique features of EPyT-Flow that make it superior to other (Python) toolboxes a
|
|
|
66
67
|
- High- and low-level interface
|
|
67
68
|
- Object-orientated design that is easy to extend and customize
|
|
68
69
|
- Sensor configurations
|
|
69
|
-
- Wide variety of pre-defined events (e.g. leakages, sensor faults, actuator events, cyber-attacks, etc.)
|
|
70
|
-
- Wide variety of pre-defined types of uncertainties (e.g. model uncertainties)
|
|
70
|
+
- Wide variety of pre-defined events (e.g. leakages, sensor faults, actuator events, contamination, cyber-attacks, etc.)
|
|
71
|
+
- Wide variety of pre-defined types of global & local uncertainties (e.g. model uncertainties)
|
|
71
72
|
- Step-wise simulation and environment for training and evaluating control strategies
|
|
72
73
|
- Serialization module for easy exchange of data and (scenario) configurations
|
|
73
74
|
- REST API to make EPyT-Flow accessible in other applications
|
|
@@ -76,7 +77,7 @@ Unique features of EPyT-Flow that make it superior to other (Python) toolboxes a
|
|
|
76
77
|
|
|
77
78
|
## Installation
|
|
78
79
|
|
|
79
|
-
EPyT-Flow supports Python 3.9 - 3.
|
|
80
|
+
EPyT-Flow supports Python 3.9 - 3.13
|
|
80
81
|
|
|
81
82
|
Note that [EPANET and EPANET-MSX sources](epyt_flow/EPANET/) are compiled and overwrite the binaries
|
|
82
83
|
shipped by EPyT **IF** EPyT-Flow is installed on a Unix system and the *gcc* compiler is available.
|