epyt-flow 0.1.1__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 +25 -18
- 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 +329 -94
- epyt_flow/simulation/sensor_config.py +369 -8
- epyt_flow/topology.py +47 -6
- epyt_flow/utils.py +75 -18
- {epyt_flow-0.1.1.dist-info → epyt_flow-0.2.0.dist-info}/METADATA +18 -1
- {epyt_flow-0.1.1.dist-info → epyt_flow-0.2.0.dist-info}/RECORD +25 -18
- epyt_flow/EPANET/compile.sh +0 -4
- {epyt_flow-0.1.1.dist-info → epyt_flow-0.2.0.dist-info}/LICENSE +0 -0
- {epyt_flow-0.1.1.dist-info → epyt_flow-0.2.0.dist-info}/WHEEL +0 -0
- {epyt_flow-0.1.1.dist-info → epyt_flow-0.2.0.dist-info}/top_level.txt +0 -0
|
@@ -118,7 +118,6 @@ class ScadaData(Serializable):
|
|
|
118
118
|
|
|
119
119
|
The default is False.
|
|
120
120
|
"""
|
|
121
|
-
|
|
122
121
|
def __init__(self, sensor_config: SensorConfig, sensor_readings_time: np.ndarray,
|
|
123
122
|
pressure_data_raw: np.ndarray = None, flow_data_raw: np.ndarray = None,
|
|
124
123
|
demand_data_raw: np.ndarray = None, node_quality_data_raw: np.ndarray = None,
|
|
@@ -132,7 +131,8 @@ class ScadaData(Serializable):
|
|
|
132
131
|
sensor_faults: list[SensorFault] = [],
|
|
133
132
|
sensor_reading_attacks: list[SensorReadingAttack] = [],
|
|
134
133
|
sensor_reading_events: list[SensorReadingEvent] = [],
|
|
135
|
-
sensor_noise: SensorNoise = None, frozen_sensor_config: bool = False,
|
|
134
|
+
sensor_noise: SensorNoise = None, frozen_sensor_config: bool = False,
|
|
135
|
+
**kwds):
|
|
136
136
|
if not isinstance(sensor_config, SensorConfig):
|
|
137
137
|
raise TypeError("'sensor_config' must be an instance of " +
|
|
138
138
|
"'epyt_flow.simulation.SensorConfig' but not of " +
|
|
@@ -73,13 +73,7 @@ class ScadaDataExport():
|
|
|
73
73
|
"""
|
|
74
74
|
old_sensor_config = scada_data.sensor_config
|
|
75
75
|
|
|
76
|
-
sensor_config = SensorConfig(
|
|
77
|
-
links=old_sensor_config.links,
|
|
78
|
-
valves=old_sensor_config.valves,
|
|
79
|
-
pumps=old_sensor_config.pumps,
|
|
80
|
-
tanks=old_sensor_config.tanks,
|
|
81
|
-
bulk_species=old_sensor_config.bulk_species,
|
|
82
|
-
surface_species=old_sensor_config.surface_species)
|
|
76
|
+
sensor_config = SensorConfig.create_empty_sensor_config(old_sensor_config)
|
|
83
77
|
sensor_config.pressure_sensors = sensor_config.nodes
|
|
84
78
|
sensor_config.flow_sensors = sensor_config.links
|
|
85
79
|
sensor_config.demand_sensors = sensor_config.nodes
|
|
@@ -582,24 +582,18 @@ class ScenarioConfig(Serializable):
|
|
|
582
582
|
sensor_config = None
|
|
583
583
|
from .scenario_simulator import ScenarioSimulator
|
|
584
584
|
with ScenarioSimulator(f_inp_in) as scenario:
|
|
585
|
-
sensor_config = SensorConfig(
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
valve_state_sensors=valve_state_sensors,
|
|
598
|
-
pump_state_sensors=pump_state_sensors,
|
|
599
|
-
tank_volume_sensors=tank_volume_sensors,
|
|
600
|
-
bulk_species_node_sensors=bulk_species_node_sensors,
|
|
601
|
-
bulk_species_link_sensors=bulk_species_link_sensors,
|
|
602
|
-
surface_species_sensors=surface_species_sensors)
|
|
585
|
+
sensor_config = SensorConfig.create_empty_sensor_config(scenario.sensor_config)
|
|
586
|
+
sensor_config.pressure_sensors = pressure_sensors
|
|
587
|
+
sensor_config.flow_sensors = flow_sensors
|
|
588
|
+
sensor_config.demand_sensors = demand_sensors
|
|
589
|
+
sensor_config.quality_node_sensors = node_quality_sensors
|
|
590
|
+
sensor_config.quality_link_sensors = link_quality_sensors
|
|
591
|
+
sensor_config.valve_state_sensors = valve_state_sensors
|
|
592
|
+
sensor_config.pump_state_sensors = pump_state_sensors
|
|
593
|
+
sensor_config.tank_volume_sensors = tank_volume_sensors
|
|
594
|
+
sensor_config.bulk_species_node_sensors = bulk_species_node_sensors
|
|
595
|
+
sensor_config.bulk_species_link_sensors = bulk_species_link_sensors
|
|
596
|
+
sensor_config.surface_species_sensors = surface_species_sensors
|
|
603
597
|
|
|
604
598
|
# Create final scenario configuration
|
|
605
599
|
return ScenarioConfig(f_inp_in=f_inp_in, f_msx_in=f_msx_in, general_params=general_params,
|
|
@@ -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,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": qualityunits_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
|
|
@@ -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,6 +1199,9 @@ 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
|
|
@@ -1035,6 +1263,9 @@ class ScenarioSimulator():
|
|
|
1035
1263
|
Generator containing the current EPANET-MSX simulation results as SCADA data
|
|
1036
1264
|
(i.e. species concentrations).
|
|
1037
1265
|
"""
|
|
1266
|
+
if self.__f_msx_in is None:
|
|
1267
|
+
raise ValueError("No .msx file specified")
|
|
1268
|
+
|
|
1038
1269
|
# Load pre-computed hydraulics
|
|
1039
1270
|
self.epanet_api.useMSXHydraulicFile(hyd_file_in)
|
|
1040
1271
|
|
|
@@ -1661,16 +1892,16 @@ class ScenarioSimulator():
|
|
|
1661
1892
|
|
|
1662
1893
|
Must be one of the following EPANET toolkit constants:
|
|
1663
1894
|
|
|
1664
|
-
- EN_CFS = 0
|
|
1665
|
-
- EN_GPM = 1
|
|
1666
|
-
- EN_MGD = 2
|
|
1667
|
-
- EN_IMGD = 3
|
|
1668
|
-
- EN_AFD = 4
|
|
1669
|
-
- EN_LPS = 5
|
|
1670
|
-
- EN_LPM = 6
|
|
1671
|
-
- EN_MLD = 7
|
|
1672
|
-
- EN_CMH = 8
|
|
1673
|
-
- 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)
|
|
1674
1905
|
|
|
1675
1906
|
The default is None.
|
|
1676
1907
|
quality_model : `dict`, optional
|
|
@@ -1758,7 +1989,7 @@ class ScenarioSimulator():
|
|
|
1758
1989
|
self.epanet_api.setQualityType("age")
|
|
1759
1990
|
elif quality_model["type"] == "CHEM":
|
|
1760
1991
|
self.epanet_api.setQualityType("chem", quality_model["chemical_name"],
|
|
1761
|
-
quality_model["units"])
|
|
1992
|
+
qualityunits_to_str(quality_model["units"]))
|
|
1762
1993
|
elif quality_model["type"] == "TRACE":
|
|
1763
1994
|
self.epanet_api.setQualityType("trace", quality_model["trace_node_id"])
|
|
1764
1995
|
else:
|
|
@@ -1809,7 +2040,7 @@ class ScenarioSimulator():
|
|
|
1809
2040
|
self.set_general_parameters(quality_model={"type": "AGE"})
|
|
1810
2041
|
|
|
1811
2042
|
def enable_chemical_analysis(self, chemical_name: str = "Chlorine",
|
|
1812
|
-
chemical_units:
|
|
2043
|
+
chemical_units: int = MASS_UNIT_MG) -> None:
|
|
1813
2044
|
"""
|
|
1814
2045
|
Sets chemical analysis.
|
|
1815
2046
|
|
|
@@ -1823,9 +2054,13 @@ class ScenarioSimulator():
|
|
|
1823
2054
|
The default is "Chlorine".
|
|
1824
2055
|
chemical_units : `str`, optional
|
|
1825
2056
|
Units that the chemical is measured in.
|
|
1826
|
-
Either "mg/L" or "ug/L".
|
|
1827
2057
|
|
|
1828
|
-
|
|
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.
|
|
1829
2064
|
"""
|
|
1830
2065
|
self.__adapt_to_network_changes()
|
|
1831
2066
|
|