epyt-flow 0.1.0__py3-none-any.whl → 0.2.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/VERSION +1 -1
- epyt_flow/__init__.py +21 -8
- 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 +167 -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 +59 -24
- epyt_flow/simulation/scada/scada_data.py +2 -2
- epyt_flow/simulation/scada/scada_data_export.py +1 -7
- epyt_flow/simulation/scenario_config.py +12 -18
- epyt_flow/simulation/scenario_simulator.py +387 -132
- epyt_flow/simulation/sensor_config.py +370 -9
- epyt_flow/topology.py +47 -6
- epyt_flow/utils.py +75 -18
- {epyt_flow-0.1.0.dist-info → epyt_flow-0.2.0.dist-info}/METADATA +29 -5
- {epyt_flow-0.1.0.dist-info → epyt_flow-0.2.0.dist-info}/RECORD +25 -18
- epyt_flow/EPANET/compile.sh +0 -4
- {epyt_flow-0.1.0.dist-info → epyt_flow-0.2.0.dist-info}/LICENSE +0 -0
- {epyt_flow-0.1.0.dist-info → epyt_flow-0.2.0.dist-info}/WHEEL +0 -0
- {epyt_flow-0.1.0.dist-info → epyt_flow-0.2.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, qualityunits_to_id, \
|
|
21
|
+
qualityunits_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 = qualityunits_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,57 +372,33 @@ 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}
|
|
331
378
|
valve_id_to_idx = None # {valve_id: self.epanet_api.getLinkValveIndex(valve_id) for valve_id in valves}
|
|
332
|
-
pump_id_to_idx = None
|
|
333
|
-
tank_id_to_idx = None
|
|
379
|
+
pump_id_to_idx = None # {pump_id: self.epanet_api.getLinkPumpIndex(pump_id) - 1 for pump_id in pumps}
|
|
380
|
+
tank_id_to_idx = None # {tank_id: self.epanet_api.getNodeTankIndex(tank_id) - 1 for tank_id in tanks}
|
|
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": qualityunits_to_id(qual_info.QualityChemUnits),
|
|
475
695
|
"trace_node_id": qual_info.TraceNode}
|
|
476
696
|
|
|
477
697
|
def get_scenario_config(self) -> ScenarioConfig:
|
|
@@ -516,8 +736,12 @@ class ScenarioSimulator():
|
|
|
516
736
|
n_time_steps = int(self.epanet_api.getTimeSimulationDuration() /
|
|
517
737
|
self.epanet_api.getTimeReportingStep())
|
|
518
738
|
n_quantities = self.epanet_api.getNodeCount() * 3 + self.epanet_api.getNodeTankCount() + \
|
|
519
|
-
|
|
520
|
-
|
|
739
|
+
self.epanet_api.getLinkValveCount() + self.epanet_api.getLinkPumpCount() + \
|
|
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
|
|
@@ -561,7 +785,8 @@ class ScenarioSimulator():
|
|
|
561
785
|
"bulk_coeff": bulk_coeff, "wall_coeff": wall_coeff,
|
|
562
786
|
"loss_coeff": loss_coeff}))
|
|
563
787
|
|
|
564
|
-
return NetworkTopology(f_inp=self.f_inp_in, nodes=nodes, links=links
|
|
788
|
+
return NetworkTopology(f_inp=self.f_inp_in, nodes=nodes, links=links,
|
|
789
|
+
units=self.get_units_category())
|
|
565
790
|
|
|
566
791
|
def randomize_demands(self) -> None:
|
|
567
792
|
"""
|
|
@@ -974,30 +1199,48 @@ class ScenarioSimulator():
|
|
|
974
1199
|
:class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
|
|
975
1200
|
Quality simulation results as SCADA data.
|
|
976
1201
|
"""
|
|
1202
|
+
if self.__f_msx_in is None:
|
|
1203
|
+
raise ValueError("No .msx file specified")
|
|
1204
|
+
|
|
977
1205
|
result = None
|
|
978
1206
|
|
|
979
1207
|
gen = self.run_advanced_quality_simulation_as_generator
|
|
980
1208
|
for scada_data in gen(hyd_file_in=hyd_file_in,
|
|
981
1209
|
verbose=verbose,
|
|
1210
|
+
return_as_dict=True,
|
|
982
1211
|
frozen_sensor_config=frozen_sensor_config):
|
|
983
1212
|
if result is None:
|
|
984
|
-
result =
|
|
1213
|
+
result = {}
|
|
1214
|
+
for data_type, data in scada_data.items():
|
|
1215
|
+
result[data_type] = [data]
|
|
985
1216
|
else:
|
|
986
|
-
|
|
1217
|
+
for data_type, data in scada_data.items():
|
|
1218
|
+
result[data_type].append(data)
|
|
987
1219
|
|
|
988
|
-
|
|
1220
|
+
# Build ScadaData instance
|
|
1221
|
+
for data_type in result:
|
|
1222
|
+
if not any(d is None for d in result[data_type]):
|
|
1223
|
+
result[data_type] = np.concatenate(result[data_type], axis=0)
|
|
1224
|
+
else:
|
|
1225
|
+
result[data_type] = None
|
|
1226
|
+
|
|
1227
|
+
return ScadaData(**result,
|
|
1228
|
+
sensor_config=self.__sensor_config,
|
|
1229
|
+
sensor_reading_events=self.__sensor_reading_events,
|
|
1230
|
+
sensor_noise=self.__sensor_noise,
|
|
1231
|
+
frozen_sensor_config=frozen_sensor_config)
|
|
989
1232
|
|
|
990
1233
|
def run_advanced_quality_simulation_as_generator(self, hyd_file_in: str, verbose: bool = False,
|
|
991
1234
|
support_abort: bool = False,
|
|
992
1235
|
return_as_dict: bool = False,
|
|
993
1236
|
frozen_sensor_config: bool = False
|
|
994
|
-
) -> Generator[Union[ScadaData, dict],
|
|
995
|
-
bool, None]:
|
|
1237
|
+
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
996
1238
|
"""
|
|
997
1239
|
Runs an advanced quality analysis using EPANET-MSX.
|
|
998
1240
|
|
|
999
1241
|
Parameters
|
|
1000
1242
|
----------
|
|
1243
|
+
support_abort : `bool`, optional
|
|
1001
1244
|
hyd_file_in : `str`
|
|
1002
1245
|
Path to an EPANET .hyd file for storing the simulated hydraulics --
|
|
1003
1246
|
the quality analysis is computed using those hydraulics.
|
|
@@ -1020,6 +1263,9 @@ class ScenarioSimulator():
|
|
|
1020
1263
|
Generator containing the current EPANET-MSX simulation results as SCADA data
|
|
1021
1264
|
(i.e. species concentrations).
|
|
1022
1265
|
"""
|
|
1266
|
+
if self.__f_msx_in is None:
|
|
1267
|
+
raise ValueError("No .msx file specified")
|
|
1268
|
+
|
|
1023
1269
|
# Load pre-computed hydraulics
|
|
1024
1270
|
self.epanet_api.useMSXHydraulicFile(hyd_file_in)
|
|
1025
1271
|
|
|
@@ -1054,13 +1300,13 @@ class ScenarioSimulator():
|
|
|
1054
1300
|
bulk_species_link_concentrations = []
|
|
1055
1301
|
for species_idx in bulk_species_idx:
|
|
1056
1302
|
cur_species_concentrations = []
|
|
1057
|
-
for node_idx in range(1, n_nodes+1):
|
|
1303
|
+
for node_idx in range(1, n_nodes + 1):
|
|
1058
1304
|
concen = msx_get_cur_value(0, node_idx, species_idx)
|
|
1059
1305
|
cur_species_concentrations.append(concen)
|
|
1060
1306
|
bulk_species_node_concentrations.append(cur_species_concentrations)
|
|
1061
1307
|
|
|
1062
1308
|
cur_species_concentrations = []
|
|
1063
|
-
for link_idx in range(1, n_links+1):
|
|
1309
|
+
for link_idx in range(1, n_links + 1):
|
|
1064
1310
|
concen = msx_get_cur_value(1, link_idx, species_idx)
|
|
1065
1311
|
cur_species_concentrations.append(concen)
|
|
1066
1312
|
bulk_species_link_concentrations.append(cur_species_concentrations)
|
|
@@ -1068,13 +1314,13 @@ class ScenarioSimulator():
|
|
|
1068
1314
|
if len(bulk_species_node_concentrations) == 0:
|
|
1069
1315
|
bulk_species_node_concentrations = None
|
|
1070
1316
|
else:
|
|
1071
|
-
bulk_species_node_concentrations = np.array(bulk_species_node_concentrations)
|
|
1317
|
+
bulk_species_node_concentrations = np.array(bulk_species_node_concentrations). \
|
|
1072
1318
|
reshape((1, len(bulk_species_idx), n_nodes))
|
|
1073
1319
|
|
|
1074
1320
|
if len(bulk_species_link_concentrations) == 0:
|
|
1075
1321
|
bulk_species_link_concentrations = None
|
|
1076
1322
|
else:
|
|
1077
|
-
bulk_species_link_concentrations = np.array(bulk_species_link_concentrations)
|
|
1323
|
+
bulk_species_link_concentrations = np.array(bulk_species_link_concentrations). \
|
|
1078
1324
|
reshape((1, len(bulk_species_idx), n_links))
|
|
1079
1325
|
|
|
1080
1326
|
# Surface species
|
|
@@ -1082,7 +1328,7 @@ class ScenarioSimulator():
|
|
|
1082
1328
|
for species_idx in surface_species_idx:
|
|
1083
1329
|
cur_species_concentrations = []
|
|
1084
1330
|
|
|
1085
|
-
for link_idx in range(1, n_links+1):
|
|
1331
|
+
for link_idx in range(1, n_links + 1):
|
|
1086
1332
|
concen = msx_get_cur_value(1, link_idx, species_idx)
|
|
1087
1333
|
cur_species_concentrations.append(concen)
|
|
1088
1334
|
|
|
@@ -1091,7 +1337,7 @@ class ScenarioSimulator():
|
|
|
1091
1337
|
if len(surface_species_concentrations) == 0:
|
|
1092
1338
|
surface_species_concentrations = None
|
|
1093
1339
|
else:
|
|
1094
|
-
surface_species_concentrations = np.array(surface_species_concentrations)
|
|
1340
|
+
surface_species_concentrations = np.array(surface_species_concentrations). \
|
|
1095
1341
|
reshape((1, len(surface_species_idx), n_links))
|
|
1096
1342
|
|
|
1097
1343
|
return bulk_species_node_concentrations, bulk_species_link_concentrations, \
|
|
@@ -1102,14 +1348,17 @@ class ScenarioSimulator():
|
|
|
1102
1348
|
surface_species_concentrations = __get_concentrations(init_qual=True)
|
|
1103
1349
|
|
|
1104
1350
|
if verbose is True:
|
|
1105
|
-
|
|
1351
|
+
try:
|
|
1352
|
+
next(progress_bar)
|
|
1353
|
+
except StopIteration:
|
|
1354
|
+
pass
|
|
1106
1355
|
|
|
1107
1356
|
if reporting_time_start == 0:
|
|
1108
1357
|
if return_as_dict is True:
|
|
1109
1358
|
yield {"bulk_species_node_concentration_raw": bulk_species_node_concentrations,
|
|
1110
1359
|
"bulk_species_link_concentration_raw": bulk_species_link_concentrations,
|
|
1111
1360
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
1112
|
-
"sensor_readings_time": np.array([
|
|
1361
|
+
"sensor_readings_time": np.array([0])}
|
|
1113
1362
|
else:
|
|
1114
1363
|
yield ScadaData(sensor_config=self.__sensor_config,
|
|
1115
1364
|
bulk_species_node_concentration_raw=bulk_species_node_concentrations,
|
|
@@ -1131,21 +1380,24 @@ class ScenarioSimulator():
|
|
|
1131
1380
|
# Compute current time step
|
|
1132
1381
|
total_time, tleft = self.epanet_api.stepMSXQualityAnalysisTimeLeft()
|
|
1133
1382
|
|
|
1383
|
+
if verbose is True:
|
|
1384
|
+
try:
|
|
1385
|
+
next(progress_bar)
|
|
1386
|
+
except StopIteration:
|
|
1387
|
+
pass
|
|
1388
|
+
|
|
1134
1389
|
# Fetch data at regular time intervals
|
|
1135
1390
|
if total_time % hyd_time_step == 0:
|
|
1136
1391
|
bulk_species_node_concentrations, bulk_species_link_concentrations, \
|
|
1137
1392
|
surface_species_concentrations = __get_concentrations()
|
|
1138
1393
|
|
|
1139
|
-
if verbose is True:
|
|
1140
|
-
next(progress_bar)
|
|
1141
|
-
|
|
1142
1394
|
# Report results in a regular time interval only!
|
|
1143
1395
|
if total_time % reporting_time_step == 0 and total_time >= reporting_time_start:
|
|
1144
1396
|
if return_as_dict is True:
|
|
1145
1397
|
yield {"bulk_species_node_concentration_raw":
|
|
1146
|
-
|
|
1398
|
+
bulk_species_node_concentrations,
|
|
1147
1399
|
"bulk_species_link_concentration_raw":
|
|
1148
|
-
|
|
1400
|
+
bulk_species_link_concentrations,
|
|
1149
1401
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
1150
1402
|
"sensor_readings_time": np.array([total_time])}
|
|
1151
1403
|
else:
|
|
@@ -1216,13 +1468,13 @@ class ScenarioSimulator():
|
|
|
1216
1468
|
support_abort: bool = False,
|
|
1217
1469
|
return_as_dict: bool = False,
|
|
1218
1470
|
frozen_sensor_config: bool = False,
|
|
1219
|
-
) -> Generator[Union[ScadaData, dict],
|
|
1220
|
-
bool, None]:
|
|
1471
|
+
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
1221
1472
|
"""
|
|
1222
1473
|
Runs a basic quality analysis using EPANET.
|
|
1223
1474
|
|
|
1224
1475
|
Parameters
|
|
1225
1476
|
----------
|
|
1477
|
+
support_abort : `bool`, optional
|
|
1226
1478
|
hyd_file_in : `str`
|
|
1227
1479
|
Path to an EPANET .hyd file for storing the simulated hydraulics --
|
|
1228
1480
|
the quality analysis is computed using those hydraulics.
|
|
@@ -1277,16 +1529,16 @@ class ScenarioSimulator():
|
|
|
1277
1529
|
|
|
1278
1530
|
if verbose is True:
|
|
1279
1531
|
if (total_time + tstep) % requested_time_step == 0:
|
|
1280
|
-
|
|
1532
|
+
try:
|
|
1533
|
+
next(progress_bar)
|
|
1534
|
+
except StopIteration:
|
|
1535
|
+
pass
|
|
1281
1536
|
|
|
1282
1537
|
# Compute current time step
|
|
1283
1538
|
t = self.epanet_api.runQualityAnalysis()
|
|
1284
1539
|
total_time = t
|
|
1285
1540
|
|
|
1286
1541
|
# Fetch data
|
|
1287
|
-
quality_node_data = None
|
|
1288
|
-
quality_link_data = None
|
|
1289
|
-
|
|
1290
1542
|
quality_node_data = self.epanet_api.getNodeActualQuality().reshape(1, -1)
|
|
1291
1543
|
quality_link_data = self.epanet_api.getLinkActualQuality().reshape(1, -1)
|
|
1292
1544
|
|
|
@@ -1381,7 +1633,11 @@ class ScenarioSimulator():
|
|
|
1381
1633
|
if hyd_export_old is not None:
|
|
1382
1634
|
shutil.copyfile(hyd_export, hyd_export_old)
|
|
1383
1635
|
|
|
1384
|
-
|
|
1636
|
+
try:
|
|
1637
|
+
# temp solution
|
|
1638
|
+
os.remove(hyd_export)
|
|
1639
|
+
except:
|
|
1640
|
+
warnings.warn(f"Failed to remove temporary file '{hyd_export}'")
|
|
1385
1641
|
|
|
1386
1642
|
return result
|
|
1387
1643
|
|
|
@@ -1464,8 +1720,11 @@ class ScenarioSimulator():
|
|
|
1464
1720
|
first_itr = False
|
|
1465
1721
|
|
|
1466
1722
|
if verbose is True:
|
|
1467
|
-
if (total_time + tstep) % requested_time_step == 0:
|
|
1723
|
+
#if (total_time + tstep) % requested_time_step == 0:
|
|
1724
|
+
try:
|
|
1468
1725
|
next(progress_bar)
|
|
1726
|
+
except StopIteration:
|
|
1727
|
+
pass
|
|
1469
1728
|
|
|
1470
1729
|
# Apply system events in a regular time interval only!
|
|
1471
1730
|
if (total_time + tstep) % requested_time_step == 0:
|
|
@@ -1478,14 +1737,6 @@ class ScenarioSimulator():
|
|
|
1478
1737
|
total_time = t
|
|
1479
1738
|
|
|
1480
1739
|
# Fetch data
|
|
1481
|
-
pressure_data = None
|
|
1482
|
-
flow_data = None
|
|
1483
|
-
demand_data = None
|
|
1484
|
-
quality_node_data = None
|
|
1485
|
-
quality_link_data = None
|
|
1486
|
-
pumps_state_data = None
|
|
1487
|
-
valves_state_data = None
|
|
1488
|
-
|
|
1489
1740
|
pressure_data = self.epanet_api.getNodePressure().reshape(1, -1)
|
|
1490
1741
|
flow_data = self.epanet_api.getLinkFlows().reshape(1, -1)
|
|
1491
1742
|
demand_data = self.epanet_api.getNodeActualDemand().reshape(1,
|
|
@@ -1641,16 +1892,16 @@ class ScenarioSimulator():
|
|
|
1641
1892
|
|
|
1642
1893
|
Must be one of the following EPANET toolkit constants:
|
|
1643
1894
|
|
|
1644
|
-
- EN_CFS = 0
|
|
1645
|
-
- EN_GPM = 1
|
|
1646
|
-
- EN_MGD = 2
|
|
1647
|
-
- EN_IMGD = 3
|
|
1648
|
-
- EN_AFD = 4
|
|
1649
|
-
- EN_LPS = 5
|
|
1650
|
-
- EN_LPM = 6
|
|
1651
|
-
- EN_MLD = 7
|
|
1652
|
-
- EN_CMH = 8
|
|
1653
|
-
- EN_CMD = 9
|
|
1895
|
+
- EN_CFS = 0 (cu foot/sec)
|
|
1896
|
+
- EN_GPM = 1 (gal/min)
|
|
1897
|
+
- EN_MGD = 2 (Million gal/day)
|
|
1898
|
+
- EN_IMGD = 3 (Imperial MGD)
|
|
1899
|
+
- EN_AFD = 4 (ac-foot/day)
|
|
1900
|
+
- EN_LPS = 5 (liter/sec)
|
|
1901
|
+
- EN_LPM = 6 (liter/min)
|
|
1902
|
+
- EN_MLD = 7 (Megaliter/day)
|
|
1903
|
+
- EN_CMH = 8 (cubic meter/hr)
|
|
1904
|
+
- EN_CMD = 9 (cubic meter/day)
|
|
1654
1905
|
|
|
1655
1906
|
The default is None.
|
|
1656
1907
|
quality_model : `dict`, optional
|
|
@@ -1738,7 +1989,7 @@ class ScenarioSimulator():
|
|
|
1738
1989
|
self.epanet_api.setQualityType("age")
|
|
1739
1990
|
elif quality_model["type"] == "CHEM":
|
|
1740
1991
|
self.epanet_api.setQualityType("chem", quality_model["chemical_name"],
|
|
1741
|
-
quality_model["units"])
|
|
1992
|
+
qualityunits_to_str(quality_model["units"]))
|
|
1742
1993
|
elif quality_model["type"] == "TRACE":
|
|
1743
1994
|
self.epanet_api.setQualityType("trace", quality_model["trace_node_id"])
|
|
1744
1995
|
else:
|
|
@@ -1789,7 +2040,7 @@ class ScenarioSimulator():
|
|
|
1789
2040
|
self.set_general_parameters(quality_model={"type": "AGE"})
|
|
1790
2041
|
|
|
1791
2042
|
def enable_chemical_analysis(self, chemical_name: str = "Chlorine",
|
|
1792
|
-
chemical_units:
|
|
2043
|
+
chemical_units: int = MASS_UNIT_MG) -> None:
|
|
1793
2044
|
"""
|
|
1794
2045
|
Sets chemical analysis.
|
|
1795
2046
|
|
|
@@ -1803,9 +2054,13 @@ class ScenarioSimulator():
|
|
|
1803
2054
|
The default is "Chlorine".
|
|
1804
2055
|
chemical_units : `str`, optional
|
|
1805
2056
|
Units that the chemical is measured in.
|
|
1806
|
-
Either "mg/L" or "ug/L".
|
|
1807
2057
|
|
|
1808
|
-
|
|
2058
|
+
Must be one of the following constants:
|
|
2059
|
+
|
|
2060
|
+
- MASS_UNIT_MG = 4 (mg/L)
|
|
2061
|
+
- MASS_UNIT_UG = 5 (ug/L)
|
|
2062
|
+
|
|
2063
|
+
The default is MASS_UNIT_MG.
|
|
1809
2064
|
"""
|
|
1810
2065
|
self.__adapt_to_network_changes()
|
|
1811
2066
|
|