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
|
@@ -5,7 +5,9 @@ import sys
|
|
|
5
5
|
import os
|
|
6
6
|
import pathlib
|
|
7
7
|
import time
|
|
8
|
-
from
|
|
8
|
+
from datetime import timedelta
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
from typing import Generator, Union, Optional
|
|
9
11
|
from copy import deepcopy
|
|
10
12
|
import shutil
|
|
11
13
|
import warnings
|
|
@@ -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,9 @@ class ScenarioSimulator():
|
|
|
105
112
|
self._model_uncertainty = ModelUncertainty()
|
|
106
113
|
self._sensor_noise = None
|
|
107
114
|
self._sensor_config = None
|
|
108
|
-
self.
|
|
115
|
+
self._custom_controls = []
|
|
116
|
+
self._simple_controls = []
|
|
117
|
+
self._complex_controls = []
|
|
109
118
|
self._system_events = []
|
|
110
119
|
self._sensor_reading_events = []
|
|
111
120
|
self.__running_simulation = False
|
|
@@ -168,6 +177,9 @@ class ScenarioSimulator():
|
|
|
168
177
|
if self.__f_msx_in is not None:
|
|
169
178
|
self.epanet_api.loadMSXFile(my_f_msx_in, customMSXlib=custom_epanetmsx_lib)
|
|
170
179
|
|
|
180
|
+
self._simple_controls = self._parse_simple_control_rules()
|
|
181
|
+
self._complex_controls = self._parse_complex_control_rules()
|
|
182
|
+
|
|
171
183
|
self._sensor_config = self._get_empty_sensor_config()
|
|
172
184
|
if scenario_config is not None:
|
|
173
185
|
if scenario_config.general_params is not None:
|
|
@@ -177,8 +189,12 @@ class ScenarioSimulator():
|
|
|
177
189
|
self._sensor_noise = scenario_config.sensor_noise
|
|
178
190
|
self._sensor_config = scenario_config.sensor_config
|
|
179
191
|
|
|
180
|
-
for control in scenario_config.
|
|
181
|
-
self.
|
|
192
|
+
for control in scenario_config.custom_controls:
|
|
193
|
+
self.add_custom_control(control)
|
|
194
|
+
for control in scenario_config.simple_controls:
|
|
195
|
+
self.add_simple_control(control)
|
|
196
|
+
for control in scenario_config.complex_controls:
|
|
197
|
+
self.add_complex_control(control)
|
|
182
198
|
for event in scenario_config.system_events:
|
|
183
199
|
self.add_system_event(event)
|
|
184
200
|
for event in scenario_config.sensor_reading_events:
|
|
@@ -323,18 +339,46 @@ class ScenarioSimulator():
|
|
|
323
339
|
self._sensor_config = sensor_config
|
|
324
340
|
|
|
325
341
|
@property
|
|
326
|
-
def
|
|
342
|
+
def custom_controls(self) -> list[CustomControlModule]:
|
|
343
|
+
"""
|
|
344
|
+
Returns all custom control modules.
|
|
345
|
+
|
|
346
|
+
Returns
|
|
347
|
+
-------
|
|
348
|
+
list[:class:`~epyt_flow.simulation.scada.custom_control.CustomControlModule`]
|
|
349
|
+
All custom control modules.
|
|
350
|
+
"""
|
|
351
|
+
self._adapt_to_network_changes()
|
|
352
|
+
|
|
353
|
+
return deepcopy(self._custom_controls)
|
|
354
|
+
|
|
355
|
+
@property
|
|
356
|
+
def simple_controls(self) -> list[SimpleControlModule]:
|
|
327
357
|
"""
|
|
328
|
-
Gets all control
|
|
358
|
+
Gets all simple EPANET control rules.
|
|
329
359
|
|
|
330
360
|
Returns
|
|
331
361
|
-------
|
|
332
|
-
list[:class:`~epyt_flow.simulation.scada.
|
|
333
|
-
All control
|
|
362
|
+
list[:class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`]
|
|
363
|
+
All simple EPANET control rules.
|
|
334
364
|
"""
|
|
335
365
|
self._adapt_to_network_changes()
|
|
336
366
|
|
|
337
|
-
return deepcopy(self.
|
|
367
|
+
return deepcopy(self._simple_controls)
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def complex_controls(self) -> list[SimpleControlModule]:
|
|
371
|
+
"""
|
|
372
|
+
Gets all complex (IF-THEN-ELSE) EPANET control rules.
|
|
373
|
+
|
|
374
|
+
Returns
|
|
375
|
+
-------
|
|
376
|
+
list[:class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`]
|
|
377
|
+
All complex EPANET control rules.
|
|
378
|
+
"""
|
|
379
|
+
self._adapt_to_network_changes()
|
|
380
|
+
|
|
381
|
+
return deepcopy(self._complex_controls)
|
|
338
382
|
|
|
339
383
|
@property
|
|
340
384
|
def leakages(self) -> list[Leakage]:
|
|
@@ -422,6 +466,132 @@ class ScenarioSimulator():
|
|
|
422
466
|
|
|
423
467
|
return deepcopy(self._sensor_reading_events)
|
|
424
468
|
|
|
469
|
+
def _parse_simple_control_rules(self) -> list[SimpleControlModule]:
|
|
470
|
+
controls = []
|
|
471
|
+
|
|
472
|
+
for idx in self.epanet_api.getControls():
|
|
473
|
+
control = self.epanet_api.getControls(idx)
|
|
474
|
+
|
|
475
|
+
if control.Setting == "OPEN":
|
|
476
|
+
link_status = ActuatorConstants.EN_OPEN
|
|
477
|
+
else:
|
|
478
|
+
link_status = ActuatorConstants.EN_CLOSED
|
|
479
|
+
|
|
480
|
+
if control.Type == "LOWLEVEL":
|
|
481
|
+
cond_type = ToolkitConstants.EN_LOWLEVEL
|
|
482
|
+
elif control.Type == "HIGHLEVEL":
|
|
483
|
+
cond_type = ToolkitConstants.EN_HILEVEL
|
|
484
|
+
elif control.Type == "TIMER":
|
|
485
|
+
cond_type = ToolkitConstants.EN_TIMER
|
|
486
|
+
elif control.Type == "TIMEOFDAY":
|
|
487
|
+
cond_type = ToolkitConstants.EN_TIMEOFDAY
|
|
488
|
+
|
|
489
|
+
if control.NodeID is not None:
|
|
490
|
+
cond_var_value = control.NodeID
|
|
491
|
+
cond_comp_value = control.Value
|
|
492
|
+
else:
|
|
493
|
+
if cond_type == ToolkitConstants.EN_TIMER:
|
|
494
|
+
cond_var_value = int(control.Value / 3600)
|
|
495
|
+
elif cond_type == ToolkitConstants.EN_TIMEOFDAY:
|
|
496
|
+
sec = control.Value
|
|
497
|
+
if sec <= 43200:
|
|
498
|
+
cond_var_value = \
|
|
499
|
+
f"{':'.join(str(timedelta(seconds=sec)).split(':')[:2])} AM"
|
|
500
|
+
else:
|
|
501
|
+
sec -= 43200
|
|
502
|
+
cond_var_value = \
|
|
503
|
+
f"{':'.join(str(timedelta(seconds=sec)).split(':')[:2])} PM"
|
|
504
|
+
cond_comp_value = None
|
|
505
|
+
|
|
506
|
+
controls.append(SimpleControlModule(link_id=control.LinkID,
|
|
507
|
+
link_status=link_status,
|
|
508
|
+
cond_type=cond_type,
|
|
509
|
+
cond_var_value=cond_var_value,
|
|
510
|
+
cond_comp_value=cond_comp_value))
|
|
511
|
+
|
|
512
|
+
return controls
|
|
513
|
+
|
|
514
|
+
def _parse_complex_control_rules(self) -> list[ComplexControlModule]:
|
|
515
|
+
controls = []
|
|
516
|
+
|
|
517
|
+
rules = self.epanet_api.getRules()
|
|
518
|
+
for rule_idx, rule in rules.items():
|
|
519
|
+
rule_info = self.epanet_api.getRuleInfo(rule_idx)
|
|
520
|
+
|
|
521
|
+
rule_id = rule["Rule_ID"]
|
|
522
|
+
rule_priority, *_ = rule_info.Priority
|
|
523
|
+
|
|
524
|
+
# Parse conditions
|
|
525
|
+
n_rule_premises, *_ = rule_info.Premises
|
|
526
|
+
|
|
527
|
+
condition_1 = None
|
|
528
|
+
additional_conditions = []
|
|
529
|
+
for j in range(1, n_rule_premises + 1):
|
|
530
|
+
[logop, object_type_id, obj_idx, variable_type_id, relop, status, value_premise] = \
|
|
531
|
+
self.epanet_api.api.ENgetpremise(rule_idx, j)
|
|
532
|
+
|
|
533
|
+
object_id = None
|
|
534
|
+
if object_type_id == ToolkitConstants.EN_R_NODE:
|
|
535
|
+
object_id = self.epanet_api.getNodeNameID(obj_idx)
|
|
536
|
+
elif object_type_id == ToolkitConstants.EN_R_LINK:
|
|
537
|
+
object_id = self.epanet_api.getLinkNameID(obj_idx)
|
|
538
|
+
elif object_type_id == ToolkitConstants.EN_R_SYSTEM:
|
|
539
|
+
object_id = ""
|
|
540
|
+
|
|
541
|
+
if variable_type_id >= ToolkitConstants.EN_R_TIME:
|
|
542
|
+
value_premise = datetime.fromtimestamp(value_premise)\
|
|
543
|
+
.strftime("%I:%M %p")
|
|
544
|
+
if status != 0:
|
|
545
|
+
value_premise = self.epanet_api.RULESTATUS[status - 1]
|
|
546
|
+
|
|
547
|
+
condition = RuleCondition(object_type_id, object_id, variable_type_id,
|
|
548
|
+
relop, value_premise)
|
|
549
|
+
if condition_1 is None:
|
|
550
|
+
condition_1 = condition
|
|
551
|
+
else:
|
|
552
|
+
additional_conditions.append((logop, condition))
|
|
553
|
+
|
|
554
|
+
# Parse actions
|
|
555
|
+
n_rule_then_actions, *_ = rule_info.ThenActions
|
|
556
|
+
actions = []
|
|
557
|
+
for j in range(1, n_rule_then_actions + 1):
|
|
558
|
+
[link_idx, link_status, link_setting] = \
|
|
559
|
+
self.epanet_api.api.ENgetthenaction(rule_idx, j)
|
|
560
|
+
|
|
561
|
+
link_type_id = self.epanet_api.getLinkTypeIndex(link_idx)
|
|
562
|
+
link_id = self.epanet_api.getLinkNameID(link_idx)
|
|
563
|
+
if link_status >= 0:
|
|
564
|
+
action_type_id = link_status
|
|
565
|
+
action_value = link_status
|
|
566
|
+
else:
|
|
567
|
+
action_type_id = EN_R_ACTION_SETTING
|
|
568
|
+
action_value = link_setting
|
|
569
|
+
|
|
570
|
+
actions.append(RuleAction(link_type_id, link_id, action_type_id, action_value))
|
|
571
|
+
|
|
572
|
+
n_rule_else_actions, *_ = rule_info.ElseActions
|
|
573
|
+
else_actions = []
|
|
574
|
+
for j in range(1, n_rule_else_actions + 1):
|
|
575
|
+
[link_idx, link_status, link_setting] = \
|
|
576
|
+
self.epanet_api.api.ENgetelseaction(rule_idx, j)
|
|
577
|
+
|
|
578
|
+
link_type_id = self.epanet_api.getLinkType(link_idx)
|
|
579
|
+
link_id = self.epanet_api.getLinkNameID(link_idx)
|
|
580
|
+
if link_status <= 3:
|
|
581
|
+
action_type_id = link_status
|
|
582
|
+
action_value = link_status
|
|
583
|
+
else:
|
|
584
|
+
action_type_id = EN_R_ACTION_SETTING
|
|
585
|
+
action_value = link_setting
|
|
586
|
+
|
|
587
|
+
else_actions.append(RuleAction(link_type_id, link_id, action_type_id, action_value))
|
|
588
|
+
|
|
589
|
+
# Create and add control module
|
|
590
|
+
controls.append(ComplexControlModule(rule_id, condition_1, additional_conditions,
|
|
591
|
+
actions, else_actions, int(rule_priority)))
|
|
592
|
+
|
|
593
|
+
return controls
|
|
594
|
+
|
|
425
595
|
def _adapt_to_network_changes(self):
|
|
426
596
|
nodes = self.epanet_api.getNodeNameID()
|
|
427
597
|
links = self.epanet_api.getLinkNameID()
|
|
@@ -800,7 +970,10 @@ class ScenarioSimulator():
|
|
|
800
970
|
return ScenarioConfig(f_inp_in=self.__f_inp_in, f_msx_in=self.__f_msx_in,
|
|
801
971
|
general_params=general_params, sensor_config=self.sensor_config,
|
|
802
972
|
memory_consumption_estimate=self.estimate_memory_consumption(),
|
|
803
|
-
|
|
973
|
+
custom_controls=self.custom_controls,
|
|
974
|
+
simple_controls=self.simple_controls,
|
|
975
|
+
complex_controls=self.complex_controls,
|
|
976
|
+
sensor_noise=self.sensor_noise,
|
|
804
977
|
model_uncertainty=self.model_uncertainty,
|
|
805
978
|
system_events=self.system_events,
|
|
806
979
|
sensor_reading_events=self.sensor_reading_events)
|
|
@@ -847,6 +1020,7 @@ class ScenarioSimulator():
|
|
|
847
1020
|
nodes_type = [self.epanet_api.TYPENODE[i] for i in self.epanet_api.getNodeTypeIndex()]
|
|
848
1021
|
nodes_coord = [self.epanet_api.api.ENgetcoord(node_idx)
|
|
849
1022
|
for node_idx in self.epanet_api.getNodeIndex()]
|
|
1023
|
+
nodes_comments = self.epanet_api.getNodeComment()
|
|
850
1024
|
node_tank_names = self.epanet_api.getNodeTankNameID()
|
|
851
1025
|
|
|
852
1026
|
links_id = self.epanet_api.getLinkNameID()
|
|
@@ -866,10 +1040,12 @@ class ScenarioSimulator():
|
|
|
866
1040
|
|
|
867
1041
|
# Build graph describing the topology
|
|
868
1042
|
nodes = []
|
|
869
|
-
for node_id, node_elevation, node_type,
|
|
870
|
-
|
|
1043
|
+
for node_id, node_elevation, node_type, \
|
|
1044
|
+
node_coord, node_comment in zip(nodes_id, nodes_elevation, nodes_type, nodes_coord,
|
|
1045
|
+
nodes_comments):
|
|
871
1046
|
node_info = {"elevation": node_elevation,
|
|
872
1047
|
"coord": node_coord,
|
|
1048
|
+
"comment": node_comment,
|
|
873
1049
|
"type": node_type}
|
|
874
1050
|
if node_type == "TANK":
|
|
875
1051
|
node_tank_idx = node_tank_names.index(node_id) + 1
|
|
@@ -878,9 +1054,10 @@ class ScenarioSimulator():
|
|
|
878
1054
|
nodes.append((node_id, node_info))
|
|
879
1055
|
|
|
880
1056
|
links = []
|
|
881
|
-
for link_id, link_type, link, diameter, length, roughness_coeff, bulk_coeff,
|
|
882
|
-
|
|
883
|
-
|
|
1057
|
+
for link_id, link_type, link, diameter, length, roughness_coeff, bulk_coeff, \
|
|
1058
|
+
wall_coeff, loss_coeff in zip(links_id, links_type, links_data, links_diameter,
|
|
1059
|
+
links_length, links_roughness_coeff, links_bulk_coeff,
|
|
1060
|
+
links_wall_coeff, links_loss_coeff):
|
|
884
1061
|
links.append((link_id, list(link),
|
|
885
1062
|
{"type": link_type, "diameter": diameter, "length": length,
|
|
886
1063
|
"roughness_coeff": roughness_coeff,
|
|
@@ -916,7 +1093,7 @@ class ScenarioSimulator():
|
|
|
916
1093
|
|
|
917
1094
|
The default is None.
|
|
918
1095
|
"""
|
|
919
|
-
from
|
|
1096
|
+
from ..visualization import ScenarioVisualizer
|
|
920
1097
|
ScenarioVisualizer(self).show_plot(export_to_file)
|
|
921
1098
|
|
|
922
1099
|
def randomize_demands(self) -> None:
|
|
@@ -989,7 +1166,10 @@ class ScenarioSimulator():
|
|
|
989
1166
|
raise ValueError(f"Inconsistent pattern shape '{pattern.shape}' " +
|
|
990
1167
|
"detected. Expected a one dimensional array!")
|
|
991
1168
|
|
|
992
|
-
self.epanet_api.addPattern(pattern_id, pattern)
|
|
1169
|
+
pattern_idx = self.epanet_api.addPattern(pattern_id, pattern)
|
|
1170
|
+
if pattern_idx == 0:
|
|
1171
|
+
raise RuntimeError("Failed to add pattern! " +
|
|
1172
|
+
"Maybe pattern name contains invalid characters or is too long?")
|
|
993
1173
|
|
|
994
1174
|
def get_node_demand_pattern(self, node_id: str) -> np.ndarray:
|
|
995
1175
|
"""
|
|
@@ -1063,23 +1243,128 @@ class ScenarioSimulator():
|
|
|
1063
1243
|
self.epanet_api.setNodeJunctionData(node_idx, self.epanet_api.getNodeElevations(node_idx),
|
|
1064
1244
|
base_demand, demand_pattern_id)
|
|
1065
1245
|
|
|
1066
|
-
def
|
|
1246
|
+
def add_custom_control(self, control: CustomControlModule) -> None:
|
|
1067
1247
|
"""
|
|
1068
|
-
Adds a control module to the scenario simulation.
|
|
1248
|
+
Adds a custom control module to the scenario simulation.
|
|
1069
1249
|
|
|
1070
1250
|
Parameters
|
|
1071
1251
|
----------
|
|
1072
|
-
control : :class:`~epyt_flow.simulation.scada.
|
|
1073
|
-
|
|
1252
|
+
control : :class:`~epyt_flow.simulation.scada.custom_control.CustomControlModule`
|
|
1253
|
+
Custom control module.
|
|
1074
1254
|
"""
|
|
1075
1255
|
self._adapt_to_network_changes()
|
|
1076
1256
|
|
|
1077
|
-
if not isinstance(control,
|
|
1257
|
+
if not isinstance(control, CustomControlModule):
|
|
1078
1258
|
raise TypeError("'control' must be an instance of " +
|
|
1079
|
-
"'epyt_flow.simulation.scada.
|
|
1259
|
+
"'epyt_flow.simulation.scada.CustomControlModule' not of " +
|
|
1080
1260
|
f"'{type(control)}'")
|
|
1081
1261
|
|
|
1082
|
-
self.
|
|
1262
|
+
self._custom_controls.append(control)
|
|
1263
|
+
|
|
1264
|
+
def add_simple_control(self, control: SimpleControlModule) -> None:
|
|
1265
|
+
"""
|
|
1266
|
+
Adds a simple EPANET control rule to the scenario simulation.
|
|
1267
|
+
|
|
1268
|
+
Parameters
|
|
1269
|
+
----------
|
|
1270
|
+
control : :class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`
|
|
1271
|
+
Simple EPANET control module.
|
|
1272
|
+
"""
|
|
1273
|
+
self._adapt_to_network_changes()
|
|
1274
|
+
|
|
1275
|
+
if not isinstance(control, SimpleControlModule):
|
|
1276
|
+
raise TypeError("'control' must be an instance of " +
|
|
1277
|
+
"'epyt_flow.simulation.scada.SimpleControlModule' not of " +
|
|
1278
|
+
f"'{type(control)}'")
|
|
1279
|
+
|
|
1280
|
+
if not any(c == control for c in self._simple_controls):
|
|
1281
|
+
self._simple_controls.append(control)
|
|
1282
|
+
self.epanet_api.addControls(str(control))
|
|
1283
|
+
|
|
1284
|
+
def remove_all_simple_controls(self) -> None:
|
|
1285
|
+
"""
|
|
1286
|
+
Removes all simple EPANET controls from the scenario.
|
|
1287
|
+
"""
|
|
1288
|
+
self.epanet_api.deleteControls()
|
|
1289
|
+
self._simple_controls = []
|
|
1290
|
+
|
|
1291
|
+
def remove_simple_control(self, control: SimpleControlModule) -> None:
|
|
1292
|
+
"""
|
|
1293
|
+
Removes a given simple EPANET control rule from the scenario.
|
|
1294
|
+
|
|
1295
|
+
Parameters
|
|
1296
|
+
----------
|
|
1297
|
+
control : :class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`
|
|
1298
|
+
Simple EPANET control module to be removed.
|
|
1299
|
+
"""
|
|
1300
|
+
self._adapt_to_network_changes()
|
|
1301
|
+
|
|
1302
|
+
if not isinstance(control, SimpleControlModule):
|
|
1303
|
+
raise TypeError("'control' must be an instance of " +
|
|
1304
|
+
"'epyt_flow.simulation.scada.SimpleControlModule' not of " +
|
|
1305
|
+
f"'{type(control)}'")
|
|
1306
|
+
|
|
1307
|
+
control_idx = None
|
|
1308
|
+
for idx, c in enumerate(self._simple_controls):
|
|
1309
|
+
if c == control:
|
|
1310
|
+
control_idx = idx + 1
|
|
1311
|
+
break
|
|
1312
|
+
if control_idx is None:
|
|
1313
|
+
raise ValueError("Invalid/Unknown control module.")
|
|
1314
|
+
|
|
1315
|
+
self.epanet_api.deleteControls(control_idx)
|
|
1316
|
+
self._simple_controls.remove(control)
|
|
1317
|
+
|
|
1318
|
+
def add_complex_control(self, control: ComplexControlModule) -> None:
|
|
1319
|
+
"""
|
|
1320
|
+
Adds an complex (IF-THEN-ELSE) EPANET control rule to the scenario simulation.
|
|
1321
|
+
|
|
1322
|
+
Parameters
|
|
1323
|
+
----------
|
|
1324
|
+
control : :class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`
|
|
1325
|
+
Complex EPANET control module.
|
|
1326
|
+
"""
|
|
1327
|
+
self._adapt_to_network_changes()
|
|
1328
|
+
|
|
1329
|
+
if not isinstance(control, ComplexControlModule):
|
|
1330
|
+
raise TypeError("'control' must be an instance of " +
|
|
1331
|
+
"'epyt_flow.simulation.scada.ComplexControlModule' not of " +
|
|
1332
|
+
f"'{type(control)}'")
|
|
1333
|
+
|
|
1334
|
+
if not any(c == control for c in self._complex_controls):
|
|
1335
|
+
self._complex_controls.append(control)
|
|
1336
|
+
self.epanet_api.addRules(str(control))
|
|
1337
|
+
|
|
1338
|
+
def remove_all_complex_controls(self) -> None:
|
|
1339
|
+
"""
|
|
1340
|
+
Removes all complex EPANET controls from the scenario.
|
|
1341
|
+
"""
|
|
1342
|
+
self.epanet_api.deleteRules()
|
|
1343
|
+
self._complex_controls = []
|
|
1344
|
+
|
|
1345
|
+
def remove_complex_control(self, control: ComplexControlModule) -> None:
|
|
1346
|
+
"""
|
|
1347
|
+
Removes a given complex (IF-THEN-ELSE) EPANET control rule from the scenario.
|
|
1348
|
+
|
|
1349
|
+
Parameters
|
|
1350
|
+
----------
|
|
1351
|
+
control : :class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`
|
|
1352
|
+
Complex EPANET control module to be removed.
|
|
1353
|
+
"""
|
|
1354
|
+
self._adapt_to_network_changes()
|
|
1355
|
+
|
|
1356
|
+
if not isinstance(control, ComplexControlModule):
|
|
1357
|
+
raise TypeError("'control' must be an instance of " +
|
|
1358
|
+
"'epyt_flow.simulation.scada.ComplexControlModule' not of " +
|
|
1359
|
+
f"'{type(control)}'")
|
|
1360
|
+
|
|
1361
|
+
if control.rule_id not in self.epanet_api.getRuleID():
|
|
1362
|
+
raise ValueError("Invalid/Unknown control module. " +
|
|
1363
|
+
f"Can not find rule ID '{control.rule_id}'")
|
|
1364
|
+
|
|
1365
|
+
rule_idx = self.epanet_api.getRuleID().index(control.rule_id) + 1
|
|
1366
|
+
self.epanet_api.deleteRules(rule_idx)
|
|
1367
|
+
self._complex_controls.remove(control)
|
|
1083
1368
|
|
|
1084
1369
|
def add_leakage(self, leakage_event: Leakage) -> None:
|
|
1085
1370
|
"""
|
|
@@ -1616,12 +1901,14 @@ class ScenarioSimulator():
|
|
|
1616
1901
|
for event in self._system_events:
|
|
1617
1902
|
event.reset()
|
|
1618
1903
|
|
|
1619
|
-
if self.
|
|
1620
|
-
for
|
|
1621
|
-
|
|
1904
|
+
if self._custom_controls is not None:
|
|
1905
|
+
for control in self._custom_controls:
|
|
1906
|
+
control.init(self.epanet_api)
|
|
1622
1907
|
|
|
1623
1908
|
def run_advanced_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
|
|
1624
|
-
frozen_sensor_config: bool = False
|
|
1909
|
+
frozen_sensor_config: bool = False,
|
|
1910
|
+
use_quality_time_step_as_reporting_time_step: bool = False
|
|
1911
|
+
) -> ScadaData:
|
|
1625
1912
|
"""
|
|
1626
1913
|
Runs an advanced quality analysis using EPANET-MSX.
|
|
1627
1914
|
|
|
@@ -1638,6 +1925,13 @@ class ScenarioSimulator():
|
|
|
1638
1925
|
If True, the sensor config can not be changed and only the required sensor nodes/links
|
|
1639
1926
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
1640
1927
|
|
|
1928
|
+
The default is False.
|
|
1929
|
+
use_quality_time_step_as_reporting_time_step : `bool`, optional
|
|
1930
|
+
If True, the water quality time step will be used as the reporting time step.
|
|
1931
|
+
|
|
1932
|
+
As a consequence, the simualtion results can not be merged
|
|
1933
|
+
with the hydraulic simulation.
|
|
1934
|
+
|
|
1641
1935
|
The default is False.
|
|
1642
1936
|
|
|
1643
1937
|
Returns
|
|
@@ -1657,7 +1951,9 @@ class ScenarioSimulator():
|
|
|
1657
1951
|
for scada_data in gen(hyd_file_in=hyd_file_in,
|
|
1658
1952
|
verbose=verbose,
|
|
1659
1953
|
return_as_dict=True,
|
|
1660
|
-
frozen_sensor_config=frozen_sensor_config
|
|
1954
|
+
frozen_sensor_config=frozen_sensor_config,
|
|
1955
|
+
use_quality_time_step_as_reporting_time_step=
|
|
1956
|
+
use_quality_time_step_as_reporting_time_step):
|
|
1661
1957
|
if result is None:
|
|
1662
1958
|
result = {}
|
|
1663
1959
|
for data_type, data in scada_data.items():
|
|
@@ -1674,6 +1970,7 @@ class ScenarioSimulator():
|
|
|
1674
1970
|
result[data_type] = None
|
|
1675
1971
|
|
|
1676
1972
|
return ScadaData(**result,
|
|
1973
|
+
network_topo=self.get_topology(),
|
|
1677
1974
|
sensor_config=self._sensor_config,
|
|
1678
1975
|
sensor_reading_events=self._sensor_reading_events,
|
|
1679
1976
|
sensor_noise=self._sensor_noise,
|
|
@@ -1682,7 +1979,8 @@ class ScenarioSimulator():
|
|
|
1682
1979
|
def run_advanced_quality_simulation_as_generator(self, hyd_file_in: str, verbose: bool = False,
|
|
1683
1980
|
support_abort: bool = False,
|
|
1684
1981
|
return_as_dict: bool = False,
|
|
1685
|
-
frozen_sensor_config: bool = False
|
|
1982
|
+
frozen_sensor_config: bool = False,
|
|
1983
|
+
use_quality_time_step_as_reporting_time_step: bool = False,
|
|
1686
1984
|
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
1687
1985
|
"""
|
|
1688
1986
|
Runs an advanced quality analysis using EPANET-MSX.
|
|
@@ -1704,6 +2002,13 @@ class ScenarioSimulator():
|
|
|
1704
2002
|
If True, the sensor config can not be changed and only the required sensor nodes/links
|
|
1705
2003
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
1706
2004
|
|
|
2005
|
+
The default is False.
|
|
2006
|
+
use_quality_time_step_as_reporting_time_step : `bool`, optional
|
|
2007
|
+
If True, the water quality time step will be used as the reporting time step.
|
|
2008
|
+
|
|
2009
|
+
As a consequence, the simualtion results can not be merged
|
|
2010
|
+
with the hydraulic simulation.
|
|
2011
|
+
|
|
1707
2012
|
The default is False.
|
|
1708
2013
|
|
|
1709
2014
|
Returns
|
|
@@ -1729,6 +2034,11 @@ class ScenarioSimulator():
|
|
|
1729
2034
|
reporting_time_step = self.epanet_api.getTimeReportingStep()
|
|
1730
2035
|
hyd_time_step = self.epanet_api.getTimeHydraulicStep()
|
|
1731
2036
|
|
|
2037
|
+
if use_quality_time_step_as_reporting_time_step is True:
|
|
2038
|
+
quality_time_step = self.epanet_api.getMSXTimeStep()
|
|
2039
|
+
reporting_time_step = quality_time_step
|
|
2040
|
+
hyd_time_step = quality_time_step
|
|
2041
|
+
|
|
1732
2042
|
self.epanet_api.initializeMSXQualityAnalysis(ToolkitConstants.EN_NOSAVE)
|
|
1733
2043
|
|
|
1734
2044
|
self.__running_simulation = True
|
|
@@ -1814,7 +2124,7 @@ class ScenarioSimulator():
|
|
|
1814
2124
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
1815
2125
|
"sensor_readings_time": np.array([0])}
|
|
1816
2126
|
else:
|
|
1817
|
-
data = ScadaData(sensor_config=self._sensor_config,
|
|
2127
|
+
data = ScadaData(network_topo=self.get_topology(), sensor_config=self._sensor_config,
|
|
1818
2128
|
bulk_species_node_concentration_raw=bulk_species_node_concentrations,
|
|
1819
2129
|
bulk_species_link_concentration_raw=bulk_species_link_concentrations,
|
|
1820
2130
|
surface_species_concentration_raw=surface_species_concentrations,
|
|
@@ -1858,7 +2168,8 @@ class ScenarioSimulator():
|
|
|
1858
2168
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
1859
2169
|
"sensor_readings_time": np.array([total_time])}
|
|
1860
2170
|
else:
|
|
1861
|
-
data = ScadaData(
|
|
2171
|
+
data = ScadaData(network_topo=self.get_topology(),
|
|
2172
|
+
sensor_config=self._sensor_config,
|
|
1862
2173
|
bulk_species_node_concentration_raw=
|
|
1863
2174
|
bulk_species_node_concentrations,
|
|
1864
2175
|
bulk_species_link_concentration_raw=
|
|
@@ -1880,7 +2191,9 @@ class ScenarioSimulator():
|
|
|
1880
2191
|
self.__running_simulation = False
|
|
1881
2192
|
|
|
1882
2193
|
def run_basic_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
|
|
1883
|
-
frozen_sensor_config: bool = False
|
|
2194
|
+
frozen_sensor_config: bool = False,
|
|
2195
|
+
use_quality_time_step_as_reporting_time_step: bool = False
|
|
2196
|
+
) -> ScadaData:
|
|
1884
2197
|
"""
|
|
1885
2198
|
Runs a basic quality analysis using EPANET.
|
|
1886
2199
|
|
|
@@ -1897,6 +2210,13 @@ class ScenarioSimulator():
|
|
|
1897
2210
|
If True, the sensor config can not be changed and only the required sensor nodes/links
|
|
1898
2211
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
1899
2212
|
|
|
2213
|
+
The default is False.
|
|
2214
|
+
use_quality_time_step_as_reporting_time_step : `bool`, optional
|
|
2215
|
+
If True, the water quality time step will be used as the reporting time step.
|
|
2216
|
+
|
|
2217
|
+
As a consequence, the simualtion results can not be merged
|
|
2218
|
+
with the hydraulic simulation.
|
|
2219
|
+
|
|
1900
2220
|
The default is False.
|
|
1901
2221
|
|
|
1902
2222
|
Returns
|
|
@@ -1914,7 +2234,9 @@ class ScenarioSimulator():
|
|
|
1914
2234
|
for scada_data in gen(hyd_file_in=hyd_file_in,
|
|
1915
2235
|
verbose=verbose,
|
|
1916
2236
|
return_as_dict=True,
|
|
1917
|
-
frozen_sensor_config=frozen_sensor_config
|
|
2237
|
+
frozen_sensor_config=frozen_sensor_config,
|
|
2238
|
+
use_quality_time_step_as_reporting_time_step=
|
|
2239
|
+
use_quality_time_step_as_reporting_time_step):
|
|
1918
2240
|
if result is None:
|
|
1919
2241
|
result = {}
|
|
1920
2242
|
for data_type, data in scada_data.items():
|
|
@@ -1928,6 +2250,7 @@ class ScenarioSimulator():
|
|
|
1928
2250
|
result[data_type] = np.concatenate(result[data_type], axis=0)
|
|
1929
2251
|
|
|
1930
2252
|
return ScadaData(**result,
|
|
2253
|
+
network_topo=self.get_topology(),
|
|
1931
2254
|
sensor_config=self._sensor_config,
|
|
1932
2255
|
sensor_reading_events=self._sensor_reading_events,
|
|
1933
2256
|
sensor_noise=self._sensor_noise,
|
|
@@ -1937,6 +2260,7 @@ class ScenarioSimulator():
|
|
|
1937
2260
|
support_abort: bool = False,
|
|
1938
2261
|
return_as_dict: bool = False,
|
|
1939
2262
|
frozen_sensor_config: bool = False,
|
|
2263
|
+
use_quality_time_step_as_reporting_time_step: bool = False
|
|
1940
2264
|
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
1941
2265
|
"""
|
|
1942
2266
|
Runs a basic quality analysis using EPANET.
|
|
@@ -1960,6 +2284,13 @@ class ScenarioSimulator():
|
|
|
1960
2284
|
If True, the sensor config can not be changed and only the required sensor nodes/links
|
|
1961
2285
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
1962
2286
|
|
|
2287
|
+
The default is False.
|
|
2288
|
+
use_quality_time_step_as_reporting_time_step : `bool`, optional
|
|
2289
|
+
If True, the water quality time step will be used as the reporting time step.
|
|
2290
|
+
|
|
2291
|
+
As a consequence, the simualtion results can not be merged
|
|
2292
|
+
with the hydraulic simulation.
|
|
2293
|
+
|
|
1963
2294
|
The default is False.
|
|
1964
2295
|
|
|
1965
2296
|
Returns
|
|
@@ -1974,6 +2305,11 @@ class ScenarioSimulator():
|
|
|
1974
2305
|
reporting_time_start = self.epanet_api.getTimeReportingStart()
|
|
1975
2306
|
reporting_time_step = self.epanet_api.getTimeReportingStep()
|
|
1976
2307
|
|
|
2308
|
+
if use_quality_time_step_as_reporting_time_step is True:
|
|
2309
|
+
quality_time_step = self.epanet_api.getTimeQualityStep()
|
|
2310
|
+
requested_time_step = quality_time_step
|
|
2311
|
+
reporting_time_step = quality_time_step
|
|
2312
|
+
|
|
1977
2313
|
self.epanet_api.useHydraulicFile(hyd_file_in)
|
|
1978
2314
|
|
|
1979
2315
|
self.epanet_api.openQualityAnalysis()
|
|
@@ -2016,7 +2352,8 @@ class ScenarioSimulator():
|
|
|
2016
2352
|
"link_quality_data_raw": quality_link_data,
|
|
2017
2353
|
"sensor_readings_time": np.array([total_time])}
|
|
2018
2354
|
else:
|
|
2019
|
-
data = ScadaData(
|
|
2355
|
+
data = ScadaData(network_topo=self.get_topology(),
|
|
2356
|
+
sensor_config=self._sensor_config,
|
|
2020
2357
|
node_quality_data_raw=quality_node_data,
|
|
2021
2358
|
link_quality_data_raw=quality_link_data,
|
|
2022
2359
|
sensor_readings_time=np.array([total_time]),
|
|
@@ -2032,7 +2369,7 @@ class ScenarioSimulator():
|
|
|
2032
2369
|
yield data
|
|
2033
2370
|
|
|
2034
2371
|
# Next
|
|
2035
|
-
tstep = self.epanet_api.
|
|
2372
|
+
tstep = self.epanet_api.stepQualityAnalysisTimeLeft()
|
|
2036
2373
|
|
|
2037
2374
|
self.epanet_api.closeHydraulicAnalysis()
|
|
2038
2375
|
|
|
@@ -2092,6 +2429,7 @@ class ScenarioSimulator():
|
|
|
2092
2429
|
result[data_type] = np.concatenate(result[data_type], axis=0)
|
|
2093
2430
|
|
|
2094
2431
|
result = ScadaData(**result,
|
|
2432
|
+
network_topo=self.get_topology(),
|
|
2095
2433
|
sensor_config=self._sensor_config,
|
|
2096
2434
|
sensor_reading_events=self._sensor_reading_events,
|
|
2097
2435
|
sensor_noise=self._sensor_noise,
|
|
@@ -2213,7 +2551,8 @@ class ScenarioSimulator():
|
|
|
2213
2551
|
link_valve_idx = self.epanet_api.getLinkValveIndex()
|
|
2214
2552
|
valves_state_data = self.epanet_api.getLinkStatus(link_valve_idx).reshape(1, -1)
|
|
2215
2553
|
|
|
2216
|
-
scada_data = ScadaData(
|
|
2554
|
+
scada_data = ScadaData(network_topo=self.get_topology(),
|
|
2555
|
+
sensor_config=self._sensor_config,
|
|
2217
2556
|
pressure_data_raw=pressure_data,
|
|
2218
2557
|
flow_data_raw=flow_data,
|
|
2219
2558
|
demand_data_raw=demand_data,
|
|
@@ -2254,7 +2593,7 @@ class ScenarioSimulator():
|
|
|
2254
2593
|
yield data
|
|
2255
2594
|
|
|
2256
2595
|
# Apply control modules
|
|
2257
|
-
for control in self.
|
|
2596
|
+
for control in self._custom_controls:
|
|
2258
2597
|
control.step(scada_data)
|
|
2259
2598
|
|
|
2260
2599
|
# Next
|
|
@@ -2272,16 +2611,6 @@ class ScenarioSimulator():
|
|
|
2272
2611
|
self.__running_simulation = False
|
|
2273
2612
|
raise ex
|
|
2274
2613
|
|
|
2275
|
-
def run_simulation_as_generator(self, hyd_export: str = None, verbose: bool = False,
|
|
2276
|
-
support_abort: bool = False,
|
|
2277
|
-
return_as_dict: bool = False,
|
|
2278
|
-
frozen_sensor_config: bool = False,
|
|
2279
|
-
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
2280
|
-
warnings.warn("'run_simulation_as_generator' is deprecated and will be removed in " +
|
|
2281
|
-
"future releases -- use 'run_hydraulic_simulation_as_generator' instead")
|
|
2282
|
-
return self.run_hydraulic_simulation_as_generator(hyd_export, verbose, support_abort,
|
|
2283
|
-
return_as_dict, frozen_sensor_config)
|
|
2284
|
-
|
|
2285
2614
|
def run_simulation(self, hyd_export: str = None, verbose: bool = False,
|
|
2286
2615
|
frozen_sensor_config: bool = False) -> ScadaData:
|
|
2287
2616
|
"""
|
|
@@ -2574,6 +2903,202 @@ class ScenarioSimulator():
|
|
|
2574
2903
|
|
|
2575
2904
|
return list(set(events_times))
|
|
2576
2905
|
|
|
2906
|
+
def set_pump_energy_price_pattern(self, pump_id: str, pattern: np.ndarray,
|
|
2907
|
+
pattern_id: Optional[str] = None) -> None:
|
|
2908
|
+
"""
|
|
2909
|
+
Specifies/sets the energy price pattern of a given pump.
|
|
2910
|
+
|
|
2911
|
+
Overwrites any existing (energy price) patterns of the given pump.
|
|
2912
|
+
|
|
2913
|
+
Parameters
|
|
2914
|
+
----------
|
|
2915
|
+
pump_id : `str`
|
|
2916
|
+
ID of the pump.
|
|
2917
|
+
pattern : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
2918
|
+
Pattern of multipliers.
|
|
2919
|
+
pattern_id : `str`, optional
|
|
2920
|
+
ID of the pattern.
|
|
2921
|
+
If not specified, 'energy_price_{pump_id}' will be used as the pattern ID.
|
|
2922
|
+
|
|
2923
|
+
The default is None.
|
|
2924
|
+
"""
|
|
2925
|
+
if not isinstance(pump_id, str):
|
|
2926
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
2927
|
+
if pump_id not in self._sensor_config.pumps:
|
|
2928
|
+
raise ValueError(f"Unknown pump '{pump_id}'")
|
|
2929
|
+
if not isinstance(pattern, np.ndarray):
|
|
2930
|
+
raise TypeError("'pattern' must be an instance of 'numpy.ndarray' " +
|
|
2931
|
+
f"but no of '{type(pattern)}'")
|
|
2932
|
+
if len(pattern.shape) > 1:
|
|
2933
|
+
raise ValueError("'pattern' must be 1-dimensional")
|
|
2934
|
+
if pattern_id is not None:
|
|
2935
|
+
if not isinstance(pattern_id, str):
|
|
2936
|
+
raise TypeError("'pattern_id' must be an instance of 'str' " +
|
|
2937
|
+
f"but not of '{type(pattern_id)}'")
|
|
2938
|
+
else:
|
|
2939
|
+
pattern_id = f"energy_price_{pump_id}"
|
|
2940
|
+
|
|
2941
|
+
pattern_idx = self.epanet_api.getPatternIndex(pattern_id)
|
|
2942
|
+
if pattern_idx != 0:
|
|
2943
|
+
warnings.warn(f"Overwriting existing pattern '{pattern_id}'")
|
|
2944
|
+
|
|
2945
|
+
pump_idx = self.epanet_api.getLinkIndex(pump_id)
|
|
2946
|
+
pattern_idx = self.epanet_api.getLinkPumpEPat(pump_idx)
|
|
2947
|
+
if pattern_idx != 0:
|
|
2948
|
+
warnings.warn(f"Overwriting existing energy price pattern of pump '{pump_id}'")
|
|
2949
|
+
|
|
2950
|
+
self.add_pattern(pattern_id, pattern)
|
|
2951
|
+
pattern_idx = self.epanet_api.getPatternIndex(pattern_id)
|
|
2952
|
+
self.epanet_api.setLinkPumpEPat(pattern_idx)
|
|
2953
|
+
|
|
2954
|
+
def get_pump_energy_price_pattern(self, pump_id: str) -> np.ndarray:
|
|
2955
|
+
"""
|
|
2956
|
+
Returns the energy price pattern of a given pump.
|
|
2957
|
+
|
|
2958
|
+
Parameters
|
|
2959
|
+
----------
|
|
2960
|
+
pump_id : `str`
|
|
2961
|
+
ID of the pump.
|
|
2962
|
+
|
|
2963
|
+
Returns
|
|
2964
|
+
-------
|
|
2965
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
2966
|
+
Energy price pattern. None, if none exists.
|
|
2967
|
+
"""
|
|
2968
|
+
if not isinstance(pump_id, str):
|
|
2969
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
2970
|
+
if pump_id not in self._sensor_config.pumps:
|
|
2971
|
+
raise ValueError(f"Unknown pump '{pump_id}'")
|
|
2972
|
+
|
|
2973
|
+
pump_idx = self.epanet_api.getLinkIndex(pump_id)
|
|
2974
|
+
pattern_idx = self.epanet_api.getLinkPumpEPat(pump_idx)
|
|
2975
|
+
if pattern_idx == 0:
|
|
2976
|
+
return None
|
|
2977
|
+
else:
|
|
2978
|
+
pattern_length = self.epanet_api.getPatternLengths(pattern_idx)
|
|
2979
|
+
return np.array([self.epanet_api.getPatternValue(pattern_idx, t+1)
|
|
2980
|
+
for t in range(pattern_length)])
|
|
2981
|
+
|
|
2982
|
+
def get_pump_energy_price(self, pump_id: str) -> float:
|
|
2983
|
+
"""
|
|
2984
|
+
Returns the energy price of a given pump.
|
|
2985
|
+
|
|
2986
|
+
Parameters
|
|
2987
|
+
----------
|
|
2988
|
+
pump_id : `str`
|
|
2989
|
+
ID of the pump.
|
|
2990
|
+
|
|
2991
|
+
Returns
|
|
2992
|
+
-------
|
|
2993
|
+
`float`
|
|
2994
|
+
Energy price.
|
|
2995
|
+
"""
|
|
2996
|
+
if not isinstance(pump_id, str):
|
|
2997
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
2998
|
+
if pump_id not in self._sensor_config.pumps:
|
|
2999
|
+
raise ValueError(f"Unknown pump '{pump_id}'")
|
|
3000
|
+
|
|
3001
|
+
pump_idx = self.epanet_api.getLinkIndex(pump_id)
|
|
3002
|
+
return self.epanet_api.getLinkPumpECost(pump_idx)
|
|
3003
|
+
|
|
3004
|
+
def set_pump_energy_price(self, pump_id, price: float) -> None:
|
|
3005
|
+
"""
|
|
3006
|
+
Sets the energy price of a given pump.
|
|
3007
|
+
|
|
3008
|
+
Parameters
|
|
3009
|
+
----------
|
|
3010
|
+
pump_id : `str`
|
|
3011
|
+
ID of the pump.
|
|
3012
|
+
price : `float`
|
|
3013
|
+
Energy price.
|
|
3014
|
+
"""
|
|
3015
|
+
if not isinstance(pump_id, str):
|
|
3016
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
3017
|
+
if pump_id not in self._sensor_config.pumps:
|
|
3018
|
+
raise ValueError(f"Unknown pump '{pump_id}'")
|
|
3019
|
+
if not isinstance(price, float):
|
|
3020
|
+
raise TypeError(f"'price' must be an instance of 'float' but not of '{type(price)}'")
|
|
3021
|
+
if price <= 0:
|
|
3022
|
+
raise ValueError("'price' must be positive")
|
|
3023
|
+
|
|
3024
|
+
pump_idx = self._sensor_config.pumps.index(pump_id) + 1
|
|
3025
|
+
pumps_energy_price = self.epanet_api.getLinkPumpECost()
|
|
3026
|
+
pumps_energy_price[pump_idx - 1] = price
|
|
3027
|
+
|
|
3028
|
+
self.epanet_api.setLinkPumpECost(pumps_energy_price)
|
|
3029
|
+
|
|
3030
|
+
def set_initial_link_status(self, link_id: str, status: int) -> None:
|
|
3031
|
+
"""
|
|
3032
|
+
Sets the initial status (open or closed) of a given link.
|
|
3033
|
+
|
|
3034
|
+
Parameters
|
|
3035
|
+
----------
|
|
3036
|
+
link_id : `str`
|
|
3037
|
+
ID of the link.
|
|
3038
|
+
status : `int`
|
|
3039
|
+
Initial status of the link. Must be one of the following EPANET constants:
|
|
3040
|
+
|
|
3041
|
+
- EN_CLOSED = 0
|
|
3042
|
+
- EN_OPEN = 1
|
|
3043
|
+
"""
|
|
3044
|
+
if not isinstance(link_id, str):
|
|
3045
|
+
raise TypeError(f"'link_id' must be an instance of 'str' but not of '{type(link_id)}'")
|
|
3046
|
+
if link_id not in self._sensor_config.pumps:
|
|
3047
|
+
raise ValueError("Invalid link ID '{link_id}'")
|
|
3048
|
+
if not isinstance(status, int):
|
|
3049
|
+
raise TypeError(f"'status' must be an instance of 'int' but not of '{type(status)}'")
|
|
3050
|
+
if status not in [ActuatorConstants.EN_CLOSED, ActuatorConstants.EN_OPEN]:
|
|
3051
|
+
raise ValueError(f"Invalid link status '{status}'")
|
|
3052
|
+
|
|
3053
|
+
link_idx = self.epanet_api.getLinkIndex(link_id)
|
|
3054
|
+
self.epanet_api.setLinkInitialStatus(link_idx, status)
|
|
3055
|
+
|
|
3056
|
+
def set_initial_pump_speed(self, pump_id: str, speed: float) -> None:
|
|
3057
|
+
"""
|
|
3058
|
+
Sets the initial pump speed of a given pump.
|
|
3059
|
+
|
|
3060
|
+
Parameters
|
|
3061
|
+
----------
|
|
3062
|
+
pump_id : `str`
|
|
3063
|
+
ID of the pump.
|
|
3064
|
+
speed : `float`
|
|
3065
|
+
Initial speed of the pump.
|
|
3066
|
+
"""
|
|
3067
|
+
if not isinstance(pump_id, str):
|
|
3068
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
3069
|
+
if pump_id not in self._sensor_config.pumps:
|
|
3070
|
+
raise ValueError("Invalid pump ID '{tank_id}'")
|
|
3071
|
+
if not isinstance(speed, float):
|
|
3072
|
+
raise TypeError(f"'speed' must be an instance of 'int' but not of '{type(speed)}'")
|
|
3073
|
+
if speed < 0:
|
|
3074
|
+
raise ValueError("'speed' can not be negative")
|
|
3075
|
+
|
|
3076
|
+
pump_idx = self.epanet_api.getLinkIndex(pump_id)
|
|
3077
|
+
self.epanet_api.setLinkInitialSetting(pump_idx, speed)
|
|
3078
|
+
|
|
3079
|
+
def set_initial_tank_level(self, tank_id, level: int) -> None:
|
|
3080
|
+
"""
|
|
3081
|
+
Sets the initial water level of a given tank.
|
|
3082
|
+
|
|
3083
|
+
Parameters
|
|
3084
|
+
----------
|
|
3085
|
+
tank_id : `str`
|
|
3086
|
+
ID of the tank.
|
|
3087
|
+
level : `int`
|
|
3088
|
+
Initial water level in the tank.
|
|
3089
|
+
"""
|
|
3090
|
+
if not isinstance(tank_id, str):
|
|
3091
|
+
raise TypeError(f"'tank_id' must be an instance of 'str' but not of '{type(tank_id)}'")
|
|
3092
|
+
if tank_id not in self._sensor_config.tanks:
|
|
3093
|
+
raise ValueError("Invalid tank ID '{tank_id}'")
|
|
3094
|
+
if not isinstance(level, int):
|
|
3095
|
+
raise TypeError(f"'level' must be an instance of 'int' but not of '{type(level)}'")
|
|
3096
|
+
if level < 0:
|
|
3097
|
+
raise ValueError("'level' can not be negative")
|
|
3098
|
+
|
|
3099
|
+
tank_idx = self.epanet_api.getNodeIndex(tank_id)
|
|
3100
|
+
self.epanet_api.setNodeTankInitialLevel(tank_idx, level)
|
|
3101
|
+
|
|
2577
3102
|
def __warn_if_quality_set(self):
|
|
2578
3103
|
qual_info = self.epanet_api.getQualityInfo()
|
|
2579
3104
|
if qual_info.QualityCode != ToolkitConstants.EN_NONE:
|
|
@@ -2686,7 +3211,7 @@ class ScenarioSimulator():
|
|
|
2686
3211
|
if pattern is None and pattern_id is None:
|
|
2687
3212
|
raise ValueError("'pattern_id' and 'pattern' can not be None at the same time")
|
|
2688
3213
|
if pattern_id is None:
|
|
2689
|
-
pattern_id = f"
|
|
3214
|
+
pattern_id = f"qual_src_pat_{node_id}"
|
|
2690
3215
|
|
|
2691
3216
|
node_idx = self.epanet_api.getNodeIndex(node_id)
|
|
2692
3217
|
|
|
@@ -2694,11 +3219,220 @@ class ScenarioSimulator():
|
|
|
2694
3219
|
pattern_idx = self.epanet_api.getPatternIndex(pattern_id)
|
|
2695
3220
|
else:
|
|
2696
3221
|
pattern_idx = self.epanet_api.addPattern(pattern_id, pattern)
|
|
3222
|
+
if pattern_idx == 0:
|
|
3223
|
+
raise RuntimeError("Failed to add/get pattern! " +
|
|
3224
|
+
"Maybe pattern name contains invalid characters or is too long?")
|
|
2697
3225
|
|
|
2698
3226
|
self.epanet_api.api.ENsetnodevalue(node_idx, ToolkitConstants.EN_SOURCETYPE, source_type)
|
|
2699
3227
|
self.epanet_api.setNodeSourceQuality(node_idx, source_strength)
|
|
2700
3228
|
self.epanet_api.setNodeSourcePatternIndex(node_idx, pattern_idx)
|
|
2701
3229
|
|
|
3230
|
+
def set_initial_node_quality(self, node_id: str, initial_quality: float) -> None:
|
|
3231
|
+
"""
|
|
3232
|
+
Specifies the initial quality at a given node.
|
|
3233
|
+
Quality represents concentration for chemicals, hours for water age,
|
|
3234
|
+
or percent for source tracing.
|
|
3235
|
+
|
|
3236
|
+
Parameters
|
|
3237
|
+
----------
|
|
3238
|
+
node_id : `str`
|
|
3239
|
+
ID of the node.
|
|
3240
|
+
initial_quality : `float`
|
|
3241
|
+
Initial node quality.
|
|
3242
|
+
"""
|
|
3243
|
+
self.set_quality_parameters(initial_quality={node_id, initial_quality})
|
|
3244
|
+
|
|
3245
|
+
def set_quality_parameters(self, initial_quality: Optional[dict[str, float]] = None,
|
|
3246
|
+
order_wall: Optional[int] = None, order_tank: Optional[int] = None,
|
|
3247
|
+
order_bulk: Optional[int] = None,
|
|
3248
|
+
global_wall_reaction_coefficient: Optional[float] = None,
|
|
3249
|
+
global_bulk_reaction_coefficient: Optional[float] = None,
|
|
3250
|
+
local_wall_reaction_coefficient: Optional[dict[str, float]] = None,
|
|
3251
|
+
local_bulk_reaction_coefficient: Optional[dict[str, float]] = None,
|
|
3252
|
+
local_tank_reaction_coefficient: Optional[dict[str, float]] = None,
|
|
3253
|
+
limiting_potential: Optional[float] = None) -> None:
|
|
3254
|
+
"""
|
|
3255
|
+
Specifies some parameters of the EPANET quality analysis.
|
|
3256
|
+
Note that those parameters are only relevant for EPANET but not for EPANET-MSX.
|
|
3257
|
+
|
|
3258
|
+
Parameters
|
|
3259
|
+
----------
|
|
3260
|
+
initial_quality : `dict[str, float]`, optional
|
|
3261
|
+
Specifies the initial quality (value in the dictionary) at nodes
|
|
3262
|
+
(key in the dictionary).
|
|
3263
|
+
Quality represents concentration for chemicals, hours for water age,
|
|
3264
|
+
or percent for source tracing.
|
|
3265
|
+
|
|
3266
|
+
The default is None.
|
|
3267
|
+
order_wall : `int`, optional
|
|
3268
|
+
Specifies the order of reactions occurring in the bulk fluid at pipe walls.
|
|
3269
|
+
Value for wall reactions must be either 0 or 1.
|
|
3270
|
+
If not specified, the default reaction order is 1.0.
|
|
3271
|
+
|
|
3272
|
+
The default is None.
|
|
3273
|
+
order_bulk : `int`, optional
|
|
3274
|
+
Specifies the order of reactions occurring in the bulk fluid in tanks.
|
|
3275
|
+
Value must be either 0 or 1.
|
|
3276
|
+
If not specified, the default reaction order is 1.0.
|
|
3277
|
+
|
|
3278
|
+
The default is None.
|
|
3279
|
+
global_wall_reaction_coefficient : `float`, optional
|
|
3280
|
+
Specifies the global value for all pipe wall reaction coefficients (pipes and tanks).
|
|
3281
|
+
If not specified, the default value is zero.
|
|
3282
|
+
|
|
3283
|
+
The default is None.
|
|
3284
|
+
global_bulk_reaction_coefficient : `float`, optional
|
|
3285
|
+
Specifies the global value for all bulk reaction coefficients (pipes and tanks).
|
|
3286
|
+
If not specified, the default value is zero.
|
|
3287
|
+
|
|
3288
|
+
The default is None.
|
|
3289
|
+
local_wall_reaction_coefficient : `dict[str, float]`, optional
|
|
3290
|
+
Overrides the global reaction coefficients for specific pipes (key in dictionary).
|
|
3291
|
+
|
|
3292
|
+
The default is None.
|
|
3293
|
+
local_bulk_reaction_coefficient : `dict[str, float]`, optional
|
|
3294
|
+
Overrides the global reaction coefficients for specific pipes (key in dictionary).
|
|
3295
|
+
|
|
3296
|
+
The default is None.
|
|
3297
|
+
local_tank_reaction_coefficient : `dict[str, float]`, optional
|
|
3298
|
+
Overrides the global reaction coefficients for specific tanks (key in dictionary).
|
|
3299
|
+
|
|
3300
|
+
The default is None.
|
|
3301
|
+
limiting_potential : `float`, optional
|
|
3302
|
+
Specifies that reaction rates are proportional to the difference between the
|
|
3303
|
+
current concentration and some (specified) limiting potential value.
|
|
3304
|
+
|
|
3305
|
+
The default is None.
|
|
3306
|
+
"""
|
|
3307
|
+
if initial_quality is not None:
|
|
3308
|
+
if not isinstance(initial_quality, dict):
|
|
3309
|
+
raise TypeError("'initial_quality' must be an instance of 'dict[str, float]' " +
|
|
3310
|
+
f"but not of '{type(initial_quality)}'")
|
|
3311
|
+
if any(not isinstance(key, str) or not isinstance(value, float)
|
|
3312
|
+
for key, value in initial_quality):
|
|
3313
|
+
raise TypeError("'initial_quality' must be an instance of 'dict[str, float]'")
|
|
3314
|
+
for node_id, node_init_qual in initial_quality:
|
|
3315
|
+
if node_id not in self._sensor_config.nodes:
|
|
3316
|
+
raise ValueError(f"Invalid node ID '{node_id}'")
|
|
3317
|
+
if node_init_qual < 0:
|
|
3318
|
+
raise ValueError(f"{node_id}: Initial node quality can not be negative")
|
|
3319
|
+
|
|
3320
|
+
init_qual = self.epanet_api.getNodeInitialQuality()
|
|
3321
|
+
for node_id, node_init_qual in initial_quality:
|
|
3322
|
+
node_idx = self.epanet_api.getNodeIndex(node_id) - 1
|
|
3323
|
+
init_qual[node_idx] = node_init_qual
|
|
3324
|
+
|
|
3325
|
+
self.epanet_api.setNodeInitialQuality(init_qual)
|
|
3326
|
+
|
|
3327
|
+
if order_wall is not None:
|
|
3328
|
+
if not isinstance(order_wall, int):
|
|
3329
|
+
raise TypeError("'order_wall' must be an instance of 'int' " +
|
|
3330
|
+
f"but not of '{type(order_wall)}'")
|
|
3331
|
+
if order_wall not in [0, 1]:
|
|
3332
|
+
raise ValueError(f"Invalid value '{order_wall}' for order_wall")
|
|
3333
|
+
|
|
3334
|
+
self.epanet_api.setOptionsPipeWallReactionOrder(order_wall)
|
|
3335
|
+
|
|
3336
|
+
if order_bulk is not None:
|
|
3337
|
+
if not isinstance(order_bulk, int):
|
|
3338
|
+
raise TypeError("'order_bulk' must be an instance of 'int' " +
|
|
3339
|
+
f"but not of '{type(order_bulk)}'")
|
|
3340
|
+
if order_bulk not in [0, 1]:
|
|
3341
|
+
raise ValueError(f"Invalid value '{order_bulk}' for order_bulk")
|
|
3342
|
+
|
|
3343
|
+
self.epanet_api.setOptionsPipeBulkReactionOrder(order_bulk)
|
|
3344
|
+
|
|
3345
|
+
if order_tank is not None:
|
|
3346
|
+
if not isinstance(order_tank, int):
|
|
3347
|
+
raise TypeError("'order_tank' must be an instance of 'int' " +
|
|
3348
|
+
f"but not of '{type(order_tank)}'")
|
|
3349
|
+
if order_tank not in [0, 1]:
|
|
3350
|
+
raise ValueError(f"Invalid value '{order_tank}' for order_wall")
|
|
3351
|
+
|
|
3352
|
+
self.epanet_api.setOptionsTankBulkReactionOrder(order_tank)
|
|
3353
|
+
|
|
3354
|
+
if global_wall_reaction_coefficient is not None:
|
|
3355
|
+
if not isinstance(global_wall_reaction_coefficient, float):
|
|
3356
|
+
raise TypeError("'global_wall_reaction_coefficient' must be an instance of " +
|
|
3357
|
+
f"'float' but not of '{type(global_wall_reaction_coefficient)}'")
|
|
3358
|
+
|
|
3359
|
+
wall_reaction_coeff = self.epanet_api.getLinkWallReactionCoeff()
|
|
3360
|
+
for i in range(len(wall_reaction_coeff)):
|
|
3361
|
+
wall_reaction_coeff[i] = global_wall_reaction_coefficient
|
|
3362
|
+
|
|
3363
|
+
self.epanet_api.setLinkWallReactionCoeff(wall_reaction_coeff)
|
|
3364
|
+
|
|
3365
|
+
if global_bulk_reaction_coefficient is not None:
|
|
3366
|
+
if not isinstance(global_bulk_reaction_coefficient, float):
|
|
3367
|
+
raise TypeError("'global_bulk_reaction_coefficient' must be an instance of " +
|
|
3368
|
+
f"'float' but not of '{type(global_bulk_reaction_coefficient)}'")
|
|
3369
|
+
|
|
3370
|
+
bulk_reaction_coeff = self.epanet_api.getLinkBulkReactionCoeff()
|
|
3371
|
+
for i in range(len(bulk_reaction_coeff)):
|
|
3372
|
+
bulk_reaction_coeff[i] = global_bulk_reaction_coefficient
|
|
3373
|
+
|
|
3374
|
+
self.epanet_api.setLinkBulkReactionCoeff(bulk_reaction_coeff)
|
|
3375
|
+
|
|
3376
|
+
if local_wall_reaction_coefficient is not None:
|
|
3377
|
+
if not isinstance(local_wall_reaction_coefficient, dict):
|
|
3378
|
+
raise TypeError("'local_wall_reaction_coefficient' must be an instance " +
|
|
3379
|
+
"of 'dict[str, float]' but not of " +
|
|
3380
|
+
f"'{type(local_wall_reaction_coefficient)}'")
|
|
3381
|
+
if any(not isinstance(key, str) or not isinstance(value, float)
|
|
3382
|
+
for key, value in local_wall_reaction_coefficient):
|
|
3383
|
+
raise TypeError("'local_wall_reaction_coefficient' must be an instance " +
|
|
3384
|
+
"of 'dict[str, float]'")
|
|
3385
|
+
for link_id, _ in local_wall_reaction_coefficient:
|
|
3386
|
+
if link_id not in self._sensor_config.links:
|
|
3387
|
+
raise ValueError(f"Invalid link ID '{link_id}'")
|
|
3388
|
+
|
|
3389
|
+
for link_id, link_reaction_coeff in local_wall_reaction_coefficient:
|
|
3390
|
+
link_idx = self.epanet_api.getLinkIndex(link_id)
|
|
3391
|
+
self.epanet_api.setLinkWallReactionCoeff(link_idx, link_reaction_coeff)
|
|
3392
|
+
|
|
3393
|
+
if local_bulk_reaction_coefficient is not None:
|
|
3394
|
+
if not isinstance(local_bulk_reaction_coefficient, dict):
|
|
3395
|
+
raise TypeError("'local_bulk_reaction_coefficient' must be an instance " +
|
|
3396
|
+
"of 'dict[str, float]' but not of " +
|
|
3397
|
+
f"'{type(local_bulk_reaction_coefficient)}'")
|
|
3398
|
+
if any(not isinstance(key, str) or not isinstance(value, float)
|
|
3399
|
+
for key, value in local_bulk_reaction_coefficient):
|
|
3400
|
+
raise TypeError("'local_bulk_reaction_coefficient' must be an instance " +
|
|
3401
|
+
"of 'dict[str, float]'")
|
|
3402
|
+
for link_id, _ in local_bulk_reaction_coefficient:
|
|
3403
|
+
if link_id not in self._sensor_config.links:
|
|
3404
|
+
raise ValueError(f"Invalid link ID '{link_id}'")
|
|
3405
|
+
|
|
3406
|
+
for link_id, link_reaction_coeff in local_bulk_reaction_coefficient:
|
|
3407
|
+
link_idx = self.epanet_api.getLinkIndex(link_id)
|
|
3408
|
+
self.epanet_api.setLinkBulkReactionCoeff(link_idx, link_reaction_coeff)
|
|
3409
|
+
|
|
3410
|
+
if local_tank_reaction_coefficient is not None:
|
|
3411
|
+
if not isinstance(local_tank_reaction_coefficient, dict):
|
|
3412
|
+
raise TypeError("'local_tank_reaction_coefficient' must be an instance " +
|
|
3413
|
+
"of 'dict[str, float]' but not of " +
|
|
3414
|
+
f"'{type(local_tank_reaction_coefficient)}'")
|
|
3415
|
+
if any(not isinstance(key, str) or not isinstance(value, float)
|
|
3416
|
+
for key, value in local_tank_reaction_coefficient):
|
|
3417
|
+
raise TypeError("'local_tank_reaction_coefficient' must be an instance " +
|
|
3418
|
+
"of 'dict[str, float]'")
|
|
3419
|
+
for tank_id, _ in local_tank_reaction_coefficient:
|
|
3420
|
+
if tank_id not in self._sensor_config.tanks:
|
|
3421
|
+
raise ValueError(f"Invalid tank ID '{tank_id}'")
|
|
3422
|
+
|
|
3423
|
+
for tank_id, tank_reaction_coeff in local_tank_reaction_coefficient:
|
|
3424
|
+
tank_idx = self.epanet_api.getNodeTankIndex(tank_id)
|
|
3425
|
+
self.epanet_api.setNodeTankBulkReactionCoeff(tank_idx, tank_reaction_coeff)
|
|
3426
|
+
|
|
3427
|
+
if limiting_potential is not None:
|
|
3428
|
+
if not isinstance(limiting_potential, float):
|
|
3429
|
+
raise TypeError("'limiting_potential' must be an instance of 'float' " +
|
|
3430
|
+
f"but not of '{type(limiting_potential)}'")
|
|
3431
|
+
if limiting_potential < 0:
|
|
3432
|
+
raise ValueError("'limiting_potential' can not be negative")
|
|
3433
|
+
|
|
3434
|
+
self.epanet_api.setOptionsLimitingConcentration(limiting_potential)
|
|
3435
|
+
|
|
2702
3436
|
def enable_sourcetracing_analysis(self, trace_node_id: str) -> None:
|
|
2703
3437
|
"""
|
|
2704
3438
|
Set source tracing analysis -- i.e. tracks the percentage of flow from a given node
|
|
@@ -2727,6 +3461,8 @@ class ScenarioSimulator():
|
|
|
2727
3461
|
"""
|
|
2728
3462
|
Adds a new external (bulk or surface) species injection source at a particular node.
|
|
2729
3463
|
|
|
3464
|
+
Only for EPANET-MSX scenarios.
|
|
3465
|
+
|
|
2730
3466
|
Parameters
|
|
2731
3467
|
----------
|
|
2732
3468
|
species_id : `str`
|
|
@@ -2782,3 +3518,81 @@ class ScenarioSimulator():
|
|
|
2782
3518
|
self.epanet_api.addMSXPattern(pattern_id, pattern)
|
|
2783
3519
|
self.epanet_api.setMSXSources(node_id, species_id, source_type_, source_strength,
|
|
2784
3520
|
pattern_id)
|
|
3521
|
+
|
|
3522
|
+
def set_bulk_species_node_initial_concentrations(self,
|
|
3523
|
+
inital_conc: dict[str, list[tuple[str, float]]]
|
|
3524
|
+
) -> None:
|
|
3525
|
+
"""
|
|
3526
|
+
Species the initial bulk species concentration at nodes.
|
|
3527
|
+
|
|
3528
|
+
Only for EPANET-MSX scenarios.
|
|
3529
|
+
|
|
3530
|
+
Parameters
|
|
3531
|
+
----------
|
|
3532
|
+
inital_conc : `dict[str, list[tuple[str, float]]]`
|
|
3533
|
+
Initial concentration of species (key) at nodes -- i.e.
|
|
3534
|
+
value: list of node ID and initial concentration.
|
|
3535
|
+
"""
|
|
3536
|
+
if not isinstance(inital_conc, dict) or \
|
|
3537
|
+
any(not isinstance(species_id, str) or not isinstance(node_initial_conc, list)
|
|
3538
|
+
for species_id, node_initial_conc in inital_conc.items()) or \
|
|
3539
|
+
any(not isinstance(node_initial_conc, tuple)
|
|
3540
|
+
for node_initial_conc in inital_conc.values()) or \
|
|
3541
|
+
any(not isinstance(node_id, str) or not isinstance(conc, float)
|
|
3542
|
+
for node_id, conc in inital_conc.values()):
|
|
3543
|
+
raise TypeError("'inital_conc' must be an instance of " +
|
|
3544
|
+
"'dict[str, list[tuple[str, float]]'")
|
|
3545
|
+
if any(species_id not in self.sensor_config.bulk_species
|
|
3546
|
+
for species_id in inital_conc.keys()):
|
|
3547
|
+
raise ValueError("Unknown bulk species in 'inital_conc'")
|
|
3548
|
+
if any(node_id not in self.sensor_config.nodes for node_id, _ in inital_conc.values()):
|
|
3549
|
+
raise ValueError("Unknown node ID in 'inital_conc'")
|
|
3550
|
+
if any(conc < 0 for _, conc in inital_conc.values()):
|
|
3551
|
+
raise ValueError("Initial node concentration can not be negative")
|
|
3552
|
+
|
|
3553
|
+
for species_id, node_initial_conc in inital_conc.items():
|
|
3554
|
+
species_idx, = self.epanet_api.getMSXSpeciesIndex([species_id])
|
|
3555
|
+
|
|
3556
|
+
for node_id, initial_conc in node_initial_conc:
|
|
3557
|
+
node_idx = self.epanet_api.getNodeIndex(node_id)
|
|
3558
|
+
self.epanet_api.msx.MSXsetinitqual(ToolkitConstants.MSX_NODE, node_idx, species_idx,
|
|
3559
|
+
initial_conc)
|
|
3560
|
+
|
|
3561
|
+
def set_species_link_initial_concentrations(self,
|
|
3562
|
+
inital_conc: dict[str, list[tuple[str, float]]]
|
|
3563
|
+
) -> None:
|
|
3564
|
+
"""
|
|
3565
|
+
Species the initial (bulk or surface) species concentration at links.
|
|
3566
|
+
|
|
3567
|
+
Only for EPANET-MSX scenarios.
|
|
3568
|
+
|
|
3569
|
+
Parameters
|
|
3570
|
+
----------
|
|
3571
|
+
inital_conc : `dict[str, list[tuple[str, float]]]`
|
|
3572
|
+
Initial concentration of species (key) at links -- i.e.
|
|
3573
|
+
value: list of link ID and initial concentration.
|
|
3574
|
+
"""
|
|
3575
|
+
if not isinstance(inital_conc, dict) or \
|
|
3576
|
+
any(not isinstance(species_id, str) or not isinstance(link_initial_conc, list)
|
|
3577
|
+
for species_id, link_initial_conc in inital_conc.items()) or \
|
|
3578
|
+
any(not isinstance(link_initial_conc, tuple)
|
|
3579
|
+
for link_initial_conc in inital_conc.values()) or \
|
|
3580
|
+
any(not isinstance(link_id, str) or not isinstance(conc, float)
|
|
3581
|
+
for link_id, conc in inital_conc.values()):
|
|
3582
|
+
raise TypeError("'inital_conc' must be an instance of " +
|
|
3583
|
+
"'dict[str, list[tuple[str, float]]'")
|
|
3584
|
+
if any(species_id not in self.sensor_config.bulk_species
|
|
3585
|
+
for species_id in inital_conc.keys()):
|
|
3586
|
+
raise ValueError("Unknown bulk species in 'inital_conc'")
|
|
3587
|
+
if any(link_id not in self.sensor_config.links for link_id, _ in inital_conc.values()):
|
|
3588
|
+
raise ValueError("Unknown link ID in 'inital_conc'")
|
|
3589
|
+
if any(conc < 0 for _, conc in inital_conc.values()):
|
|
3590
|
+
raise ValueError("Initial link concentration can not be negative")
|
|
3591
|
+
|
|
3592
|
+
for species_id, link_initial_conc in inital_conc.items():
|
|
3593
|
+
species_idx, = self.epanet_api.getMSXSpeciesIndex([species_id])
|
|
3594
|
+
|
|
3595
|
+
for link_id, initial_conc in link_initial_conc:
|
|
3596
|
+
link_idx = self.epanet_api.getLinkIndex(link_id)
|
|
3597
|
+
self.epanet_api.msx.MSXsetinitqual(ToolkitConstants.MSX_LINK, link_idx, species_idx,
|
|
3598
|
+
initial_conc)
|