epyt-flow 0.1.1__py3-none-any.whl → 0.3.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/EPANET/compile_linux.sh +4 -0
- epyt_flow/EPANET/compile_macos.sh +4 -0
- epyt_flow/VERSION +1 -1
- epyt_flow/__init__.py +29 -18
- epyt_flow/data/benchmarks/leakdb.py +7 -12
- epyt_flow/data/networks.py +404 -40
- epyt_flow/rest_api/base_handler.py +14 -0
- epyt_flow/rest_api/scada_data/__init__.py +0 -0
- epyt_flow/rest_api/{scada_data_handler.py → scada_data/data_handlers.py} +3 -162
- epyt_flow/rest_api/scada_data/export_handlers.py +140 -0
- epyt_flow/rest_api/scada_data/handlers.py +209 -0
- epyt_flow/rest_api/scenario/__init__.py +0 -0
- epyt_flow/rest_api/scenario/event_handlers.py +118 -0
- epyt_flow/rest_api/{scenario_handler.py → scenario/handlers.py} +86 -67
- epyt_flow/rest_api/scenario/simulation_handlers.py +174 -0
- epyt_flow/rest_api/scenario/uncertainty_handlers.py +118 -0
- epyt_flow/rest_api/server.py +61 -24
- epyt_flow/simulation/events/leakages.py +27 -17
- epyt_flow/simulation/scada/scada_data.py +545 -14
- epyt_flow/simulation/scada/scada_data_export.py +39 -12
- epyt_flow/simulation/scenario_config.py +14 -20
- epyt_flow/simulation/scenario_simulator.py +358 -114
- epyt_flow/simulation/sensor_config.py +693 -37
- epyt_flow/topology.py +149 -8
- epyt_flow/utils.py +75 -18
- {epyt_flow-0.1.1.dist-info → epyt_flow-0.3.0.dist-info}/METADATA +33 -5
- {epyt_flow-0.1.1.dist-info → epyt_flow-0.3.0.dist-info}/RECORD +30 -22
- epyt_flow/EPANET/compile.sh +0 -4
- {epyt_flow-0.1.1.dist-info → epyt_flow-0.3.0.dist-info}/LICENSE +0 -0
- {epyt_flow-0.1.1.dist-info → epyt_flow-0.3.0.dist-info}/WHEEL +0 -0
- {epyt_flow-0.1.1.dist-info → epyt_flow-0.3.0.dist-info}/top_level.txt +0 -0
|
@@ -17,15 +17,17 @@ from epyt.epanet import ToolkitConstants
|
|
|
17
17
|
from tqdm import tqdm
|
|
18
18
|
|
|
19
19
|
from .scenario_config import ScenarioConfig
|
|
20
|
-
from .sensor_config import SensorConfig,
|
|
21
|
-
|
|
20
|
+
from .sensor_config import SensorConfig, areaunit_to_id, massunit_to_id, qualityunit_to_id, \
|
|
21
|
+
qualityunit_to_str, MASS_UNIT_MG, \
|
|
22
|
+
SENSOR_TYPE_LINK_FLOW, SENSOR_TYPE_LINK_QUALITY, SENSOR_TYPE_NODE_DEMAND, \
|
|
23
|
+
SENSOR_TYPE_NODE_PRESSURE, SENSOR_TYPE_NODE_QUALITY, \
|
|
22
24
|
SENSOR_TYPE_PUMP_STATE, SENSOR_TYPE_TANK_VOLUME, SENSOR_TYPE_VALVE_STATE, \
|
|
23
25
|
SENSOR_TYPE_NODE_BULK_SPECIES, SENSOR_TYPE_LINK_BULK_SPECIES, SENSOR_TYPE_SURFACE_SPECIES
|
|
24
26
|
from ..uncertainty import ModelUncertainty, SensorNoise
|
|
25
27
|
from .events import SystemEvent, Leakage, ActuatorEvent, SensorFault, SensorReadingAttack, \
|
|
26
28
|
SensorReadingEvent
|
|
27
29
|
from .scada import ScadaData, AdvancedControlModule
|
|
28
|
-
from ..topology import NetworkTopology
|
|
30
|
+
from ..topology import NetworkTopology, UNITS_SIMETRIC, UNITS_USCUSTOM
|
|
29
31
|
from ..utils import get_temp_folder
|
|
30
32
|
|
|
31
33
|
|
|
@@ -48,6 +50,10 @@ class ScenarioSimulator():
|
|
|
48
50
|
|
|
49
51
|
If this is None, then 'f_inp_in' must be set with a valid path to the .inp file
|
|
50
52
|
that is to be simulated.
|
|
53
|
+
epanet_verbose : `bool`, optional
|
|
54
|
+
If True, EPyT is verbose and might print messages from time to time.
|
|
55
|
+
|
|
56
|
+
The default is False.
|
|
51
57
|
|
|
52
58
|
Attributes
|
|
53
59
|
----------
|
|
@@ -56,7 +62,7 @@ class ScenarioSimulator():
|
|
|
56
62
|
"""
|
|
57
63
|
|
|
58
64
|
def __init__(self, f_inp_in: str = None, f_msx_in: str = None,
|
|
59
|
-
scenario_config: ScenarioConfig = None):
|
|
65
|
+
scenario_config: ScenarioConfig = None, epanet_verbose: bool = False):
|
|
60
66
|
if f_msx_in is not None and f_inp_in is None:
|
|
61
67
|
raise ValueError("'f_inp_in' must be set if 'f_msx_in' is set.")
|
|
62
68
|
if f_inp_in is None and scenario_config is None:
|
|
@@ -74,6 +80,9 @@ class ScenarioSimulator():
|
|
|
74
80
|
raise TypeError("'scenario_config' must be an instance of " +
|
|
75
81
|
"'epyt_flow.simulation.ScenarioConfig' but not of " +
|
|
76
82
|
f"'{type(scenario_config)}'")
|
|
83
|
+
if not isinstance(epanet_verbose, bool):
|
|
84
|
+
raise TypeError("'epanet_verbose' must be an instance of 'bool' " +
|
|
85
|
+
f"but not of '{type(epanet_verbose)}'")
|
|
77
86
|
|
|
78
87
|
self.__f_inp_in = f_inp_in if scenario_config is None else scenario_config.f_inp_in
|
|
79
88
|
self.__f_msx_in = f_msx_in if scenario_config is None else scenario_config.f_msx_in
|
|
@@ -86,37 +95,28 @@ class ScenarioSimulator():
|
|
|
86
95
|
|
|
87
96
|
custom_epanet_lib = None
|
|
88
97
|
custom_epanetmsx_lib = None
|
|
89
|
-
if sys.platform.startswith("linux"):
|
|
98
|
+
if sys.platform.startswith("linux") or sys.platform.startswith("darwin") :
|
|
90
99
|
path_to_custom_libs = os.path.join(pathlib.Path(__file__).parent.resolve(),
|
|
91
100
|
"..", "customlibs")
|
|
92
101
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
102
|
+
libepanet_name = "libepanet2_2.so" if sys.platform.startswith("linux") \
|
|
103
|
+
else "libepanet2_2.dylib"
|
|
104
|
+
libepanetmsx_name = "libepanetmsx2_2_0.so" if sys.platform.startswith("linux") \
|
|
105
|
+
else "libepanetmsx2_2_0.dylib"
|
|
106
|
+
|
|
107
|
+
if os.path.isfile(os.path.join(path_to_custom_libs, libepanet_name)):
|
|
108
|
+
custom_epanet_lib = os.path.join(path_to_custom_libs, libepanet_name)
|
|
109
|
+
if os.path.isfile(os.path.join(path_to_custom_libs, libepanetmsx_name)):
|
|
110
|
+
custom_epanetmsx_lib = os.path.join(path_to_custom_libs, libepanetmsx_name)
|
|
97
111
|
|
|
98
112
|
self.epanet_api = epanet(self.__f_inp_in, ph=self.__f_msx_in is None,
|
|
99
|
-
customlib=custom_epanet_lib
|
|
113
|
+
customlib=custom_epanet_lib, loadfile=True,
|
|
114
|
+
display_msg=epanet_verbose)
|
|
100
115
|
|
|
101
|
-
bulk_species = []
|
|
102
|
-
surface_species = []
|
|
103
116
|
if self.__f_msx_in is not None:
|
|
104
117
|
self.epanet_api.loadMSXFile(self.__f_msx_in, customMSXlib=custom_epanetmsx_lib)
|
|
105
118
|
|
|
106
|
-
|
|
107
|
-
self.epanet_api.getMSXSpeciesType()):
|
|
108
|
-
if species_type == "BULK":
|
|
109
|
-
bulk_species.append(species_id)
|
|
110
|
-
elif species_type == "WALL":
|
|
111
|
-
surface_species.append(species_id)
|
|
112
|
-
|
|
113
|
-
self.__sensor_config = SensorConfig(nodes=self.epanet_api.getNodeNameID(),
|
|
114
|
-
links=self.epanet_api.getLinkNameID(),
|
|
115
|
-
valves=self.epanet_api.getLinkValveNameID(),
|
|
116
|
-
pumps=self.epanet_api.getLinkPumpNameID(),
|
|
117
|
-
tanks=self.epanet_api.getNodeTankNameID(),
|
|
118
|
-
bulk_species=bulk_species,
|
|
119
|
-
surface_species=surface_species)
|
|
119
|
+
self.__sensor_config = self.__get_empty_sensor_config()
|
|
120
120
|
if scenario_config is not None:
|
|
121
121
|
if scenario_config.general_params is not None:
|
|
122
122
|
self.set_general_parameters(**scenario_config.general_params)
|
|
@@ -132,6 +132,51 @@ class ScenarioSimulator():
|
|
|
132
132
|
for event in scenario_config.sensor_reading_events:
|
|
133
133
|
self.add_sensor_reading_event(event)
|
|
134
134
|
|
|
135
|
+
def __get_empty_sensor_config(self, node_id_to_idx: dict = None, link_id_to_idx: dict = None,
|
|
136
|
+
valve_id_to_idx: dict = None, pump_id_to_idx: dict = None,
|
|
137
|
+
tank_id_to_idx: dict = None, bulkspecies_id_to_idx: dict = None,
|
|
138
|
+
surfacespecies_id_to_idx: dict = None) -> SensorConfig:
|
|
139
|
+
flow_unit = self.epanet_api.api.ENgetflowunits()
|
|
140
|
+
quality_unit = qualityunit_to_id(self.epanet_api.getQualityInfo().QualityChemUnits)
|
|
141
|
+
bulk_species = []
|
|
142
|
+
surface_species = []
|
|
143
|
+
bulk_species_mass_unit = []
|
|
144
|
+
surface_species_mass_unit = []
|
|
145
|
+
surface_species_area_unit = None
|
|
146
|
+
|
|
147
|
+
if self.__f_msx_in is not None:
|
|
148
|
+
surface_species_area_unit = areaunit_to_id(self.epanet_api.getMSXAreaUnits())
|
|
149
|
+
|
|
150
|
+
for species_id, species_type, mass_unit in zip(self.epanet_api.getMSXSpeciesNameID(),
|
|
151
|
+
self.epanet_api.getMSXSpeciesType(),
|
|
152
|
+
self.epanet_api.getMSXSpeciesUnits()):
|
|
153
|
+
if species_type == "BULK":
|
|
154
|
+
bulk_species.append(species_id)
|
|
155
|
+
bulk_species_mass_unit.append(massunit_to_id(mass_unit))
|
|
156
|
+
elif species_type == "WALL":
|
|
157
|
+
surface_species.append(species_id)
|
|
158
|
+
surface_species_mass_unit.append(massunit_to_id(mass_unit))
|
|
159
|
+
|
|
160
|
+
return SensorConfig(nodes=self.epanet_api.getNodeNameID(),
|
|
161
|
+
links=self.epanet_api.getLinkNameID(),
|
|
162
|
+
valves=self.epanet_api.getLinkValveNameID(),
|
|
163
|
+
pumps=self.epanet_api.getLinkPumpNameID(),
|
|
164
|
+
tanks=self.epanet_api.getNodeTankNameID(),
|
|
165
|
+
bulk_species=bulk_species,
|
|
166
|
+
surface_species=surface_species,
|
|
167
|
+
flow_unit=flow_unit,
|
|
168
|
+
quality_unit=quality_unit,
|
|
169
|
+
bulk_species_mass_unit=bulk_species_mass_unit,
|
|
170
|
+
surface_species_mass_unit=surface_species_mass_unit,
|
|
171
|
+
surface_species_area_unit=surface_species_area_unit,
|
|
172
|
+
node_id_to_idx=node_id_to_idx,
|
|
173
|
+
link_id_to_idx=link_id_to_idx,
|
|
174
|
+
valve_id_to_idx=valve_id_to_idx,
|
|
175
|
+
pump_id_to_idx=pump_id_to_idx,
|
|
176
|
+
tank_id_to_idx=tank_id_to_idx,
|
|
177
|
+
bulkspecies_id_to_idx=bulkspecies_id_to_idx,
|
|
178
|
+
surfacespecies_id_to_idx=surfacespecies_id_to_idx)
|
|
179
|
+
|
|
135
180
|
@property
|
|
136
181
|
def f_inp_in(self) -> str:
|
|
137
182
|
"""
|
|
@@ -295,6 +340,21 @@ class ScenarioSimulator():
|
|
|
295
340
|
return deepcopy(list(filter(lambda e: isinstance(e, SensorFault),
|
|
296
341
|
self.__sensor_reading_events)))
|
|
297
342
|
|
|
343
|
+
@property
|
|
344
|
+
def sensor_reading_attacks(self) -> list[SensorReadingAttack]:
|
|
345
|
+
"""
|
|
346
|
+
Gets all sensor reading attacks.
|
|
347
|
+
|
|
348
|
+
Returns
|
|
349
|
+
-------
|
|
350
|
+
list[:class:`~epyt_flow.simulation.events.sensor_reading_attacks.SensorReadingAttack`]
|
|
351
|
+
All sensor reading attacks.
|
|
352
|
+
"""
|
|
353
|
+
self.__adapt_to_network_changes()
|
|
354
|
+
|
|
355
|
+
return deepcopy(list(filter(lambda e: isinstance(e, SensorReadingAttack)),
|
|
356
|
+
self.__sensor_reading_events))
|
|
357
|
+
|
|
298
358
|
@property
|
|
299
359
|
def sensor_reading_events(self) -> list[SensorReadingEvent]:
|
|
300
360
|
"""
|
|
@@ -312,19 +372,6 @@ class ScenarioSimulator():
|
|
|
312
372
|
def __adapt_to_network_changes(self):
|
|
313
373
|
nodes = self.epanet_api.getNodeNameID()
|
|
314
374
|
links = self.epanet_api.getLinkNameID()
|
|
315
|
-
valves = self.epanet_api.getLinkValveNameID()
|
|
316
|
-
pumps = self.epanet_api.getLinkPumpNameID()
|
|
317
|
-
tanks = self.epanet_api.getNodeTankNameID()
|
|
318
|
-
bulk_species = []
|
|
319
|
-
surface_species = []
|
|
320
|
-
|
|
321
|
-
if self.__f_msx_in is not None:
|
|
322
|
-
for species_id, species_type in zip(self.epanet_api.getMSXSpeciesNameID(),
|
|
323
|
-
self.epanet_api.getMSXSpeciesType()):
|
|
324
|
-
if species_type == "BULK":
|
|
325
|
-
bulk_species.append(species_id)
|
|
326
|
-
elif species_type == "WALL":
|
|
327
|
-
surface_species.append(species_id)
|
|
328
375
|
|
|
329
376
|
node_id_to_idx = {node_id: self.epanet_api.getNodeIndex(node_id) - 1 for node_id in nodes}
|
|
330
377
|
link_id_to_idx = {link_id: self.epanet_api.getLinkIndex(link_id) - 1 for link_id in links}
|
|
@@ -334,35 +381,24 @@ class ScenarioSimulator():
|
|
|
334
381
|
bulkspecies_id_to_idx = None
|
|
335
382
|
surfacespecies_id_to_idx = None
|
|
336
383
|
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
new_sensor_config.demand_sensors = self.__sensor_config.demand_sensors
|
|
356
|
-
new_sensor_config.quality_node_sensors = self.__sensor_config.quality_node_sensors
|
|
357
|
-
new_sensor_config.quality_link_sensors = self.__sensor_config.quality_link_sensors
|
|
358
|
-
new_sensor_config.pump_state_sensors = self.__sensor_config.pump_state_sensors
|
|
359
|
-
new_sensor_config.valve_state_sensors = self.__sensor_config.valve_state_sensors
|
|
360
|
-
new_sensor_config.tank_volume_sensors = self.__sensor_config.tank_volume_sensors
|
|
361
|
-
new_sensor_config.bulk_species_node_sensors = self.__sensor_config.bulk_species_node_sensors
|
|
362
|
-
new_sensor_config.bulk_species_link_sensors = self.__sensor_config.bulk_species_link_sensors
|
|
363
|
-
new_sensor_config.surface_species_sensors = self.__sensor_config.surface_species_sensors
|
|
364
|
-
|
|
365
|
-
self.__sensor_config = new_sensor_config
|
|
384
|
+
# Adapt sensor configuration to potential cahnges in the network's topology
|
|
385
|
+
new_sensor_config = self.__get_empty_sensor_config(node_id_to_idx, link_id_to_idx,
|
|
386
|
+
valve_id_to_idx, pump_id_to_idx,
|
|
387
|
+
tank_id_to_idx, bulkspecies_id_to_idx,
|
|
388
|
+
surfacespecies_id_to_idx)
|
|
389
|
+
new_sensor_config.pressure_sensors = self.__sensor_config.pressure_sensors
|
|
390
|
+
new_sensor_config.flow_sensors = self.__sensor_config.flow_sensors
|
|
391
|
+
new_sensor_config.demand_sensors = self.__sensor_config.demand_sensors
|
|
392
|
+
new_sensor_config.quality_node_sensors = self.__sensor_config.quality_node_sensors
|
|
393
|
+
new_sensor_config.quality_link_sensors = self.__sensor_config.quality_link_sensors
|
|
394
|
+
new_sensor_config.pump_state_sensors = self.__sensor_config.pump_state_sensors
|
|
395
|
+
new_sensor_config.valve_state_sensors = self.__sensor_config.valve_state_sensors
|
|
396
|
+
new_sensor_config.tank_volume_sensors = self.__sensor_config.tank_volume_sensors
|
|
397
|
+
new_sensor_config.bulk_species_node_sensors = self.__sensor_config.bulk_species_node_sensors
|
|
398
|
+
new_sensor_config.bulk_species_link_sensors = self.__sensor_config.bulk_species_link_sensors
|
|
399
|
+
new_sensor_config.surface_species_sensors = self.__sensor_config.surface_species_sensors
|
|
400
|
+
|
|
401
|
+
self.__sensor_config = new_sensor_config
|
|
366
402
|
|
|
367
403
|
def close(self):
|
|
368
404
|
"""
|
|
@@ -381,22 +417,185 @@ class ScenarioSimulator():
|
|
|
381
417
|
def __exit__(self, *args):
|
|
382
418
|
self.close()
|
|
383
419
|
|
|
420
|
+
def save_to_epanet_file(self, inp_file_path: str, msx_file_path: str = None,
|
|
421
|
+
export_sensor_config: bool = True) -> None:
|
|
422
|
+
"""
|
|
423
|
+
Exports this scenario to EPANET files -- i.e. an .inp file
|
|
424
|
+
and (optionally) a .msx file if EPANET-MSX was loaded.
|
|
425
|
+
|
|
426
|
+
Parameters
|
|
427
|
+
----------
|
|
428
|
+
inp_file_path : `str`
|
|
429
|
+
Path to the .inp file where this scenario will be stored.
|
|
430
|
+
|
|
431
|
+
If 'inp_file_path' is None, 'msx_file_path' must not be None!
|
|
432
|
+
msx_file_path : `str`, optional
|
|
433
|
+
Path to the .msx file where this MSX component of this scneario will be stored.
|
|
434
|
+
|
|
435
|
+
Note that this is only applicable if EPANET-MSX was loaded.
|
|
436
|
+
|
|
437
|
+
The default is None.
|
|
438
|
+
export_sensor_config : `bool`, optional
|
|
439
|
+
If True, the current sensor placement is exported as well.
|
|
440
|
+
|
|
441
|
+
The default is True.
|
|
442
|
+
"""
|
|
443
|
+
if inp_file_path is None and msx_file_path is None:
|
|
444
|
+
raise ValueError("At least one of the paths (.inp and .msx) must not be None")
|
|
445
|
+
if inp_file_path is not None:
|
|
446
|
+
if not isinstance(inp_file_path, str):
|
|
447
|
+
raise TypeError("'inp_file_path' must be an instance of 'str' " +
|
|
448
|
+
f"but not of '{type(inp_file_path)}'")
|
|
449
|
+
if msx_file_path is not None:
|
|
450
|
+
if not isinstance(msx_file_path, str):
|
|
451
|
+
raise TypeError("msx_file_path' msut be an instance of 'str' " +
|
|
452
|
+
f"but not of {type(msx_file_path)}")
|
|
453
|
+
if not isinstance(export_sensor_config, bool):
|
|
454
|
+
raise TypeError("'export_sensor_config' must be an instance of 'bool' " +
|
|
455
|
+
f"but not of '{type(export_sensor_config)}'")
|
|
456
|
+
|
|
457
|
+
def __override_report_section(file_in: str, report_desc: str) -> None:
|
|
458
|
+
with open(file_in, mode="r+", encoding="utf-8") as f_in:
|
|
459
|
+
# Find and remove exiting REPORT section
|
|
460
|
+
content = f_in.read()
|
|
461
|
+
try:
|
|
462
|
+
report_section_start_idx = content.index("[REPORT]")
|
|
463
|
+
report_section_end_idx = content.index("[", report_section_start_idx + 1)
|
|
464
|
+
|
|
465
|
+
content = content[:report_section_start_idx] + content[report_section_end_idx:]
|
|
466
|
+
f_in.seek(0)
|
|
467
|
+
f_in.write(content)
|
|
468
|
+
f_in.truncate()
|
|
469
|
+
except ValueError:
|
|
470
|
+
pass
|
|
471
|
+
|
|
472
|
+
# Write new REPORT section in the very end of the file
|
|
473
|
+
write_end_section = False
|
|
474
|
+
try:
|
|
475
|
+
end_idx = content.index("[END]")
|
|
476
|
+
write_end_section = True
|
|
477
|
+
f_in.seek(end_idx)
|
|
478
|
+
except ValueError:
|
|
479
|
+
pass
|
|
480
|
+
f_in.write(report_desc)
|
|
481
|
+
if write_end_section is True:
|
|
482
|
+
f_in.write("\n[END]")
|
|
483
|
+
|
|
484
|
+
if inp_file_path is not None:
|
|
485
|
+
self.epanet_api.saveInputFile(inp_file_path)
|
|
486
|
+
|
|
487
|
+
if export_sensor_config is True:
|
|
488
|
+
report_desc = "\n\n[REPORT]\n"
|
|
489
|
+
report_desc += "ENERGY YES\n"
|
|
490
|
+
report_desc += "STATUS YES\n"
|
|
491
|
+
|
|
492
|
+
nodes = []
|
|
493
|
+
links = []
|
|
494
|
+
|
|
495
|
+
# Parse sensor config
|
|
496
|
+
pressure_sensors = self.__sensor_config.pressure_sensors
|
|
497
|
+
if len(pressure_sensors) != 0:
|
|
498
|
+
report_desc += "Pressure YES\n"
|
|
499
|
+
nodes += pressure_sensors
|
|
500
|
+
|
|
501
|
+
flow_sensors = self.__sensor_config.flow_sensors
|
|
502
|
+
if len(flow_sensors) != 0:
|
|
503
|
+
report_desc += "Flow YES\n"
|
|
504
|
+
links += flow_sensors
|
|
505
|
+
|
|
506
|
+
demand_sensors = self.__sensor_config.demand_sensors
|
|
507
|
+
if len(demand_sensors) != 0:
|
|
508
|
+
report_desc += "Demand YES\n"
|
|
509
|
+
nodes += demand_sensors
|
|
510
|
+
|
|
511
|
+
node_quality_sensors = self.__sensor_config.quality_node_sensors
|
|
512
|
+
if len(node_quality_sensors) != 0:
|
|
513
|
+
report_desc += "Quality YES\n"
|
|
514
|
+
nodes += node_quality_sensors
|
|
515
|
+
|
|
516
|
+
link_quality_sensors = self.__sensor_config.quality_link_sensors
|
|
517
|
+
if len(link_quality_sensors) != 0:
|
|
518
|
+
if len(node_quality_sensors) == 0:
|
|
519
|
+
report_desc += "Quality YES\n"
|
|
520
|
+
links += link_quality_sensors
|
|
521
|
+
|
|
522
|
+
# Create final REPORT section
|
|
523
|
+
nodes = list(set(nodes))
|
|
524
|
+
links = list(set(links))
|
|
525
|
+
|
|
526
|
+
if len(nodes) != 0:
|
|
527
|
+
if set(nodes) == set(self.__sensor_config.nodes):
|
|
528
|
+
nodes = ["ALL"]
|
|
529
|
+
report_desc += f"NODES {' '.join(nodes)}\n"
|
|
530
|
+
|
|
531
|
+
if len(links) != 0:
|
|
532
|
+
if set(links) == set(self.__sensor_config.links):
|
|
533
|
+
links = ["ALL"]
|
|
534
|
+
report_desc += f"LINKS {' '.join(links)}\n"
|
|
535
|
+
|
|
536
|
+
__override_report_section(inp_file_path, report_desc)
|
|
537
|
+
|
|
538
|
+
if self.__f_msx_in is not None and msx_file_path is not None:
|
|
539
|
+
self.epanet_api.saveMSXFile(msx_file_path)
|
|
540
|
+
|
|
541
|
+
if export_sensor_config is True:
|
|
542
|
+
report_desc = "\n\n[REPORT]\n"
|
|
543
|
+
species = []
|
|
544
|
+
nodes = []
|
|
545
|
+
links = []
|
|
546
|
+
|
|
547
|
+
# Parse sensor config
|
|
548
|
+
bulk_species_node_sensors = self.__sensor_config.bulk_species_node_sensors
|
|
549
|
+
for bulk_species_id in bulk_species_node_sensors.keys():
|
|
550
|
+
species.append(bulk_species_id)
|
|
551
|
+
nodes += bulk_species_node_sensors[bulk_species_id]
|
|
552
|
+
|
|
553
|
+
bulk_species_link_sensors = self.__sensor_config.bulk_species_link_sensors
|
|
554
|
+
for bulk_species_id in bulk_species_link_sensors.keys():
|
|
555
|
+
species.append(bulk_species_id)
|
|
556
|
+
links += bulk_species_link_sensors[bulk_species_id]
|
|
557
|
+
|
|
558
|
+
surface_species_link_sensors = self.__sensor_config.surface_species_sensors
|
|
559
|
+
for surface_species_id in surface_species_link_sensors.keys():
|
|
560
|
+
species.append(surface_species_id)
|
|
561
|
+
links += surface_species_link_sensors[surface_species_id]
|
|
562
|
+
|
|
563
|
+
nodes = list(set(nodes))
|
|
564
|
+
links = list((set(links)))
|
|
565
|
+
species = list(set(species))
|
|
566
|
+
|
|
567
|
+
# Create REPORT section
|
|
568
|
+
if len(nodes) != 0:
|
|
569
|
+
if set(nodes) == set(self.__sensor_config.nodes):
|
|
570
|
+
nodes = ["ALL"]
|
|
571
|
+
report_desc += f"NODES {' '.join(nodes)}\n"
|
|
572
|
+
|
|
573
|
+
if len(links) != 0:
|
|
574
|
+
if set(links) == set(self.__sensor_config.links):
|
|
575
|
+
links = ["ALL"]
|
|
576
|
+
report_desc += f"LINKS {' '.join(links)}\n"
|
|
577
|
+
|
|
578
|
+
for species_id in species:
|
|
579
|
+
report_desc += f"SPECIES {species_id} YES\n"
|
|
580
|
+
|
|
581
|
+
__override_report_section(msx_file_path, report_desc)
|
|
582
|
+
|
|
384
583
|
def get_flow_units(self) -> int:
|
|
385
584
|
"""
|
|
386
585
|
Gets the flow units.
|
|
387
586
|
|
|
388
587
|
Will be one of the following EPANET toolkit constants:
|
|
389
588
|
|
|
390
|
-
- EN_CFS = 0
|
|
391
|
-
- EN_GPM = 1
|
|
392
|
-
- EN_MGD = 2
|
|
393
|
-
- EN_IMGD = 3
|
|
394
|
-
- EN_AFD = 4
|
|
395
|
-
- EN_LPS = 5
|
|
396
|
-
- EN_LPM = 6
|
|
397
|
-
- EN_MLD = 7
|
|
398
|
-
- EN_CMH = 8
|
|
399
|
-
- EN_CMD = 9
|
|
589
|
+
- EN_CFS = 0 (cu foot/sec)
|
|
590
|
+
- EN_GPM = 1 (gal/min)
|
|
591
|
+
- EN_MGD = 2 (Million gal/day)
|
|
592
|
+
- EN_IMGD = 3 (Imperial MGD)
|
|
593
|
+
- EN_AFD = 4 (ac-foot/day)
|
|
594
|
+
- EN_LPS = 5 (liter/sec)
|
|
595
|
+
- EN_LPM = 6 (liter/min)
|
|
596
|
+
- EN_MLD = 7 (Megaliter/day)
|
|
597
|
+
- EN_CMH = 8 (cubic meter/hr)
|
|
598
|
+
- EN_CMD = 9 (cubic meter/day)
|
|
400
599
|
|
|
401
600
|
Returns
|
|
402
601
|
-------
|
|
@@ -405,6 +604,27 @@ class ScenarioSimulator():
|
|
|
405
604
|
"""
|
|
406
605
|
return self.epanet_api.api.ENgetflowunits()
|
|
407
606
|
|
|
607
|
+
def get_units_category(self) -> int:
|
|
608
|
+
"""
|
|
609
|
+
Gets the category of units -- i.e. US Customary or SI Metric units.
|
|
610
|
+
|
|
611
|
+
Will be one of the following constants:
|
|
612
|
+
|
|
613
|
+
- UNITS_USCUSTOM = 0 (US Customary)
|
|
614
|
+
- UNITS_SIMETRIC = 1 (SI Metric)
|
|
615
|
+
|
|
616
|
+
Returns
|
|
617
|
+
-------
|
|
618
|
+
`int`
|
|
619
|
+
Units category.
|
|
620
|
+
"""
|
|
621
|
+
if self.get_flow_units() in [ToolkitConstants.EN_CFS, ToolkitConstants.EN_GPM,
|
|
622
|
+
ToolkitConstants.EN_MGD, ToolkitConstants.EN_IMGD,
|
|
623
|
+
ToolkitConstants.EN_AFD]:
|
|
624
|
+
return UNITS_USCUSTOM
|
|
625
|
+
else:
|
|
626
|
+
return UNITS_SIMETRIC
|
|
627
|
+
|
|
408
628
|
def get_hydraulic_time_step(self) -> int:
|
|
409
629
|
"""
|
|
410
630
|
Gets the hydraulic time step -- i.e. time step in the hydraulic simulation.
|
|
@@ -471,7 +691,7 @@ class ScenarioSimulator():
|
|
|
471
691
|
return {"code": qual_info.QualityCode,
|
|
472
692
|
"type": qual_info.QualityType,
|
|
473
693
|
"chemical_name": qual_info.QualityChemName,
|
|
474
|
-
"units": qual_info.QualityChemUnits,
|
|
694
|
+
"units": qualityunit_to_id(qual_info.QualityChemUnits),
|
|
475
695
|
"trace_node_id": qual_info.TraceNode}
|
|
476
696
|
|
|
477
697
|
def get_scenario_config(self) -> ScenarioConfig:
|
|
@@ -518,6 +738,10 @@ class ScenarioSimulator():
|
|
|
518
738
|
n_quantities = self.epanet_api.getNodeCount() * 3 + self.epanet_api.getNodeTankCount() + \
|
|
519
739
|
self.epanet_api.getLinkValveCount() + self.epanet_api.getLinkPumpCount() + \
|
|
520
740
|
self.epanet_api.getLinkCount() * 2
|
|
741
|
+
|
|
742
|
+
if self.__f_msx_in is not None:
|
|
743
|
+
n_quantities += self.epanet_api.getLinkCount() * 2 + self.epanet_api.getNodeCount()
|
|
744
|
+
|
|
521
745
|
n_bytes_per_quantity = 64
|
|
522
746
|
|
|
523
747
|
return n_time_steps * n_quantities * n_bytes_per_quantity * .000001
|
|
@@ -537,6 +761,9 @@ class ScenarioSimulator():
|
|
|
537
761
|
nodes_id = self.epanet_api.getNodeNameID()
|
|
538
762
|
nodes_elevation = self.epanet_api.getNodeElevations()
|
|
539
763
|
nodes_type = [self.epanet_api.TYPENODE[i] for i in self.epanet_api.getNodeTypeIndex()]
|
|
764
|
+
nodes_coord = [self.epanet_api.api.ENgetcoord(node_idx)
|
|
765
|
+
for node_idx in self.epanet_api.getNodeIndex()]
|
|
766
|
+
node_tank_names = self.epanet_api.getNodeTankNameID()
|
|
540
767
|
|
|
541
768
|
links_id = self.epanet_api.getLinkNameID()
|
|
542
769
|
links_data = self.epanet_api.getNodesConnectingLinksID()
|
|
@@ -549,8 +776,16 @@ class ScenarioSimulator():
|
|
|
549
776
|
|
|
550
777
|
# Build graph describing the topology
|
|
551
778
|
nodes = []
|
|
552
|
-
for
|
|
553
|
-
|
|
779
|
+
for node_id, node_elevation, node_type, node_coord in zip(nodes_id, nodes_elevation,
|
|
780
|
+
nodes_type, nodes_coord):
|
|
781
|
+
node_info = {"elevation": node_elevation,
|
|
782
|
+
"coord": node_coord,
|
|
783
|
+
"type": node_type}
|
|
784
|
+
if node_type == "TANK":
|
|
785
|
+
node_tank_idx = node_tank_names.index(node_id) + 1
|
|
786
|
+
node_info["diameter"] = float(self.epanet_api.getNodeTankDiameter(node_tank_idx))
|
|
787
|
+
|
|
788
|
+
nodes.append((node_id, node_info))
|
|
554
789
|
|
|
555
790
|
links = []
|
|
556
791
|
for link_id, link, diameter, length, roughness_coeff, bulk_coeff, wall_coeff, loss_coeff \
|
|
@@ -561,7 +796,8 @@ class ScenarioSimulator():
|
|
|
561
796
|
"bulk_coeff": bulk_coeff, "wall_coeff": wall_coeff,
|
|
562
797
|
"loss_coeff": loss_coeff}))
|
|
563
798
|
|
|
564
|
-
return NetworkTopology(f_inp=self.f_inp_in, nodes=nodes, links=links
|
|
799
|
+
return NetworkTopology(f_inp=self.f_inp_in, nodes=nodes, links=links,
|
|
800
|
+
units=self.get_units_category())
|
|
565
801
|
|
|
566
802
|
def randomize_demands(self) -> None:
|
|
567
803
|
"""
|
|
@@ -974,6 +1210,9 @@ class ScenarioSimulator():
|
|
|
974
1210
|
:class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
|
|
975
1211
|
Quality simulation results as SCADA data.
|
|
976
1212
|
"""
|
|
1213
|
+
if self.__f_msx_in is None:
|
|
1214
|
+
raise ValueError("No .msx file specified")
|
|
1215
|
+
|
|
977
1216
|
result = None
|
|
978
1217
|
|
|
979
1218
|
gen = self.run_advanced_quality_simulation_as_generator
|
|
@@ -1035,6 +1274,9 @@ class ScenarioSimulator():
|
|
|
1035
1274
|
Generator containing the current EPANET-MSX simulation results as SCADA data
|
|
1036
1275
|
(i.e. species concentrations).
|
|
1037
1276
|
"""
|
|
1277
|
+
if self.__f_msx_in is None:
|
|
1278
|
+
raise ValueError("No .msx file specified")
|
|
1279
|
+
|
|
1038
1280
|
# Load pre-computed hydraulics
|
|
1039
1281
|
self.epanet_api.useMSXHydraulicFile(hyd_file_in)
|
|
1040
1282
|
|
|
@@ -1508,8 +1750,7 @@ class ScenarioSimulator():
|
|
|
1508
1750
|
# Fetch data
|
|
1509
1751
|
pressure_data = self.epanet_api.getNodePressure().reshape(1, -1)
|
|
1510
1752
|
flow_data = self.epanet_api.getLinkFlows().reshape(1, -1)
|
|
1511
|
-
demand_data = self.epanet_api.getNodeActualDemand().reshape(1,
|
|
1512
|
-
-1) # TODO: Does not go back after emitter coefficient is changed back to zero
|
|
1753
|
+
demand_data = self.epanet_api.getNodeActualDemand().reshape(1, -1)
|
|
1513
1754
|
quality_node_data = self.epanet_api.getNodeActualQuality().reshape(1, -1)
|
|
1514
1755
|
quality_link_data = self.epanet_api.getLinkActualQuality().reshape(1, -1)
|
|
1515
1756
|
pumps_state_data = self.epanet_api.getLinkPumpState().reshape(1, -1)
|
|
@@ -1610,7 +1851,7 @@ class ScenarioSimulator():
|
|
|
1610
1851
|
def set_general_parameters(self, demand_model: dict = None, simulation_duration: int = None,
|
|
1611
1852
|
hydraulic_time_step: int = None, quality_time_step: int = None,
|
|
1612
1853
|
reporting_time_step: int = None, reporting_time_start: int = None,
|
|
1613
|
-
|
|
1854
|
+
flow_units_id: int = None, quality_model: dict = None) -> None:
|
|
1614
1855
|
"""
|
|
1615
1856
|
Sets some general parameters.
|
|
1616
1857
|
|
|
@@ -1655,22 +1896,22 @@ class ScenarioSimulator():
|
|
|
1655
1896
|
Start time (in seconds) at which reporting of hydraulic and quality states starts.
|
|
1656
1897
|
|
|
1657
1898
|
The default is None.
|
|
1658
|
-
|
|
1899
|
+
flow_units_id : `int`, optional
|
|
1659
1900
|
Specifies the flow units -- i.e. all flows will be reported in these units.
|
|
1660
1901
|
If None, the units from the .inp file will be used.
|
|
1661
1902
|
|
|
1662
1903
|
Must be one of the following EPANET toolkit constants:
|
|
1663
1904
|
|
|
1664
|
-
- EN_CFS
|
|
1665
|
-
- EN_GPM
|
|
1666
|
-
- EN_MGD
|
|
1667
|
-
- EN_IMGD = 3
|
|
1668
|
-
- EN_AFD
|
|
1669
|
-
- EN_LPS
|
|
1670
|
-
- EN_LPM
|
|
1671
|
-
- EN_MLD
|
|
1672
|
-
- EN_CMH
|
|
1673
|
-
- EN_CMD
|
|
1905
|
+
- EN_CFS = 0 (cubic foot/sec)
|
|
1906
|
+
- EN_GPM = 1 (gal/min)
|
|
1907
|
+
- EN_MGD = 2 (Million gal/day)
|
|
1908
|
+
- EN_IMGD = 3 (Imperial MGD)
|
|
1909
|
+
- EN_AFD = 4 (ac-foot/day)
|
|
1910
|
+
- EN_LPS = 5 (liter/sec)
|
|
1911
|
+
- EN_LPM = 6 (liter/min)
|
|
1912
|
+
- EN_MLD = 7 (Megaliter/day)
|
|
1913
|
+
- EN_CMH = 8 (cubic meter/hr)
|
|
1914
|
+
- EN_CMD = 9 (cubic meter/day)
|
|
1674
1915
|
|
|
1675
1916
|
The default is None.
|
|
1676
1917
|
quality_model : `dict`, optional
|
|
@@ -1691,8 +1932,7 @@ class ScenarioSimulator():
|
|
|
1691
1932
|
if not isinstance(simulation_duration, int) or simulation_duration <= 0:
|
|
1692
1933
|
raise ValueError("'simulation_duration' must be a positive integer specifying " +
|
|
1693
1934
|
"the number of seconds to simulate")
|
|
1694
|
-
self.epanet_api.setTimeSimulationDuration(simulation_duration)
|
|
1695
|
-
# duration from .inp file seems to break EPANET-MSX
|
|
1935
|
+
self.epanet_api.setTimeSimulationDuration(simulation_duration)
|
|
1696
1936
|
|
|
1697
1937
|
if hydraulic_time_step is not None:
|
|
1698
1938
|
if not isinstance(hydraulic_time_step, int) or hydraulic_time_step <= 0:
|
|
@@ -1727,29 +1967,29 @@ class ScenarioSimulator():
|
|
|
1727
1967
|
"greater than the hydraulic time step")
|
|
1728
1968
|
self.epanet_api.setTimeQualityStep(quality_time_step)
|
|
1729
1969
|
|
|
1730
|
-
if
|
|
1731
|
-
if
|
|
1970
|
+
if flow_units_id is not None:
|
|
1971
|
+
if flow_units_id == ToolkitConstants.EN_CFS:
|
|
1732
1972
|
self.epanet_api.setFlowUnitsCFS()
|
|
1733
|
-
elif
|
|
1973
|
+
elif flow_units_id == ToolkitConstants.EN_GPM:
|
|
1734
1974
|
self.epanet_api.setFlowUnitsGPM()
|
|
1735
|
-
elif
|
|
1975
|
+
elif flow_units_id == ToolkitConstants.EN_MGD:
|
|
1736
1976
|
self.epanet_api.setFlowUnitsMGD()
|
|
1737
|
-
elif
|
|
1977
|
+
elif flow_units_id == ToolkitConstants.EN_IMGD:
|
|
1738
1978
|
self.epanet_api.setFlowUnitsIMGD()
|
|
1739
|
-
elif
|
|
1979
|
+
elif flow_units_id == ToolkitConstants.EN_AFD:
|
|
1740
1980
|
self.epanet_api.setFlowUnitsAFD()
|
|
1741
|
-
elif
|
|
1981
|
+
elif flow_units_id == ToolkitConstants.EN_LPS:
|
|
1742
1982
|
self.epanet_api.setFlowUnitsLPS()
|
|
1743
|
-
elif
|
|
1983
|
+
elif flow_units_id == ToolkitConstants.EN_LPM:
|
|
1744
1984
|
self.epanet_api.setFlowUnitsLPM()
|
|
1745
|
-
elif
|
|
1985
|
+
elif flow_units_id == ToolkitConstants.EN_MLD:
|
|
1746
1986
|
self.epanet_api.setFlowUnitsMLD()
|
|
1747
|
-
elif
|
|
1987
|
+
elif flow_units_id == ToolkitConstants.EN_CMH:
|
|
1748
1988
|
self.epanet_api.setFlowUnitsCMH()
|
|
1749
|
-
elif
|
|
1989
|
+
elif flow_units_id == ToolkitConstants.EN_CMD:
|
|
1750
1990
|
self.epanet_api.setFlowUnitsCMD()
|
|
1751
1991
|
else:
|
|
1752
|
-
raise ValueError(f"Unknown flow units '{
|
|
1992
|
+
raise ValueError(f"Unknown flow units '{flow_units_id}'")
|
|
1753
1993
|
|
|
1754
1994
|
if quality_model is not None:
|
|
1755
1995
|
if quality_model["type"] == "NONE":
|
|
@@ -1758,7 +1998,7 @@ class ScenarioSimulator():
|
|
|
1758
1998
|
self.epanet_api.setQualityType("age")
|
|
1759
1999
|
elif quality_model["type"] == "CHEM":
|
|
1760
2000
|
self.epanet_api.setQualityType("chem", quality_model["chemical_name"],
|
|
1761
|
-
quality_model["units"])
|
|
2001
|
+
qualityunit_to_str(quality_model["units"]))
|
|
1762
2002
|
elif quality_model["type"] == "TRACE":
|
|
1763
2003
|
self.epanet_api.setQualityType("trace", quality_model["trace_node_id"])
|
|
1764
2004
|
else:
|
|
@@ -1809,7 +2049,7 @@ class ScenarioSimulator():
|
|
|
1809
2049
|
self.set_general_parameters(quality_model={"type": "AGE"})
|
|
1810
2050
|
|
|
1811
2051
|
def enable_chemical_analysis(self, chemical_name: str = "Chlorine",
|
|
1812
|
-
chemical_units:
|
|
2052
|
+
chemical_units: int = MASS_UNIT_MG) -> None:
|
|
1813
2053
|
"""
|
|
1814
2054
|
Sets chemical analysis.
|
|
1815
2055
|
|
|
@@ -1823,9 +2063,13 @@ class ScenarioSimulator():
|
|
|
1823
2063
|
The default is "Chlorine".
|
|
1824
2064
|
chemical_units : `str`, optional
|
|
1825
2065
|
Units that the chemical is measured in.
|
|
1826
|
-
Either "mg/L" or "ug/L".
|
|
1827
2066
|
|
|
1828
|
-
|
|
2067
|
+
Must be one of the following constants:
|
|
2068
|
+
|
|
2069
|
+
- MASS_UNIT_MG = 4 (mg/L)
|
|
2070
|
+
- MASS_UNIT_UG = 5 (ug/L)
|
|
2071
|
+
|
|
2072
|
+
The default is MASS_UNIT_MG.
|
|
1829
2073
|
"""
|
|
1830
2074
|
self.__adapt_to_network_changes()
|
|
1831
2075
|
|