epyt-flow 0.8.1__py3-none-any.whl → 0.10.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epyt_flow/VERSION +1 -1
- epyt_flow/__init__.py +1 -0
- epyt_flow/data/benchmarks/batadal.py +1 -1
- epyt_flow/data/benchmarks/battledim.py +4 -3
- epyt_flow/data/benchmarks/gecco_water_quality.py +4 -4
- epyt_flow/data/benchmarks/leakdb.py +7 -7
- epyt_flow/data/benchmarks/water_usage.py +2 -2
- epyt_flow/data/networks.py +1 -1
- epyt_flow/gym/control_gyms.py +2 -2
- epyt_flow/gym/scenario_control_env.py +9 -1
- epyt_flow/metrics.py +28 -28
- epyt_flow/models/sensor_interpolation_detector.py +3 -3
- epyt_flow/rest_api/base_handler.py +4 -4
- epyt_flow/rest_api/scada_data/data_handlers.py +11 -11
- epyt_flow/rest_api/scada_data/export_handlers.py +2 -2
- epyt_flow/rest_api/scada_data/handlers.py +9 -9
- epyt_flow/rest_api/scenario/event_handlers.py +6 -6
- epyt_flow/rest_api/scenario/handlers.py +15 -15
- epyt_flow/rest_api/scenario/simulation_handlers.py +7 -7
- epyt_flow/rest_api/scenario/uncertainty_handlers.py +6 -6
- epyt_flow/serialization.py +8 -2
- epyt_flow/simulation/events/actuator_events.py +1 -1
- epyt_flow/simulation/events/leakages.py +1 -1
- epyt_flow/simulation/events/quality_events.py +16 -5
- epyt_flow/simulation/events/sensor_reading_attack.py +17 -4
- epyt_flow/simulation/events/sensor_reading_event.py +21 -6
- epyt_flow/simulation/events/system_event.py +1 -1
- epyt_flow/simulation/parallel_simulation.py +1 -1
- epyt_flow/simulation/scada/__init__.py +3 -1
- epyt_flow/simulation/scada/advanced_control.py +8 -4
- 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 +133 -130
- epyt_flow/simulation/scada/scada_data_export.py +1 -1
- epyt_flow/simulation/scada/simple_control.py +317 -0
- epyt_flow/simulation/scenario_config.py +124 -24
- epyt_flow/simulation/scenario_simulator.py +514 -49
- epyt_flow/simulation/scenario_visualizer.py +9 -9
- epyt_flow/simulation/sensor_config.py +38 -28
- epyt_flow/topology.py +2 -2
- epyt_flow/uncertainty/model_uncertainty.py +624 -147
- epyt_flow/uncertainty/sensor_noise.py +94 -19
- epyt_flow/uncertainty/uncertainties.py +4 -4
- epyt_flow/uncertainty/utils.py +7 -7
- epyt_flow/utils.py +9 -8
- {epyt_flow-0.8.1.dist-info → epyt_flow-0.10.0.dist-info}/LICENSE +1 -1
- {epyt_flow-0.8.1.dist-info → epyt_flow-0.10.0.dist-info}/METADATA +7 -6
- {epyt_flow-0.8.1.dist-info → epyt_flow-0.10.0.dist-info}/RECORD +50 -47
- {epyt_flow-0.8.1.dist-info → epyt_flow-0.10.0.dist-info}/WHEEL +1 -1
- {epyt_flow-0.8.1.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.
|
|
384
|
+
"""
|
|
385
|
+
self._adapt_to_network_changes()
|
|
386
|
+
|
|
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.
|
|
334
398
|
"""
|
|
335
399
|
self._adapt_to_network_changes()
|
|
336
400
|
|
|
337
|
-
return deepcopy(self.
|
|
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()
|
|
@@ -477,7 +667,8 @@ class ScenarioSimulator():
|
|
|
477
667
|
self.close()
|
|
478
668
|
|
|
479
669
|
def save_to_epanet_file(self, inp_file_path: str, msx_file_path: str = None,
|
|
480
|
-
export_sensor_config: bool = True
|
|
670
|
+
export_sensor_config: bool = True, undo_system_events: bool = True
|
|
671
|
+
) -> None:
|
|
481
672
|
"""
|
|
482
673
|
Exports this scenario to EPANET files -- i.e. an .inp file
|
|
483
674
|
and (optionally) a .msx file if EPANET-MSX was loaded.
|
|
@@ -540,6 +731,10 @@ class ScenarioSimulator():
|
|
|
540
731
|
if write_end_section is True:
|
|
541
732
|
f_in.write("\n[END]")
|
|
542
733
|
|
|
734
|
+
if undo_system_events is True:
|
|
735
|
+
for event in self._system_events:
|
|
736
|
+
event.cleanup()
|
|
737
|
+
|
|
543
738
|
if inp_file_path is not None:
|
|
544
739
|
self.epanet_api.saveInputFile(inp_file_path)
|
|
545
740
|
self.__f_inp_in = inp_file_path
|
|
@@ -641,6 +836,10 @@ class ScenarioSimulator():
|
|
|
641
836
|
|
|
642
837
|
__override_report_section(msx_file_path, report_desc)
|
|
643
838
|
|
|
839
|
+
if undo_system_events is True:
|
|
840
|
+
for event in self._system_events:
|
|
841
|
+
event.init(self.epanet_api)
|
|
842
|
+
|
|
644
843
|
def get_flow_units(self) -> int:
|
|
645
844
|
"""
|
|
646
845
|
Gets the flow units.
|
|
@@ -791,7 +990,11 @@ class ScenarioSimulator():
|
|
|
791
990
|
return ScenarioConfig(f_inp_in=self.__f_inp_in, f_msx_in=self.__f_msx_in,
|
|
792
991
|
general_params=general_params, sensor_config=self.sensor_config,
|
|
793
992
|
memory_consumption_estimate=self.estimate_memory_consumption(),
|
|
794
|
-
|
|
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,
|
|
795
998
|
model_uncertainty=self.model_uncertainty,
|
|
796
999
|
system_events=self.system_events,
|
|
797
1000
|
sensor_reading_events=self.sensor_reading_events)
|
|
@@ -938,8 +1141,72 @@ class ScenarioSimulator():
|
|
|
938
1141
|
for t in range(pattern_length): # Set shuffled/randomized pattern
|
|
939
1142
|
self.epanet_api.setPatternValue(pattern_id, t + 1, pattern[t])
|
|
940
1143
|
|
|
1144
|
+
def get_pattern(self, pattern_id: str) -> np.ndarray:
|
|
1145
|
+
"""
|
|
1146
|
+
Returns the EPANET pattern (i.e. all multiplier factors over time) given its ID.
|
|
1147
|
+
|
|
1148
|
+
Parameters
|
|
1149
|
+
----------
|
|
1150
|
+
pattern_id : `str`
|
|
1151
|
+
ID of the pattern.
|
|
1152
|
+
|
|
1153
|
+
Returns
|
|
1154
|
+
-------
|
|
1155
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
1156
|
+
The pattern -- i.e. multiplier factors over time.
|
|
1157
|
+
"""
|
|
1158
|
+
pattern_idx = self.epanet_api.getPatternIndex(pattern_id)
|
|
1159
|
+
pattern_length = self.epanet_api.getPatternLengths(pattern_idx)
|
|
1160
|
+
return np.array([self.epanet_api.getPatternValue(pattern_idx, t+1)
|
|
1161
|
+
for t in range(pattern_length)])
|
|
1162
|
+
|
|
1163
|
+
def add_pattern(self, pattern_id: str, pattern: np.ndarray) -> None:
|
|
1164
|
+
"""
|
|
1165
|
+
Adds a pattern to the EPANET scenario.
|
|
1166
|
+
|
|
1167
|
+
Parameters
|
|
1168
|
+
----------
|
|
1169
|
+
pattern_id : `str`
|
|
1170
|
+
ID of the pattern.
|
|
1171
|
+
pattern : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
1172
|
+
Pattern of multipliers over time.
|
|
1173
|
+
"""
|
|
1174
|
+
self._adapt_to_network_changes()
|
|
1175
|
+
|
|
1176
|
+
if not isinstance(pattern_id, str):
|
|
1177
|
+
raise TypeError("'pattern_id' must be an instance of 'str' " +
|
|
1178
|
+
f"but not of '{type(pattern_id)}'")
|
|
1179
|
+
if not isinstance(pattern, np.ndarray):
|
|
1180
|
+
raise TypeError("'pattern' must be an instance of 'numpy.ndarray' " +
|
|
1181
|
+
f"but not of '{type(pattern)}'")
|
|
1182
|
+
if len(pattern.shape) > 1:
|
|
1183
|
+
raise ValueError(f"Inconsistent pattern shape '{pattern.shape}' " +
|
|
1184
|
+
"detected. Expected a one dimensional array!")
|
|
1185
|
+
|
|
1186
|
+
self.epanet_api.addPattern(pattern_id, pattern)
|
|
1187
|
+
|
|
1188
|
+
def get_node_demand_pattern(self, node_id: str) -> np.ndarray:
|
|
1189
|
+
"""
|
|
1190
|
+
Returns the values of the demand pattern of a given node --
|
|
1191
|
+
i.e. multiplier factors that are applied to the base demand.
|
|
1192
|
+
|
|
1193
|
+
Parameters
|
|
1194
|
+
----------
|
|
1195
|
+
node_id : `str`
|
|
1196
|
+
ID of the node.
|
|
1197
|
+
|
|
1198
|
+
Returns
|
|
1199
|
+
-------
|
|
1200
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
1201
|
+
The demand pattern -- i.e. multiplier factors over time.
|
|
1202
|
+
"""
|
|
1203
|
+
node_idx = self.epanet_api.getNodeIndex(node_id)
|
|
1204
|
+
demand_category = self.epanet_api.getNodeDemandCategoriesNumber()[node_idx]
|
|
1205
|
+
demand_pattern_id = self.epanet_api.getNodeDemandPatternNameID()[demand_category][node_idx - 1]
|
|
1206
|
+
return self.get_pattern(demand_pattern_id)
|
|
1207
|
+
|
|
941
1208
|
def set_node_demand_pattern(self, node_id: str, base_demand: float, demand_pattern_id: str,
|
|
942
|
-
demand_pattern: np.ndarray) -> None:
|
|
1209
|
+
demand_pattern: np.ndarray = None) -> None:
|
|
943
1210
|
"""
|
|
944
1211
|
Sets the demand pattern (incl. base demand) at a given node.
|
|
945
1212
|
|
|
@@ -950,9 +1217,12 @@ class ScenarioSimulator():
|
|
|
950
1217
|
base_demand : `float`
|
|
951
1218
|
Base demand.
|
|
952
1219
|
demand_pattern_id : `str`
|
|
953
|
-
ID of the new demand pattern.
|
|
954
|
-
demand_pattern : `numpy.ndarray
|
|
1220
|
+
ID of the (new) demand pattern. Existing demand pattern will be overriden if it already exisits.
|
|
1221
|
+
demand_pattern : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
|
|
955
1222
|
Demand pattern over time. Final demand over time = base_demand * demand_pattern
|
|
1223
|
+
If None, the pattern demand_pattern_id is assumed to already exist.
|
|
1224
|
+
|
|
1225
|
+
The default is None.
|
|
956
1226
|
"""
|
|
957
1227
|
self._adapt_to_network_changes()
|
|
958
1228
|
|
|
@@ -964,35 +1234,170 @@ class ScenarioSimulator():
|
|
|
964
1234
|
if not isinstance(demand_pattern_id, str):
|
|
965
1235
|
raise TypeError("'demand_pattern_id' must be an instance of 'str' " +
|
|
966
1236
|
f"but not of '{type(demand_pattern_id)}'")
|
|
967
|
-
if not
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
1237
|
+
if demand_pattern is not None:
|
|
1238
|
+
if not isinstance(demand_pattern, np.ndarray):
|
|
1239
|
+
raise TypeError("'demand_pattern' must be an instance of 'numpy.ndarray' " +
|
|
1240
|
+
f"but not of '{type(demand_pattern)}'")
|
|
1241
|
+
if len(demand_pattern.shape) > 1:
|
|
1242
|
+
raise ValueError(f"Inconsistent demand pattern shape '{demand_pattern.shape}' " +
|
|
1243
|
+
"detected. Expected a one dimensional array!")
|
|
973
1244
|
|
|
974
1245
|
node_idx = self.epanet_api.getNodeIndex(node_id)
|
|
975
|
-
|
|
1246
|
+
|
|
1247
|
+
if demand_pattern_id not in self.epanet_api.getPatternNameID():
|
|
1248
|
+
if demand_pattern is None:
|
|
1249
|
+
raise ValueError("'demand_pattern' can not be None if " +
|
|
1250
|
+
"'demand_pattern_id' does not already exist.")
|
|
1251
|
+
self.epanet_api.addPattern(demand_pattern_id, demand_pattern)
|
|
1252
|
+
else:
|
|
1253
|
+
if demand_pattern is not None:
|
|
1254
|
+
pattern_idx = self.epanet_api.getPatternIndex(demand_pattern_id)
|
|
1255
|
+
self.epanet_api.setPattern(pattern_idx, demand_pattern)
|
|
1256
|
+
|
|
976
1257
|
self.epanet_api.setNodeJunctionData(node_idx, self.epanet_api.getNodeElevations(node_idx),
|
|
977
1258
|
base_demand, demand_pattern_id)
|
|
978
1259
|
|
|
979
|
-
def
|
|
1260
|
+
def add_advanced_control(self, control) -> None:
|
|
980
1261
|
"""
|
|
981
|
-
Adds
|
|
1262
|
+
Adds an advanced control module to the scenario simulation.
|
|
982
1263
|
|
|
983
1264
|
Parameters
|
|
984
1265
|
----------
|
|
985
1266
|
control : :class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`
|
|
986
|
-
|
|
1267
|
+
Advanced control module.
|
|
987
1268
|
"""
|
|
988
1269
|
self._adapt_to_network_changes()
|
|
989
1270
|
|
|
1271
|
+
from .scada.advanced_control import AdvancedControlModule
|
|
990
1272
|
if not isinstance(control, AdvancedControlModule):
|
|
991
1273
|
raise TypeError("'control' must be an instance of " +
|
|
992
1274
|
"'epyt_flow.simulation.scada.AdvancedControlModule' not of " +
|
|
993
1275
|
f"'{type(control)}'")
|
|
994
1276
|
|
|
995
|
-
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)
|
|
996
1401
|
|
|
997
1402
|
def add_leakage(self, leakage_event: Leakage) -> None:
|
|
998
1403
|
"""
|
|
@@ -1529,12 +1934,17 @@ class ScenarioSimulator():
|
|
|
1529
1934
|
for event in self._system_events:
|
|
1530
1935
|
event.reset()
|
|
1531
1936
|
|
|
1532
|
-
if self.
|
|
1533
|
-
for c in self.
|
|
1937
|
+
if self._advanced_controls is not None:
|
|
1938
|
+
for c in self._advanced_controls:
|
|
1534
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)
|
|
1535
1943
|
|
|
1536
1944
|
def run_advanced_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
|
|
1537
|
-
frozen_sensor_config: bool = False
|
|
1945
|
+
frozen_sensor_config: bool = False,
|
|
1946
|
+
use_quality_time_step_as_reporting_time_step: bool = False
|
|
1947
|
+
) -> ScadaData:
|
|
1538
1948
|
"""
|
|
1539
1949
|
Runs an advanced quality analysis using EPANET-MSX.
|
|
1540
1950
|
|
|
@@ -1551,6 +1961,13 @@ class ScenarioSimulator():
|
|
|
1551
1961
|
If True, the sensor config can not be changed and only the required sensor nodes/links
|
|
1552
1962
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
1553
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
|
+
|
|
1554
1971
|
The default is False.
|
|
1555
1972
|
|
|
1556
1973
|
Returns
|
|
@@ -1570,7 +1987,9 @@ class ScenarioSimulator():
|
|
|
1570
1987
|
for scada_data in gen(hyd_file_in=hyd_file_in,
|
|
1571
1988
|
verbose=verbose,
|
|
1572
1989
|
return_as_dict=True,
|
|
1573
|
-
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):
|
|
1574
1993
|
if result is None:
|
|
1575
1994
|
result = {}
|
|
1576
1995
|
for data_type, data in scada_data.items():
|
|
@@ -1595,7 +2014,8 @@ class ScenarioSimulator():
|
|
|
1595
2014
|
def run_advanced_quality_simulation_as_generator(self, hyd_file_in: str, verbose: bool = False,
|
|
1596
2015
|
support_abort: bool = False,
|
|
1597
2016
|
return_as_dict: bool = False,
|
|
1598
|
-
frozen_sensor_config: bool = False
|
|
2017
|
+
frozen_sensor_config: bool = False,
|
|
2018
|
+
use_quality_time_step_as_reporting_time_step: bool = False,
|
|
1599
2019
|
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
1600
2020
|
"""
|
|
1601
2021
|
Runs an advanced quality analysis using EPANET-MSX.
|
|
@@ -1617,6 +2037,13 @@ class ScenarioSimulator():
|
|
|
1617
2037
|
If True, the sensor config can not be changed and only the required sensor nodes/links
|
|
1618
2038
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
1619
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
|
+
|
|
1620
2047
|
The default is False.
|
|
1621
2048
|
|
|
1622
2049
|
Returns
|
|
@@ -1642,6 +2069,11 @@ class ScenarioSimulator():
|
|
|
1642
2069
|
reporting_time_step = self.epanet_api.getTimeReportingStep()
|
|
1643
2070
|
hyd_time_step = self.epanet_api.getTimeHydraulicStep()
|
|
1644
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
|
+
|
|
1645
2077
|
self.epanet_api.initializeMSXQualityAnalysis(ToolkitConstants.EN_NOSAVE)
|
|
1646
2078
|
|
|
1647
2079
|
self.__running_simulation = True
|
|
@@ -1654,7 +2086,7 @@ class ScenarioSimulator():
|
|
|
1654
2086
|
print("Running EPANET-MSX ...")
|
|
1655
2087
|
n_iterations = math.ceil(self.epanet_api.getTimeSimulationDuration() /
|
|
1656
2088
|
hyd_time_step)
|
|
1657
|
-
progress_bar = iter(tqdm(range(n_iterations + 1), desc="Time steps"))
|
|
2089
|
+
progress_bar = iter(tqdm(range(n_iterations + 1), ascii=True, desc="Time steps"))
|
|
1658
2090
|
|
|
1659
2091
|
def __get_concentrations(init_qual=False):
|
|
1660
2092
|
if init_qual is True:
|
|
@@ -1793,7 +2225,9 @@ class ScenarioSimulator():
|
|
|
1793
2225
|
self.__running_simulation = False
|
|
1794
2226
|
|
|
1795
2227
|
def run_basic_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
|
|
1796
|
-
frozen_sensor_config: bool = False
|
|
2228
|
+
frozen_sensor_config: bool = False,
|
|
2229
|
+
use_quality_time_step_as_reporting_time_step: bool = False
|
|
2230
|
+
) -> ScadaData:
|
|
1797
2231
|
"""
|
|
1798
2232
|
Runs a basic quality analysis using EPANET.
|
|
1799
2233
|
|
|
@@ -1810,6 +2244,13 @@ class ScenarioSimulator():
|
|
|
1810
2244
|
If True, the sensor config can not be changed and only the required sensor nodes/links
|
|
1811
2245
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
1812
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
|
+
|
|
1813
2254
|
The default is False.
|
|
1814
2255
|
|
|
1815
2256
|
Returns
|
|
@@ -1827,7 +2268,9 @@ class ScenarioSimulator():
|
|
|
1827
2268
|
for scada_data in gen(hyd_file_in=hyd_file_in,
|
|
1828
2269
|
verbose=verbose,
|
|
1829
2270
|
return_as_dict=True,
|
|
1830
|
-
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):
|
|
1831
2274
|
if result is None:
|
|
1832
2275
|
result = {}
|
|
1833
2276
|
for data_type, data in scada_data.items():
|
|
@@ -1850,6 +2293,7 @@ class ScenarioSimulator():
|
|
|
1850
2293
|
support_abort: bool = False,
|
|
1851
2294
|
return_as_dict: bool = False,
|
|
1852
2295
|
frozen_sensor_config: bool = False,
|
|
2296
|
+
use_quality_time_step_as_reporting_time_step: bool = False
|
|
1853
2297
|
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
1854
2298
|
"""
|
|
1855
2299
|
Runs a basic quality analysis using EPANET.
|
|
@@ -1873,6 +2317,13 @@ class ScenarioSimulator():
|
|
|
1873
2317
|
If True, the sensor config can not be changed and only the required sensor nodes/links
|
|
1874
2318
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
1875
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
|
+
|
|
1876
2327
|
The default is False.
|
|
1877
2328
|
|
|
1878
2329
|
Returns
|
|
@@ -1887,6 +2338,11 @@ class ScenarioSimulator():
|
|
|
1887
2338
|
reporting_time_start = self.epanet_api.getTimeReportingStart()
|
|
1888
2339
|
reporting_time_step = self.epanet_api.getTimeReportingStep()
|
|
1889
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
|
+
|
|
1890
2346
|
self.epanet_api.useHydraulicFile(hyd_file_in)
|
|
1891
2347
|
|
|
1892
2348
|
self.epanet_api.openQualityAnalysis()
|
|
@@ -1896,7 +2352,7 @@ class ScenarioSimulator():
|
|
|
1896
2352
|
print("Running basic quality analysis using EPANET ...")
|
|
1897
2353
|
n_iterations = math.ceil(self.epanet_api.getTimeSimulationDuration() /
|
|
1898
2354
|
requested_time_step)
|
|
1899
|
-
progress_bar = iter(tqdm(range(n_iterations + 1), desc="Time steps"))
|
|
2355
|
+
progress_bar = iter(tqdm(range(n_iterations + 1), ascii=True, desc="Time steps"))
|
|
1900
2356
|
|
|
1901
2357
|
# Run simulation step by step
|
|
1902
2358
|
total_time = 0
|
|
@@ -1945,7 +2401,7 @@ class ScenarioSimulator():
|
|
|
1945
2401
|
yield data
|
|
1946
2402
|
|
|
1947
2403
|
# Next
|
|
1948
|
-
tstep = self.epanet_api.
|
|
2404
|
+
tstep = self.epanet_api.stepQualityAnalysisTimeLeft()
|
|
1949
2405
|
|
|
1950
2406
|
self.epanet_api.closeHydraulicAnalysis()
|
|
1951
2407
|
|
|
@@ -2081,7 +2537,7 @@ class ScenarioSimulator():
|
|
|
2081
2537
|
print("Running EPANET ...")
|
|
2082
2538
|
n_iterations = math.ceil(self.epanet_api.getTimeSimulationDuration() /
|
|
2083
2539
|
requested_time_step)
|
|
2084
|
-
progress_bar = iter(tqdm(range(n_iterations + 1), desc="Time steps"))
|
|
2540
|
+
progress_bar = iter(tqdm(range(n_iterations + 1), ascii=True, desc="Time steps"))
|
|
2085
2541
|
|
|
2086
2542
|
try:
|
|
2087
2543
|
# Run simulation step by step
|
|
@@ -2167,7 +2623,9 @@ class ScenarioSimulator():
|
|
|
2167
2623
|
yield data
|
|
2168
2624
|
|
|
2169
2625
|
# Apply control modules
|
|
2170
|
-
for control in self.
|
|
2626
|
+
for control in self._advanced_controls:
|
|
2627
|
+
control.step(scada_data)
|
|
2628
|
+
for control in self._custom_controls:
|
|
2171
2629
|
control.step(scada_data)
|
|
2172
2630
|
|
|
2173
2631
|
# Next
|
|
@@ -2538,7 +2996,7 @@ class ScenarioSimulator():
|
|
|
2538
2996
|
self.set_general_parameters(quality_model={"type": "CHEM", "chemical_name": chemical_name,
|
|
2539
2997
|
"units": chemical_units})
|
|
2540
2998
|
|
|
2541
|
-
def add_quality_source(self, node_id: str, pattern: np.ndarray
|
|
2999
|
+
def add_quality_source(self, node_id: str, source_type: int, pattern: np.ndarray = None,
|
|
2542
3000
|
pattern_id: str = None, source_strength: int = 1.) -> None:
|
|
2543
3001
|
"""
|
|
2544
3002
|
Adds a new external water quality source at a particular node.
|
|
@@ -2547,8 +3005,6 @@ class ScenarioSimulator():
|
|
|
2547
3005
|
----------
|
|
2548
3006
|
node_id : `str`
|
|
2549
3007
|
ID of the node at which this external water quality source is placed.
|
|
2550
|
-
pattern : `numpy.ndarray`
|
|
2551
|
-
1d source pattern.
|
|
2552
3008
|
source_type : `int`,
|
|
2553
3009
|
Types of the external water quality source -- must be of the following
|
|
2554
3010
|
EPANET toolkit constants:
|
|
@@ -2564,6 +3020,12 @@ class ScenarioSimulator():
|
|
|
2564
3020
|
- EN_MASS Injects a given mass/minute into a node
|
|
2565
3021
|
- EN_SETPOINT Sets the concentration leaving a node to a given value
|
|
2566
3022
|
- EN_FLOWPACED Adds a given value to the concentration leaving a node
|
|
3023
|
+
pattern : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
|
|
3024
|
+
1d source pattern multipiers over time -- i.e. quality-source = source_strength * pattern.
|
|
3025
|
+
|
|
3026
|
+
If None, the pattern pattern_id is assume to already exist.
|
|
3027
|
+
|
|
3028
|
+
The default is None.
|
|
2567
3029
|
pattern_id : `str`, optional
|
|
2568
3030
|
ID of the source pattern.
|
|
2569
3031
|
|
|
@@ -2586,20 +3048,23 @@ class ScenarioSimulator():
|
|
|
2586
3048
|
"call 'enable_chemical_analysis()' before calling this function.")
|
|
2587
3049
|
if node_id not in self._sensor_config.nodes:
|
|
2588
3050
|
raise ValueError(f"Unknown node '{node_id}'")
|
|
2589
|
-
if not isinstance(pattern, np.ndarray):
|
|
2590
|
-
raise TypeError("'pattern' must be an instance of 'numpy.ndarray' " +
|
|
2591
|
-
f"but not of '{type(pattern)}'")
|
|
2592
3051
|
if not isinstance(source_type, int) or not 0 <= source_type <= 3:
|
|
2593
3052
|
raise ValueError("Invalid type of water quality source")
|
|
2594
|
-
|
|
3053
|
+
if pattern is not None:
|
|
3054
|
+
if not isinstance(pattern, np.ndarray):
|
|
3055
|
+
raise TypeError("'pattern' must be an instance of 'numpy.ndarray' " +
|
|
3056
|
+
f"but not of '{type(pattern)}'")
|
|
3057
|
+
if pattern is None and pattern_id is None:
|
|
3058
|
+
raise ValueError("'pattern_id' and 'pattern' can not be None at the same time")
|
|
2595
3059
|
if pattern_id is None:
|
|
2596
3060
|
pattern_id = f"quality_source_pattern_node={node_id}"
|
|
2597
|
-
if pattern_id in self.epanet_api.getPatternNameID():
|
|
2598
|
-
raise ValueError("Invalid 'pattern_id' -- " +
|
|
2599
|
-
f"there already exists a pattern with ID '{pattern_id}'")
|
|
2600
3061
|
|
|
2601
3062
|
node_idx = self.epanet_api.getNodeIndex(node_id)
|
|
2602
|
-
|
|
3063
|
+
|
|
3064
|
+
if pattern is None:
|
|
3065
|
+
pattern_idx = self.epanet_api.getPatternIndex(pattern_id)
|
|
3066
|
+
else:
|
|
3067
|
+
pattern_idx = self.epanet_api.addPattern(pattern_id, pattern)
|
|
2603
3068
|
|
|
2604
3069
|
self.epanet_api.api.ENsetnodevalue(node_idx, ToolkitConstants.EN_SOURCETYPE, source_type)
|
|
2605
3070
|
self.epanet_api.setNodeSourceQuality(node_idx, source_strength)
|
|
@@ -2640,7 +3105,7 @@ class ScenarioSimulator():
|
|
|
2640
3105
|
node_id : `str`
|
|
2641
3106
|
ID of the node at which this external (bulk or surface) species injection source
|
|
2642
3107
|
is placed.
|
|
2643
|
-
pattern : `numpy.ndarray
|
|
3108
|
+
pattern : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
2644
3109
|
1d source pattern.
|
|
2645
3110
|
source_type : `int`,
|
|
2646
3111
|
Type of the external (bulk or surface) species injection source -- must be one of
|