epyt-flow 0.7.3__py3-none-any.whl → 0.8.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epyt_flow/VERSION +1 -1
- epyt_flow/data/benchmarks/leakdb.py +2 -2
- epyt_flow/data/networks.py +14 -26
- epyt_flow/gym/scenario_control_env.py +129 -10
- epyt_flow/serialization.py +10 -3
- epyt_flow/simulation/events/__init__.py +1 -0
- epyt_flow/simulation/events/leakages.py +55 -12
- epyt_flow/simulation/events/quality_events.py +194 -0
- epyt_flow/simulation/events/system_event.py +5 -0
- epyt_flow/simulation/scada/scada_data.py +512 -64
- epyt_flow/simulation/scada/scada_data_export.py +7 -5
- epyt_flow/simulation/scenario_config.py +13 -2
- epyt_flow/simulation/scenario_simulator.py +275 -187
- epyt_flow/simulation/scenario_visualizer.py +1259 -13
- {epyt_flow-0.7.3.dist-info → epyt_flow-0.8.1.dist-info}/METADATA +31 -30
- {epyt_flow-0.7.3.dist-info → epyt_flow-0.8.1.dist-info}/RECORD +19 -18
- {epyt_flow-0.7.3.dist-info → epyt_flow-0.8.1.dist-info}/WHEEL +1 -1
- {epyt_flow-0.7.3.dist-info → epyt_flow-0.8.1.dist-info}/LICENSE +0 -0
- {epyt_flow-0.7.3.dist-info → epyt_flow-0.8.1.dist-info}/top_level.txt +0 -0
|
@@ -61,6 +61,18 @@ class ScenarioSimulator():
|
|
|
61
61
|
----------
|
|
62
62
|
epanet_api : `epyt.epanet`
|
|
63
63
|
API to EPANET and EPANET-MSX.
|
|
64
|
+
_model_uncertainty : :class:`~epyt_flow.uncertainty.model_uncertainty.ModelUncertainty`, protected
|
|
65
|
+
Model uncertainty.
|
|
66
|
+
_sensor_noise : :class:`~epyt_flow.uncertainty.sensor_noise.SensorNoise`, protected
|
|
67
|
+
Sensor noise.
|
|
68
|
+
_sensor_config : :class:`~epyt_flow.simulation.sensor_config.SensorConfig`, protected
|
|
69
|
+
Sensor configuration.
|
|
70
|
+
_controls : list[:class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`], protected
|
|
71
|
+
List of custom control modules.
|
|
72
|
+
_system_events : list[:class:`~epyt_flow.simulation.events.system_event.SystemEvent`], protected
|
|
73
|
+
Lsit of system events such as leakages.
|
|
74
|
+
_sensor_reading_events : list[:class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`], protected
|
|
75
|
+
List of sensor reading events such as sensor override attacks.
|
|
64
76
|
"""
|
|
65
77
|
|
|
66
78
|
def __init__(self, f_inp_in: str = None, f_msx_in: str = None,
|
|
@@ -69,6 +81,8 @@ class ScenarioSimulator():
|
|
|
69
81
|
raise ValueError("'f_inp_in' must be set if 'f_msx_in' is set.")
|
|
70
82
|
if f_inp_in is None and scenario_config is None:
|
|
71
83
|
raise ValueError("Either 'f_inp_in' or 'scenario_config' must be set.")
|
|
84
|
+
if scenario_config is not None and f_inp_in is not None:
|
|
85
|
+
raise ValueError("'f_inp_in' or 'scenario_config' can not be used at the same time")
|
|
72
86
|
if f_inp_in is not None:
|
|
73
87
|
if not isinstance(f_inp_in, str):
|
|
74
88
|
raise TypeError("'f_inp_in' must be an instance of 'str' but not of " +
|
|
@@ -88,12 +102,12 @@ class ScenarioSimulator():
|
|
|
88
102
|
|
|
89
103
|
self.__f_inp_in = f_inp_in if scenario_config is None else scenario_config.f_inp_in
|
|
90
104
|
self.__f_msx_in = f_msx_in if scenario_config is None else scenario_config.f_msx_in
|
|
91
|
-
self.
|
|
92
|
-
self.
|
|
93
|
-
self.
|
|
94
|
-
self.
|
|
95
|
-
self.
|
|
96
|
-
self.
|
|
105
|
+
self._model_uncertainty = ModelUncertainty()
|
|
106
|
+
self._sensor_noise = None
|
|
107
|
+
self._sensor_config = None
|
|
108
|
+
self._controls = []
|
|
109
|
+
self._system_events = []
|
|
110
|
+
self._sensor_reading_events = []
|
|
97
111
|
self.__running_simulation = False
|
|
98
112
|
|
|
99
113
|
custom_epanet_lib = None
|
|
@@ -140,14 +154,11 @@ class ScenarioSimulator():
|
|
|
140
154
|
if self.__f_msx_in is not None:
|
|
141
155
|
if not __file_exists(self.__f_msx_in):
|
|
142
156
|
my_f_msx_in = self.__f_msx_in
|
|
143
|
-
self.__my_f_msx_in = None
|
|
144
157
|
else:
|
|
145
158
|
my_f_msx_in = os.path.join(tmp_folder_path, pathlib.Path(self.__f_msx_in).name)
|
|
146
159
|
shutil.copyfile(self.__f_msx_in, my_f_msx_in)
|
|
147
|
-
self.__my_f_msx_in = my_f_msx_in
|
|
148
160
|
else:
|
|
149
161
|
my_f_msx_in = None
|
|
150
|
-
self.__my_f_msx_in = None
|
|
151
162
|
|
|
152
163
|
self.epanet_api = epanet(my_f_inp_in, ph=self.__f_msx_in is None,
|
|
153
164
|
customlib=custom_epanet_lib, loadfile=True,
|
|
@@ -157,14 +168,14 @@ class ScenarioSimulator():
|
|
|
157
168
|
if self.__f_msx_in is not None:
|
|
158
169
|
self.epanet_api.loadMSXFile(my_f_msx_in, customMSXlib=custom_epanetmsx_lib)
|
|
159
170
|
|
|
160
|
-
self.
|
|
171
|
+
self._sensor_config = self._get_empty_sensor_config()
|
|
161
172
|
if scenario_config is not None:
|
|
162
173
|
if scenario_config.general_params is not None:
|
|
163
174
|
self.set_general_parameters(**scenario_config.general_params)
|
|
164
175
|
|
|
165
|
-
self.
|
|
166
|
-
self.
|
|
167
|
-
self.
|
|
176
|
+
self._model_uncertainty = scenario_config.model_uncertainty
|
|
177
|
+
self._sensor_noise = scenario_config.sensor_noise
|
|
178
|
+
self._sensor_config = scenario_config.sensor_config
|
|
168
179
|
|
|
169
180
|
for control in scenario_config.controls:
|
|
170
181
|
self.add_control(control)
|
|
@@ -173,10 +184,10 @@ class ScenarioSimulator():
|
|
|
173
184
|
for event in scenario_config.sensor_reading_events:
|
|
174
185
|
self.add_sensor_reading_event(event)
|
|
175
186
|
|
|
176
|
-
def
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
187
|
+
def _get_empty_sensor_config(self, node_id_to_idx: dict = None, link_id_to_idx: dict = None,
|
|
188
|
+
valve_id_to_idx: dict = None, pump_id_to_idx: dict = None,
|
|
189
|
+
tank_id_to_idx: dict = None, bulkspecies_id_to_idx: dict = None,
|
|
190
|
+
surfacespecies_id_to_idx: dict = None) -> SensorConfig:
|
|
180
191
|
flow_unit = self.epanet_api.api.ENgetflowunits()
|
|
181
192
|
quality_unit = qualityunit_to_id(self.epanet_api.getQualityInfo().QualityChemUnits)
|
|
182
193
|
bulk_species = []
|
|
@@ -228,7 +239,7 @@ class ScenarioSimulator():
|
|
|
228
239
|
`str`
|
|
229
240
|
Path to the .inp file.
|
|
230
241
|
"""
|
|
231
|
-
self.
|
|
242
|
+
self._adapt_to_network_changes()
|
|
232
243
|
|
|
233
244
|
return self.__f_inp_in
|
|
234
245
|
|
|
@@ -242,7 +253,7 @@ class ScenarioSimulator():
|
|
|
242
253
|
`str`
|
|
243
254
|
Path to the .msx file.
|
|
244
255
|
"""
|
|
245
|
-
self.
|
|
256
|
+
self._adapt_to_network_changes()
|
|
246
257
|
|
|
247
258
|
return self.__f_msx_in
|
|
248
259
|
|
|
@@ -256,13 +267,13 @@ class ScenarioSimulator():
|
|
|
256
267
|
:class:`~epyt_flow.uncertainty.model_uncertainty.ModelUncertainty`
|
|
257
268
|
Model uncertainty.
|
|
258
269
|
"""
|
|
259
|
-
self.
|
|
270
|
+
self._adapt_to_network_changes()
|
|
260
271
|
|
|
261
|
-
return deepcopy(self.
|
|
272
|
+
return deepcopy(self._model_uncertainty)
|
|
262
273
|
|
|
263
274
|
@model_uncertainty.setter
|
|
264
275
|
def model_uncertainty(self, model_uncertainty: ModelUncertainty) -> None:
|
|
265
|
-
self.
|
|
276
|
+
self._adapt_to_network_changes()
|
|
266
277
|
|
|
267
278
|
self.set_model_uncertainty(model_uncertainty)
|
|
268
279
|
|
|
@@ -276,13 +287,13 @@ class ScenarioSimulator():
|
|
|
276
287
|
:class:`~epyt_flow.uncertainty.sensor_noise.SensorNoise`
|
|
277
288
|
Sensor noise.
|
|
278
289
|
"""
|
|
279
|
-
self.
|
|
290
|
+
self._adapt_to_network_changes()
|
|
280
291
|
|
|
281
|
-
return deepcopy(self.
|
|
292
|
+
return deepcopy(self._sensor_noise)
|
|
282
293
|
|
|
283
294
|
@sensor_noise.setter
|
|
284
295
|
def sensor_noise(self, sensor_noise: SensorNoise) -> None:
|
|
285
|
-
self.
|
|
296
|
+
self._adapt_to_network_changes()
|
|
286
297
|
|
|
287
298
|
self.set_sensor_noise(sensor_noise)
|
|
288
299
|
|
|
@@ -296,9 +307,9 @@ class ScenarioSimulator():
|
|
|
296
307
|
:class:`~epyt_flow.simulation.sensor_config.SensorConfig`
|
|
297
308
|
Sensor configuration.
|
|
298
309
|
"""
|
|
299
|
-
self.
|
|
310
|
+
self._adapt_to_network_changes()
|
|
300
311
|
|
|
301
|
-
return deepcopy(self.
|
|
312
|
+
return deepcopy(self._sensor_config)
|
|
302
313
|
|
|
303
314
|
@sensor_config.setter
|
|
304
315
|
def sensor_config(self, sensor_config: SensorConfig) -> None:
|
|
@@ -309,7 +320,7 @@ class ScenarioSimulator():
|
|
|
309
320
|
|
|
310
321
|
sensor_config.validate(self.epanet_api)
|
|
311
322
|
|
|
312
|
-
self.
|
|
323
|
+
self._sensor_config = sensor_config
|
|
313
324
|
|
|
314
325
|
@property
|
|
315
326
|
def controls(self) -> list[AdvancedControlModule]:
|
|
@@ -321,9 +332,9 @@ class ScenarioSimulator():
|
|
|
321
332
|
list[:class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`]
|
|
322
333
|
All control modules.
|
|
323
334
|
"""
|
|
324
|
-
self.
|
|
335
|
+
self._adapt_to_network_changes()
|
|
325
336
|
|
|
326
|
-
return deepcopy(self.
|
|
337
|
+
return deepcopy(self._controls)
|
|
327
338
|
|
|
328
339
|
@property
|
|
329
340
|
def leakages(self) -> list[Leakage]:
|
|
@@ -335,9 +346,9 @@ class ScenarioSimulator():
|
|
|
335
346
|
list[:class:`~epyt_flow.simulation.events.leakages.Leakage`]
|
|
336
347
|
All leakages.
|
|
337
348
|
"""
|
|
338
|
-
self.
|
|
349
|
+
self._adapt_to_network_changes()
|
|
339
350
|
|
|
340
|
-
return deepcopy(list(filter(lambda e: isinstance(e, Leakage), self.
|
|
351
|
+
return deepcopy(list(filter(lambda e: isinstance(e, Leakage), self._system_events)))
|
|
341
352
|
|
|
342
353
|
@property
|
|
343
354
|
def actuator_events(self) -> list[ActuatorEvent]:
|
|
@@ -349,9 +360,9 @@ class ScenarioSimulator():
|
|
|
349
360
|
list[:class:`~epyt_flow.simulation.events.actuator_event.ActuatorEvent`]
|
|
350
361
|
All actuator events.
|
|
351
362
|
"""
|
|
352
|
-
self.
|
|
363
|
+
self._adapt_to_network_changes()
|
|
353
364
|
|
|
354
|
-
return deepcopy(list(filter(lambda e: isinstance(e, ActuatorEvent), self.
|
|
365
|
+
return deepcopy(list(filter(lambda e: isinstance(e, ActuatorEvent), self._system_events)))
|
|
355
366
|
|
|
356
367
|
@property
|
|
357
368
|
def system_events(self) -> list[SystemEvent]:
|
|
@@ -363,9 +374,9 @@ class ScenarioSimulator():
|
|
|
363
374
|
list[:class:`~epyt_flow.simulation.events.system_event.SystemEvent`]
|
|
364
375
|
All system events.
|
|
365
376
|
"""
|
|
366
|
-
self.
|
|
377
|
+
self._adapt_to_network_changes()
|
|
367
378
|
|
|
368
|
-
return deepcopy(self.
|
|
379
|
+
return deepcopy(self._system_events)
|
|
369
380
|
|
|
370
381
|
@property
|
|
371
382
|
def sensor_faults(self) -> list[SensorFault]:
|
|
@@ -377,10 +388,10 @@ class ScenarioSimulator():
|
|
|
377
388
|
list[:class:`~epyt_flow.simulation.events.sensor_faults.SensorFault`]
|
|
378
389
|
All sensor faults.
|
|
379
390
|
"""
|
|
380
|
-
self.
|
|
391
|
+
self._adapt_to_network_changes()
|
|
381
392
|
|
|
382
393
|
return deepcopy(list(filter(lambda e: isinstance(e, SensorFault),
|
|
383
|
-
self.
|
|
394
|
+
self._sensor_reading_events)))
|
|
384
395
|
|
|
385
396
|
@property
|
|
386
397
|
def sensor_reading_attacks(self) -> list[SensorReadingAttack]:
|
|
@@ -392,10 +403,10 @@ class ScenarioSimulator():
|
|
|
392
403
|
list[:class:`~epyt_flow.simulation.events.sensor_reading_attacks.SensorReadingAttack`]
|
|
393
404
|
All sensor reading attacks.
|
|
394
405
|
"""
|
|
395
|
-
self.
|
|
406
|
+
self._adapt_to_network_changes()
|
|
396
407
|
|
|
397
408
|
return deepcopy(list(filter(lambda e: isinstance(e, SensorReadingAttack)),
|
|
398
|
-
self.
|
|
409
|
+
self._sensor_reading_events))
|
|
399
410
|
|
|
400
411
|
@property
|
|
401
412
|
def sensor_reading_events(self) -> list[SensorReadingEvent]:
|
|
@@ -407,11 +418,11 @@ class ScenarioSimulator():
|
|
|
407
418
|
list[:class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`]
|
|
408
419
|
All sensor reading events.
|
|
409
420
|
"""
|
|
410
|
-
self.
|
|
421
|
+
self._adapt_to_network_changes()
|
|
411
422
|
|
|
412
|
-
return deepcopy(self.
|
|
423
|
+
return deepcopy(self._sensor_reading_events)
|
|
413
424
|
|
|
414
|
-
def
|
|
425
|
+
def _adapt_to_network_changes(self):
|
|
415
426
|
nodes = self.epanet_api.getNodeNameID()
|
|
416
427
|
links = self.epanet_api.getLinkNameID()
|
|
417
428
|
|
|
@@ -424,26 +435,26 @@ class ScenarioSimulator():
|
|
|
424
435
|
surfacespecies_id_to_idx = None
|
|
425
436
|
|
|
426
437
|
# Adapt sensor configuration to potential cahnges in the network's topology
|
|
427
|
-
new_sensor_config = self.
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
new_sensor_config.pressure_sensors = self.
|
|
432
|
-
new_sensor_config.flow_sensors = self.
|
|
433
|
-
new_sensor_config.demand_sensors = self.
|
|
434
|
-
new_sensor_config.quality_node_sensors = self.
|
|
435
|
-
new_sensor_config.quality_link_sensors = self.
|
|
436
|
-
new_sensor_config.pump_state_sensors = self.
|
|
437
|
-
new_sensor_config.pump_efficiency_sensors = self.
|
|
438
|
-
new_sensor_config.pump_energyconsumption_sensors = self.
|
|
438
|
+
new_sensor_config = self._get_empty_sensor_config(node_id_to_idx, link_id_to_idx,
|
|
439
|
+
valve_id_to_idx, pump_id_to_idx,
|
|
440
|
+
tank_id_to_idx, bulkspecies_id_to_idx,
|
|
441
|
+
surfacespecies_id_to_idx)
|
|
442
|
+
new_sensor_config.pressure_sensors = self._sensor_config.pressure_sensors
|
|
443
|
+
new_sensor_config.flow_sensors = self._sensor_config.flow_sensors
|
|
444
|
+
new_sensor_config.demand_sensors = self._sensor_config.demand_sensors
|
|
445
|
+
new_sensor_config.quality_node_sensors = self._sensor_config.quality_node_sensors
|
|
446
|
+
new_sensor_config.quality_link_sensors = self._sensor_config.quality_link_sensors
|
|
447
|
+
new_sensor_config.pump_state_sensors = self._sensor_config.pump_state_sensors
|
|
448
|
+
new_sensor_config.pump_efficiency_sensors = self._sensor_config.pump_efficiency_sensors
|
|
449
|
+
new_sensor_config.pump_energyconsumption_sensors = self._sensor_config.\
|
|
439
450
|
pump_energyconsumption_sensors
|
|
440
|
-
new_sensor_config.valve_state_sensors = self.
|
|
441
|
-
new_sensor_config.tank_volume_sensors = self.
|
|
442
|
-
new_sensor_config.bulk_species_node_sensors = self.
|
|
443
|
-
new_sensor_config.bulk_species_link_sensors = self.
|
|
444
|
-
new_sensor_config.surface_species_sensors = self.
|
|
451
|
+
new_sensor_config.valve_state_sensors = self._sensor_config.valve_state_sensors
|
|
452
|
+
new_sensor_config.tank_volume_sensors = self._sensor_config.tank_volume_sensors
|
|
453
|
+
new_sensor_config.bulk_species_node_sensors = self._sensor_config.bulk_species_node_sensors
|
|
454
|
+
new_sensor_config.bulk_species_link_sensors = self._sensor_config.bulk_species_link_sensors
|
|
455
|
+
new_sensor_config.surface_species_sensors = self._sensor_config.surface_species_sensors
|
|
445
456
|
|
|
446
|
-
self.
|
|
457
|
+
self._sensor_config = new_sensor_config
|
|
447
458
|
|
|
448
459
|
def close(self):
|
|
449
460
|
"""
|
|
@@ -542,27 +553,27 @@ class ScenarioSimulator():
|
|
|
542
553
|
links = []
|
|
543
554
|
|
|
544
555
|
# Parse sensor config
|
|
545
|
-
pressure_sensors = self.
|
|
556
|
+
pressure_sensors = self._sensor_config.pressure_sensors
|
|
546
557
|
if len(pressure_sensors) != 0:
|
|
547
558
|
report_desc += "Pressure YES\n"
|
|
548
559
|
nodes += pressure_sensors
|
|
549
560
|
|
|
550
|
-
flow_sensors = self.
|
|
561
|
+
flow_sensors = self._sensor_config.flow_sensors
|
|
551
562
|
if len(flow_sensors) != 0:
|
|
552
563
|
report_desc += "Flow YES\n"
|
|
553
564
|
links += flow_sensors
|
|
554
565
|
|
|
555
|
-
demand_sensors = self.
|
|
566
|
+
demand_sensors = self._sensor_config.demand_sensors
|
|
556
567
|
if len(demand_sensors) != 0:
|
|
557
568
|
report_desc += "Demand YES\n"
|
|
558
569
|
nodes += demand_sensors
|
|
559
570
|
|
|
560
|
-
node_quality_sensors = self.
|
|
571
|
+
node_quality_sensors = self._sensor_config.quality_node_sensors
|
|
561
572
|
if len(node_quality_sensors) != 0:
|
|
562
573
|
report_desc += "Quality YES\n"
|
|
563
574
|
nodes += node_quality_sensors
|
|
564
575
|
|
|
565
|
-
link_quality_sensors = self.
|
|
576
|
+
link_quality_sensors = self._sensor_config.quality_link_sensors
|
|
566
577
|
if len(link_quality_sensors) != 0:
|
|
567
578
|
if len(node_quality_sensors) == 0:
|
|
568
579
|
report_desc += "Quality YES\n"
|
|
@@ -573,12 +584,12 @@ class ScenarioSimulator():
|
|
|
573
584
|
links = list(set(links))
|
|
574
585
|
|
|
575
586
|
if len(nodes) != 0:
|
|
576
|
-
if set(nodes) == set(self.
|
|
587
|
+
if set(nodes) == set(self._sensor_config.nodes):
|
|
577
588
|
nodes = ["ALL"]
|
|
578
589
|
report_desc += f"NODES {' '.join(nodes)}\n"
|
|
579
590
|
|
|
580
591
|
if len(links) != 0:
|
|
581
|
-
if set(links) == set(self.
|
|
592
|
+
if set(links) == set(self._sensor_config.links):
|
|
582
593
|
links = ["ALL"]
|
|
583
594
|
report_desc += f"LINKS {' '.join(links)}\n"
|
|
584
595
|
|
|
@@ -595,17 +606,17 @@ class ScenarioSimulator():
|
|
|
595
606
|
links = []
|
|
596
607
|
|
|
597
608
|
# Parse sensor config
|
|
598
|
-
bulk_species_node_sensors = self.
|
|
609
|
+
bulk_species_node_sensors = self._sensor_config.bulk_species_node_sensors
|
|
599
610
|
for bulk_species_id in bulk_species_node_sensors.keys():
|
|
600
611
|
species.append(bulk_species_id)
|
|
601
612
|
nodes += bulk_species_node_sensors[bulk_species_id]
|
|
602
613
|
|
|
603
|
-
bulk_species_link_sensors = self.
|
|
614
|
+
bulk_species_link_sensors = self._sensor_config.bulk_species_link_sensors
|
|
604
615
|
for bulk_species_id in bulk_species_link_sensors.keys():
|
|
605
616
|
species.append(bulk_species_id)
|
|
606
617
|
links += bulk_species_link_sensors[bulk_species_id]
|
|
607
618
|
|
|
608
|
-
surface_species_link_sensors = self.
|
|
619
|
+
surface_species_link_sensors = self._sensor_config.surface_species_sensors
|
|
609
620
|
for surface_species_id in surface_species_link_sensors.keys():
|
|
610
621
|
species.append(surface_species_id)
|
|
611
622
|
links += surface_species_link_sensors[surface_species_id]
|
|
@@ -616,12 +627,12 @@ class ScenarioSimulator():
|
|
|
616
627
|
|
|
617
628
|
# Create REPORT section
|
|
618
629
|
if len(nodes) != 0:
|
|
619
|
-
if set(nodes) == set(self.
|
|
630
|
+
if set(nodes) == set(self._sensor_config.nodes):
|
|
620
631
|
nodes = ["ALL"]
|
|
621
632
|
report_desc += f"NODES {' '.join(nodes)}\n"
|
|
622
633
|
|
|
623
634
|
if len(links) != 0:
|
|
624
|
-
if set(links) == set(self.
|
|
635
|
+
if set(links) == set(self._sensor_config.links):
|
|
625
636
|
links = ["ALL"]
|
|
626
637
|
report_desc += f"LINKS {' '.join(links)}\n"
|
|
627
638
|
|
|
@@ -767,7 +778,7 @@ class ScenarioSimulator():
|
|
|
767
778
|
:class:`~epyt_flow.simulation.scenario_config.ScenarioConfig`
|
|
768
779
|
Complete scenario specification.
|
|
769
780
|
"""
|
|
770
|
-
self.
|
|
781
|
+
self._adapt_to_network_changes()
|
|
771
782
|
|
|
772
783
|
general_params = {"hydraulic_time_step": self.get_hydraulic_time_step(),
|
|
773
784
|
"quality_time_step": self.get_quality_time_step(),
|
|
@@ -795,7 +806,7 @@ class ScenarioSimulator():
|
|
|
795
806
|
`float`
|
|
796
807
|
Estimated memory consumption in MB.
|
|
797
808
|
"""
|
|
798
|
-
self.
|
|
809
|
+
self._adapt_to_network_changes()
|
|
799
810
|
|
|
800
811
|
n_time_steps = int(self.epanet_api.getTimeSimulationDuration() /
|
|
801
812
|
self.epanet_api.getTimeReportingStep())
|
|
@@ -819,7 +830,7 @@ class ScenarioSimulator():
|
|
|
819
830
|
:class:`~epyt_flow.topology.NetworkTopology`
|
|
820
831
|
Topology of this WDN as a graph.
|
|
821
832
|
"""
|
|
822
|
-
self.
|
|
833
|
+
self._adapt_to_network_changes()
|
|
823
834
|
|
|
824
835
|
# Collect information about the topology of the water distribution network
|
|
825
836
|
nodes_id = self.epanet_api.getNodeNameID()
|
|
@@ -883,6 +894,22 @@ class ScenarioSimulator():
|
|
|
883
894
|
return NetworkTopology(f_inp=self.f_inp_in, nodes=nodes, links=links, pumps=pumps,
|
|
884
895
|
valves=valves, units=self.get_units_category())
|
|
885
896
|
|
|
897
|
+
def plot_topology(self, export_to_file: str = None) -> None:
|
|
898
|
+
"""
|
|
899
|
+
Plots the topology of the water distribution network.
|
|
900
|
+
|
|
901
|
+
Parameters
|
|
902
|
+
----------
|
|
903
|
+
export_to_file : `str`, optional
|
|
904
|
+
Path to the file where the visualization will be stored.
|
|
905
|
+
If None, visualization will be just shown but NOT be stored
|
|
906
|
+
anywhere.
|
|
907
|
+
|
|
908
|
+
The default is None.
|
|
909
|
+
"""
|
|
910
|
+
from .scenario_visualizer import ScenarioVisualizer
|
|
911
|
+
ScenarioVisualizer(self).show_plot(export_to_file)
|
|
912
|
+
|
|
886
913
|
def randomize_demands(self) -> None:
|
|
887
914
|
"""
|
|
888
915
|
Randomizes all demand patterns.
|
|
@@ -890,7 +917,7 @@ class ScenarioSimulator():
|
|
|
890
917
|
if self.__running_simulation is True:
|
|
891
918
|
raise RuntimeError("Can not change general parameters when simulation is running.")
|
|
892
919
|
|
|
893
|
-
self.
|
|
920
|
+
self._adapt_to_network_changes()
|
|
894
921
|
|
|
895
922
|
# Get all demand patterns
|
|
896
923
|
demand_patterns_idx = self.epanet_api.getNodeDemandPatternIndex()
|
|
@@ -927,9 +954,9 @@ class ScenarioSimulator():
|
|
|
927
954
|
demand_pattern : `numpy.ndarray`
|
|
928
955
|
Demand pattern over time. Final demand over time = base_demand * demand_pattern
|
|
929
956
|
"""
|
|
930
|
-
self.
|
|
957
|
+
self._adapt_to_network_changes()
|
|
931
958
|
|
|
932
|
-
if node_id not in self.
|
|
959
|
+
if node_id not in self._sensor_config.nodes:
|
|
933
960
|
raise ValueError(f"Unknown node '{node_id}'")
|
|
934
961
|
if not isinstance(base_demand, float):
|
|
935
962
|
raise TypeError("'base_demand' must be an instance of 'float' " +
|
|
@@ -958,14 +985,14 @@ class ScenarioSimulator():
|
|
|
958
985
|
control : :class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`
|
|
959
986
|
Control module.
|
|
960
987
|
"""
|
|
961
|
-
self.
|
|
988
|
+
self._adapt_to_network_changes()
|
|
962
989
|
|
|
963
990
|
if not isinstance(control, AdvancedControlModule):
|
|
964
991
|
raise TypeError("'control' must be an instance of " +
|
|
965
992
|
"'epyt_flow.simulation.scada.AdvancedControlModule' not of " +
|
|
966
993
|
f"'{type(control)}'")
|
|
967
994
|
|
|
968
|
-
self.
|
|
995
|
+
self._controls.append(control)
|
|
969
996
|
|
|
970
997
|
def add_leakage(self, leakage_event: Leakage) -> None:
|
|
971
998
|
"""
|
|
@@ -976,7 +1003,7 @@ class ScenarioSimulator():
|
|
|
976
1003
|
event : :class:`~epyt_flow.simulation.events.leakages.Leakage`
|
|
977
1004
|
Leakage.
|
|
978
1005
|
"""
|
|
979
|
-
self.
|
|
1006
|
+
self._adapt_to_network_changes()
|
|
980
1007
|
|
|
981
1008
|
if not isinstance(leakage_event, Leakage):
|
|
982
1009
|
raise TypeError("'leakage_event' must be an instance of " +
|
|
@@ -994,7 +1021,7 @@ class ScenarioSimulator():
|
|
|
994
1021
|
event : :class:`~epyt_flow.simulation.events.actuator_events.ActuatorEvent`
|
|
995
1022
|
Actuator event.
|
|
996
1023
|
"""
|
|
997
|
-
self.
|
|
1024
|
+
self._adapt_to_network_changes()
|
|
998
1025
|
|
|
999
1026
|
if not isinstance(event, ActuatorEvent):
|
|
1000
1027
|
raise TypeError("'event' must be an instance of " +
|
|
@@ -1015,7 +1042,7 @@ class ScenarioSimulator():
|
|
|
1015
1042
|
if self.__running_simulation is True:
|
|
1016
1043
|
raise RuntimeError("Can not add events when simulation is running.")
|
|
1017
1044
|
|
|
1018
|
-
self.
|
|
1045
|
+
self._adapt_to_network_changes()
|
|
1019
1046
|
|
|
1020
1047
|
if not isinstance(event, SystemEvent):
|
|
1021
1048
|
raise TypeError("'event' must be an instance of " +
|
|
@@ -1023,7 +1050,7 @@ class ScenarioSimulator():
|
|
|
1023
1050
|
|
|
1024
1051
|
event.init(self.epanet_api)
|
|
1025
1052
|
|
|
1026
|
-
self.
|
|
1053
|
+
self._system_events.append(event)
|
|
1027
1054
|
|
|
1028
1055
|
def add_sensor_fault(self, sensor_fault_event: SensorFault) -> None:
|
|
1029
1056
|
"""
|
|
@@ -1037,16 +1064,16 @@ class ScenarioSimulator():
|
|
|
1037
1064
|
if self.__running_simulation is True:
|
|
1038
1065
|
raise RuntimeError("Can not add events when simulation is running.")
|
|
1039
1066
|
|
|
1040
|
-
self.
|
|
1067
|
+
self._adapt_to_network_changes()
|
|
1041
1068
|
|
|
1042
|
-
sensor_fault_event.validate(self.
|
|
1069
|
+
sensor_fault_event.validate(self._sensor_config)
|
|
1043
1070
|
|
|
1044
1071
|
if not isinstance(sensor_fault_event, SensorFault):
|
|
1045
1072
|
raise TypeError("'sensor_fault_event' must be an instance of " +
|
|
1046
1073
|
"'epyt_flow.simulation.events.SensorFault' not of " +
|
|
1047
1074
|
f"'{type(sensor_fault_event)}'")
|
|
1048
1075
|
|
|
1049
|
-
self.
|
|
1076
|
+
self._sensor_reading_events.append(sensor_fault_event)
|
|
1050
1077
|
|
|
1051
1078
|
def add_sensor_reading_attack(self, sensor_reading_attack: SensorReadingAttack) -> None:
|
|
1052
1079
|
"""
|
|
@@ -1060,16 +1087,16 @@ class ScenarioSimulator():
|
|
|
1060
1087
|
if self.__running_simulation is True:
|
|
1061
1088
|
raise RuntimeError("Can not add events when simulation is running.")
|
|
1062
1089
|
|
|
1063
|
-
self.
|
|
1090
|
+
self._adapt_to_network_changes()
|
|
1064
1091
|
|
|
1065
|
-
sensor_reading_attack.validate(self.
|
|
1092
|
+
sensor_reading_attack.validate(self._sensor_config)
|
|
1066
1093
|
|
|
1067
1094
|
if not isinstance(sensor_reading_attack, SensorReadingAttack):
|
|
1068
1095
|
raise TypeError("'sensor_reading_attack' must be an instance of " +
|
|
1069
1096
|
"'epyt_flow.simulation.events.SensorReadingAttack' not of " +
|
|
1070
1097
|
f"'{type(sensor_reading_attack)}'")
|
|
1071
1098
|
|
|
1072
|
-
self.
|
|
1099
|
+
self._sensor_reading_events.append(sensor_reading_attack)
|
|
1073
1100
|
|
|
1074
1101
|
def add_sensor_reading_event(self, event: SensorReadingEvent) -> None:
|
|
1075
1102
|
"""
|
|
@@ -1083,16 +1110,16 @@ class ScenarioSimulator():
|
|
|
1083
1110
|
if self.__running_simulation is True:
|
|
1084
1111
|
raise RuntimeError("Can not add events when simulation is running.")
|
|
1085
1112
|
|
|
1086
|
-
self.
|
|
1113
|
+
self._adapt_to_network_changes()
|
|
1087
1114
|
|
|
1088
|
-
event.validate(self.
|
|
1115
|
+
event.validate(self._sensor_config)
|
|
1089
1116
|
|
|
1090
1117
|
if not isinstance(event, SensorReadingEvent):
|
|
1091
1118
|
raise TypeError("'event' must be an instance of " +
|
|
1092
1119
|
"'epyt_flow.simulation.events.SensorReadingEvent' not of " +
|
|
1093
1120
|
f"'{type(event)}'")
|
|
1094
1121
|
|
|
1095
|
-
self.
|
|
1122
|
+
self._sensor_reading_events.append(event)
|
|
1096
1123
|
|
|
1097
1124
|
def set_sensors(self, sensor_type: int, sensor_locations: Union[list[str], dict]) -> None:
|
|
1098
1125
|
"""
|
|
@@ -1118,38 +1145,38 @@ class ScenarioSimulator():
|
|
|
1118
1145
|
Locations (IDs) of sensors either as a list or as a dict in the case of
|
|
1119
1146
|
bulk and surface species.
|
|
1120
1147
|
"""
|
|
1121
|
-
self.
|
|
1148
|
+
self._adapt_to_network_changes()
|
|
1122
1149
|
|
|
1123
1150
|
if sensor_type == SENSOR_TYPE_NODE_PRESSURE:
|
|
1124
|
-
self.
|
|
1151
|
+
self._sensor_config.pressure_sensors = sensor_locations
|
|
1125
1152
|
elif sensor_type == SENSOR_TYPE_LINK_FLOW:
|
|
1126
|
-
self.
|
|
1153
|
+
self._sensor_config.flow_sensors = sensor_locations
|
|
1127
1154
|
elif sensor_type == SENSOR_TYPE_NODE_DEMAND:
|
|
1128
|
-
self.
|
|
1155
|
+
self._sensor_config.demand_sensors = sensor_locations
|
|
1129
1156
|
elif sensor_type == SENSOR_TYPE_NODE_QUALITY:
|
|
1130
|
-
self.
|
|
1157
|
+
self._sensor_config.quality_node_sensors = sensor_locations
|
|
1131
1158
|
elif sensor_type == SENSOR_TYPE_LINK_QUALITY:
|
|
1132
|
-
self.
|
|
1159
|
+
self._sensor_config.quality_link_sensors = sensor_locations
|
|
1133
1160
|
elif sensor_type == SENSOR_TYPE_VALVE_STATE:
|
|
1134
|
-
self.
|
|
1161
|
+
self._sensor_config.valve_state_sensors = sensor_locations
|
|
1135
1162
|
elif sensor_type == SENSOR_TYPE_PUMP_STATE:
|
|
1136
|
-
self.
|
|
1163
|
+
self._sensor_config.pump_state_sensors = sensor_locations
|
|
1137
1164
|
elif sensor_type == SENSOR_TYPE_PUMP_EFFICIENCY:
|
|
1138
|
-
self.
|
|
1165
|
+
self._sensor_config.pump_efficiency_sensors = sensor_locations
|
|
1139
1166
|
elif sensor_type == SENSOR_TYPE_PUMP_ENERGYCONSUMPTION:
|
|
1140
|
-
self.
|
|
1167
|
+
self._sensor_config.pump_energyconsumption_sensors = sensor_locations
|
|
1141
1168
|
elif sensor_type == SENSOR_TYPE_TANK_VOLUME:
|
|
1142
|
-
self.
|
|
1169
|
+
self._sensor_config.tank_volume_sensors = sensor_locations
|
|
1143
1170
|
elif sensor_type == SENSOR_TYPE_NODE_BULK_SPECIES:
|
|
1144
|
-
self.
|
|
1171
|
+
self._sensor_config.bulk_species_node_sensors = sensor_locations
|
|
1145
1172
|
elif sensor_type == SENSOR_TYPE_LINK_BULK_SPECIES:
|
|
1146
|
-
self.
|
|
1173
|
+
self._sensor_config.bulk_species_link_sensors = sensor_locations
|
|
1147
1174
|
elif sensor_type == SENSOR_TYPE_SURFACE_SPECIES:
|
|
1148
|
-
self.
|
|
1175
|
+
self._sensor_config.surface_species_sensors = sensor_locations
|
|
1149
1176
|
else:
|
|
1150
1177
|
raise ValueError(f"Unknown sensor type '{sensor_type}'")
|
|
1151
1178
|
|
|
1152
|
-
self.
|
|
1179
|
+
self._sensor_config.validate(self.epanet_api)
|
|
1153
1180
|
|
|
1154
1181
|
def set_pressure_sensors(self, sensor_locations: list[str]) -> None:
|
|
1155
1182
|
"""
|
|
@@ -1176,7 +1203,7 @@ class ScenarioSimulator():
|
|
|
1176
1203
|
if junctions_only is True:
|
|
1177
1204
|
self.set_pressure_sensors(self.epanet_api.getNodeJunctionNameID())
|
|
1178
1205
|
else:
|
|
1179
|
-
self.set_pressure_sensors(self.
|
|
1206
|
+
self.set_pressure_sensors(self._sensor_config.nodes)
|
|
1180
1207
|
|
|
1181
1208
|
def set_flow_sensors(self, sensor_locations: list[str]) -> None:
|
|
1182
1209
|
"""
|
|
@@ -1193,7 +1220,7 @@ class ScenarioSimulator():
|
|
|
1193
1220
|
"""
|
|
1194
1221
|
Places a flow sensors at every link/pipe in the network.
|
|
1195
1222
|
"""
|
|
1196
|
-
self.set_flow_sensors(self.
|
|
1223
|
+
self.set_flow_sensors(self._sensor_config.links)
|
|
1197
1224
|
|
|
1198
1225
|
def set_demand_sensors(self, sensor_locations: list[str]) -> None:
|
|
1199
1226
|
"""
|
|
@@ -1210,7 +1237,7 @@ class ScenarioSimulator():
|
|
|
1210
1237
|
"""
|
|
1211
1238
|
Places a demand sensor at every node in the network.
|
|
1212
1239
|
"""
|
|
1213
|
-
self.set_demand_sensors(self.
|
|
1240
|
+
self.set_demand_sensors(self._sensor_config.nodes)
|
|
1214
1241
|
|
|
1215
1242
|
def set_node_quality_sensors(self, sensor_locations: list[str]) -> None:
|
|
1216
1243
|
"""
|
|
@@ -1228,7 +1255,7 @@ class ScenarioSimulator():
|
|
|
1228
1255
|
"""
|
|
1229
1256
|
Places a water quality sensor at every node in the network.
|
|
1230
1257
|
"""
|
|
1231
|
-
self.set_node_quality_sensors(self.
|
|
1258
|
+
self.set_node_quality_sensors(self._sensor_config.nodes)
|
|
1232
1259
|
|
|
1233
1260
|
def set_link_quality_sensors(self, sensor_locations: list[str]) -> None:
|
|
1234
1261
|
"""
|
|
@@ -1246,7 +1273,7 @@ class ScenarioSimulator():
|
|
|
1246
1273
|
"""
|
|
1247
1274
|
Places a water quality sensor at every link/pipe in the network.
|
|
1248
1275
|
"""
|
|
1249
|
-
self.set_link_quality_sensors(self.
|
|
1276
|
+
self.set_link_quality_sensors(self._sensor_config.links)
|
|
1250
1277
|
|
|
1251
1278
|
def set_valve_sensors(self, sensor_locations: list[str]) -> None:
|
|
1252
1279
|
"""
|
|
@@ -1263,10 +1290,10 @@ class ScenarioSimulator():
|
|
|
1263
1290
|
"""
|
|
1264
1291
|
Places a valve state sensor at every valve in the network.
|
|
1265
1292
|
"""
|
|
1266
|
-
if len(self.
|
|
1293
|
+
if len(self._sensor_config.valves) == 0:
|
|
1267
1294
|
warnings.warn("Network does not contain any valves", UserWarning)
|
|
1268
1295
|
|
|
1269
|
-
self.set_valve_sensors(self.
|
|
1296
|
+
self.set_valve_sensors(self._sensor_config.valves)
|
|
1270
1297
|
|
|
1271
1298
|
def set_pump_state_sensors(self, sensor_locations: list[str]) -> None:
|
|
1272
1299
|
"""
|
|
@@ -1283,10 +1310,10 @@ class ScenarioSimulator():
|
|
|
1283
1310
|
"""
|
|
1284
1311
|
Places a pump state sensor at every pump in the network.
|
|
1285
1312
|
"""
|
|
1286
|
-
if len(self.
|
|
1313
|
+
if len(self._sensor_config.pumps) == 0:
|
|
1287
1314
|
warnings.warn("Network does not contain any pumps", UserWarning)
|
|
1288
1315
|
|
|
1289
|
-
self.set_pump_state_sensors(self.
|
|
1316
|
+
self.set_pump_state_sensors(self._sensor_config.pumps)
|
|
1290
1317
|
|
|
1291
1318
|
def set_pump_efficiency_sensors(self, sensor_locations: list[str]) -> None:
|
|
1292
1319
|
"""
|
|
@@ -1304,10 +1331,10 @@ class ScenarioSimulator():
|
|
|
1304
1331
|
"""
|
|
1305
1332
|
Places a pump efficiency sensor at every pump in the network.
|
|
1306
1333
|
"""
|
|
1307
|
-
if len(self.
|
|
1334
|
+
if len(self._sensor_config.pumps) == 0:
|
|
1308
1335
|
warnings.warn("Network does not contain any pumps", UserWarning)
|
|
1309
1336
|
|
|
1310
|
-
self.set_pump_efficiency_sensors(self.
|
|
1337
|
+
self.set_pump_efficiency_sensors(self._sensor_config.pumps)
|
|
1311
1338
|
|
|
1312
1339
|
def set_pump_energyconsumption_sensors(self, sensor_locations: list[str]) -> None:
|
|
1313
1340
|
"""
|
|
@@ -1325,10 +1352,10 @@ class ScenarioSimulator():
|
|
|
1325
1352
|
"""
|
|
1326
1353
|
Places a pump energy consumption sensor at every pump in the network.
|
|
1327
1354
|
"""
|
|
1328
|
-
if len(self.
|
|
1355
|
+
if len(self._sensor_config.pumps) == 0:
|
|
1329
1356
|
warnings.warn("Network does not contain any pumps", UserWarning)
|
|
1330
1357
|
|
|
1331
|
-
self.set_pump_energyconsumption_sensors(self.
|
|
1358
|
+
self.set_pump_energyconsumption_sensors(self._sensor_config.pumps)
|
|
1332
1359
|
|
|
1333
1360
|
def set_pump_sensors(self, sensor_locations: list[str]) -> None:
|
|
1334
1361
|
"""
|
|
@@ -1349,10 +1376,10 @@ class ScenarioSimulator():
|
|
|
1349
1376
|
Palces pump sensors at every pump in the network -- i.e. retrieving the state, efficiency,
|
|
1350
1377
|
and energy consumption of all pumps in the network.
|
|
1351
1378
|
"""
|
|
1352
|
-
if len(self.
|
|
1379
|
+
if len(self._sensor_config.pumps) == 0:
|
|
1353
1380
|
warnings.warn("Network does not contain any pumps", UserWarning)
|
|
1354
1381
|
|
|
1355
|
-
self.set_pump_sensors(self.
|
|
1382
|
+
self.set_pump_sensors(self._sensor_config.pumps)
|
|
1356
1383
|
|
|
1357
1384
|
def set_tank_sensors(self, sensor_locations: list[str]) -> None:
|
|
1358
1385
|
"""
|
|
@@ -1369,10 +1396,10 @@ class ScenarioSimulator():
|
|
|
1369
1396
|
"""
|
|
1370
1397
|
Places a water tank volume sensor at every tank in the network.
|
|
1371
1398
|
"""
|
|
1372
|
-
if len(self.
|
|
1399
|
+
if len(self._sensor_config.tanks) == 0:
|
|
1373
1400
|
warnings.warn("Network does not contain any tanks", UserWarning)
|
|
1374
1401
|
|
|
1375
|
-
self.set_tank_sensors(self.
|
|
1402
|
+
self.set_tank_sensors(self._sensor_config.tanks)
|
|
1376
1403
|
|
|
1377
1404
|
def set_bulk_species_node_sensors(self, sensor_info: dict) -> None:
|
|
1378
1405
|
"""
|
|
@@ -1386,13 +1413,30 @@ class ScenarioSimulator():
|
|
|
1386
1413
|
"""
|
|
1387
1414
|
self.set_sensors(SENSOR_TYPE_NODE_BULK_SPECIES, sensor_info)
|
|
1388
1415
|
|
|
1389
|
-
def place_bulk_species_node_sensors_everywhere(self) -> None:
|
|
1416
|
+
def place_bulk_species_node_sensors_everywhere(self, bulk_species: list[str] = None) -> None:
|
|
1390
1417
|
"""
|
|
1391
1418
|
Places bulk species concentration sensors at every node in the network for
|
|
1392
1419
|
every bulk species.
|
|
1420
|
+
|
|
1421
|
+
Parameters
|
|
1422
|
+
----------
|
|
1423
|
+
bulk_species : `list[str]`, optional
|
|
1424
|
+
List of bulk species IDs which we want to monitor at every node.
|
|
1425
|
+
If None, every bulk species will be monitored at every node.
|
|
1426
|
+
|
|
1427
|
+
The default is None.
|
|
1393
1428
|
"""
|
|
1394
|
-
|
|
1395
|
-
|
|
1429
|
+
if bulk_species is None:
|
|
1430
|
+
self.set_bulk_species_node_sensors({species_id: self._sensor_config.nodes
|
|
1431
|
+
for species_id in
|
|
1432
|
+
self._sensor_config.bulk_species})
|
|
1433
|
+
else:
|
|
1434
|
+
if any(species_id not in self._sensor_config.bulk_species
|
|
1435
|
+
for species_id in bulk_species):
|
|
1436
|
+
raise ValueError("Invalid bulk species ID in 'bulk_species'")
|
|
1437
|
+
|
|
1438
|
+
self.set_bulk_species_node_sensors({species_id: self._sensor_config.nodes
|
|
1439
|
+
for species_id in bulk_species})
|
|
1396
1440
|
|
|
1397
1441
|
def set_bulk_species_link_sensors(self, sensor_info: dict) -> None:
|
|
1398
1442
|
"""
|
|
@@ -1406,13 +1450,30 @@ class ScenarioSimulator():
|
|
|
1406
1450
|
"""
|
|
1407
1451
|
self.set_sensors(SENSOR_TYPE_LINK_BULK_SPECIES, sensor_info)
|
|
1408
1452
|
|
|
1409
|
-
def place_bulk_species_link_sensors_everywhere(self) -> None:
|
|
1453
|
+
def place_bulk_species_link_sensors_everywhere(self, bulk_species: list[str] = None) -> None:
|
|
1410
1454
|
"""
|
|
1411
1455
|
Places bulk species concentration sensors at every link/pipe in the network
|
|
1412
1456
|
for every bulk species.
|
|
1457
|
+
|
|
1458
|
+
Parameters
|
|
1459
|
+
----------
|
|
1460
|
+
bulk_species : `list[str]`, optional
|
|
1461
|
+
List of bulk species IDs which we want to monitor at every link/pipe.
|
|
1462
|
+
If None, every bulk species will be monitored at every link/pipe.
|
|
1463
|
+
|
|
1464
|
+
The default is None.
|
|
1413
1465
|
"""
|
|
1414
|
-
|
|
1415
|
-
|
|
1466
|
+
if bulk_species is None:
|
|
1467
|
+
self.set_bulk_species_link_sensors({species_id: self._sensor_config.links
|
|
1468
|
+
for species_id in
|
|
1469
|
+
self._sensor_config.bulk_species})
|
|
1470
|
+
else:
|
|
1471
|
+
if any(species_id not in self._sensor_config.bulk_species
|
|
1472
|
+
for species_id in bulk_species):
|
|
1473
|
+
raise ValueError("Invalid bulk species ID in 'bulk_species'")
|
|
1474
|
+
|
|
1475
|
+
self.set_bulk_species_link_sensors({species_id: self._sensor_config.links
|
|
1476
|
+
for species_id in bulk_species})
|
|
1416
1477
|
|
|
1417
1478
|
def set_surface_species_sensors(self, sensor_info: dict) -> None:
|
|
1418
1479
|
"""
|
|
@@ -1426,33 +1487,50 @@ class ScenarioSimulator():
|
|
|
1426
1487
|
"""
|
|
1427
1488
|
self.set_sensors(SENSOR_TYPE_SURFACE_SPECIES, sensor_info)
|
|
1428
1489
|
|
|
1429
|
-
def place_surface_species_sensors_everywhere(self
|
|
1490
|
+
def place_surface_species_sensors_everywhere(self, surface_species_id: list[str] = None
|
|
1491
|
+
) -> None:
|
|
1430
1492
|
"""
|
|
1431
1493
|
Places surface species concentration sensors at every link/pipe in the network
|
|
1432
1494
|
for every surface species.
|
|
1495
|
+
|
|
1496
|
+
Parameters
|
|
1497
|
+
----------
|
|
1498
|
+
surface_species_id : `list[str]`, optional
|
|
1499
|
+
List of surface species IDs which we want to monitor at every link/pipe.
|
|
1500
|
+
If None, every surface species will be monitored at every link/pipe.
|
|
1501
|
+
|
|
1502
|
+
The default is None.
|
|
1433
1503
|
"""
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1504
|
+
if surface_species_id is None:
|
|
1505
|
+
self.set_bulk_species_node_sensors({species_id: self._sensor_config.links
|
|
1506
|
+
for species_id in
|
|
1507
|
+
self._sensor_config.surface_species})
|
|
1508
|
+
else:
|
|
1509
|
+
if any(species_id not in self._sensor_config.surface_species
|
|
1510
|
+
for species_id in surface_species_id):
|
|
1511
|
+
raise ValueError("Invalid surface species ID in 'surface_species_id'")
|
|
1512
|
+
|
|
1513
|
+
self.set_bulk_species_node_sensors({species_id: self._sensor_config.links
|
|
1514
|
+
for species_id in surface_species_id})
|
|
1437
1515
|
|
|
1438
1516
|
def place_sensors_everywhere(self) -> None:
|
|
1439
1517
|
"""
|
|
1440
1518
|
Places sensors everywhere -- i.e. every possible quantity is monitored
|
|
1441
1519
|
at every position in the network.
|
|
1442
1520
|
"""
|
|
1443
|
-
self.
|
|
1521
|
+
self._sensor_config.place_sensors_everywhere()
|
|
1444
1522
|
|
|
1445
|
-
def
|
|
1446
|
-
self.
|
|
1523
|
+
def _prepare_simulation(self) -> None:
|
|
1524
|
+
self._adapt_to_network_changes()
|
|
1447
1525
|
|
|
1448
|
-
if self.
|
|
1449
|
-
self.
|
|
1526
|
+
if self._model_uncertainty is not None:
|
|
1527
|
+
self._model_uncertainty.apply(self.epanet_api)
|
|
1450
1528
|
|
|
1451
|
-
for event in self.
|
|
1529
|
+
for event in self._system_events:
|
|
1452
1530
|
event.reset()
|
|
1453
1531
|
|
|
1454
|
-
if self.
|
|
1455
|
-
for c in self.
|
|
1532
|
+
if self._controls is not None:
|
|
1533
|
+
for c in self._controls:
|
|
1456
1534
|
c.init(self.epanet_api)
|
|
1457
1535
|
|
|
1458
1536
|
def run_advanced_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
|
|
@@ -1509,9 +1587,9 @@ class ScenarioSimulator():
|
|
|
1509
1587
|
result[data_type] = None
|
|
1510
1588
|
|
|
1511
1589
|
return ScadaData(**result,
|
|
1512
|
-
sensor_config=self.
|
|
1513
|
-
sensor_reading_events=self.
|
|
1514
|
-
sensor_noise=self.
|
|
1590
|
+
sensor_config=self._sensor_config,
|
|
1591
|
+
sensor_reading_events=self._sensor_reading_events,
|
|
1592
|
+
sensor_noise=self._sensor_noise,
|
|
1515
1593
|
frozen_sensor_config=frozen_sensor_config)
|
|
1516
1594
|
|
|
1517
1595
|
def run_advanced_quality_simulation_as_generator(self, hyd_file_in: str, verbose: bool = False,
|
|
@@ -1568,9 +1646,9 @@ class ScenarioSimulator():
|
|
|
1568
1646
|
|
|
1569
1647
|
self.__running_simulation = True
|
|
1570
1648
|
|
|
1571
|
-
bulk_species_idx = self.epanet_api.getMSXSpeciesIndex(self.
|
|
1649
|
+
bulk_species_idx = self.epanet_api.getMSXSpeciesIndex(self._sensor_config.bulk_species)
|
|
1572
1650
|
surface_species_idx = self.epanet_api.getMSXSpeciesIndex(
|
|
1573
|
-
self.
|
|
1651
|
+
self._sensor_config.surface_species)
|
|
1574
1652
|
|
|
1575
1653
|
if verbose is True:
|
|
1576
1654
|
print("Running EPANET-MSX ...")
|
|
@@ -1649,13 +1727,13 @@ class ScenarioSimulator():
|
|
|
1649
1727
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
1650
1728
|
"sensor_readings_time": np.array([0])}
|
|
1651
1729
|
else:
|
|
1652
|
-
data = ScadaData(sensor_config=self.
|
|
1730
|
+
data = ScadaData(sensor_config=self._sensor_config,
|
|
1653
1731
|
bulk_species_node_concentration_raw=bulk_species_node_concentrations,
|
|
1654
1732
|
bulk_species_link_concentration_raw=bulk_species_link_concentrations,
|
|
1655
1733
|
surface_species_concentration_raw=surface_species_concentrations,
|
|
1656
1734
|
sensor_readings_time=np.array([0]),
|
|
1657
|
-
sensor_reading_events=self.
|
|
1658
|
-
sensor_noise=self.
|
|
1735
|
+
sensor_reading_events=self._sensor_reading_events,
|
|
1736
|
+
sensor_noise=self._sensor_noise,
|
|
1659
1737
|
frozen_sensor_config=frozen_sensor_config)
|
|
1660
1738
|
|
|
1661
1739
|
if support_abort is True: # Can the simulation be aborted? If so, handle it.
|
|
@@ -1693,7 +1771,7 @@ class ScenarioSimulator():
|
|
|
1693
1771
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
1694
1772
|
"sensor_readings_time": np.array([total_time])}
|
|
1695
1773
|
else:
|
|
1696
|
-
data = ScadaData(sensor_config=self.
|
|
1774
|
+
data = ScadaData(sensor_config=self._sensor_config,
|
|
1697
1775
|
bulk_species_node_concentration_raw=
|
|
1698
1776
|
bulk_species_node_concentrations,
|
|
1699
1777
|
bulk_species_link_concentration_raw=
|
|
@@ -1701,8 +1779,8 @@ class ScenarioSimulator():
|
|
|
1701
1779
|
surface_species_concentration_raw=
|
|
1702
1780
|
surface_species_concentrations,
|
|
1703
1781
|
sensor_readings_time=np.array([total_time]),
|
|
1704
|
-
sensor_reading_events=self.
|
|
1705
|
-
sensor_noise=self.
|
|
1782
|
+
sensor_reading_events=self._sensor_reading_events,
|
|
1783
|
+
sensor_noise=self._sensor_noise,
|
|
1706
1784
|
frozen_sensor_config=frozen_sensor_config)
|
|
1707
1785
|
|
|
1708
1786
|
if support_abort is True: # Can the simulation be aborted? If so, handle it.
|
|
@@ -1763,9 +1841,9 @@ class ScenarioSimulator():
|
|
|
1763
1841
|
result[data_type] = np.concatenate(result[data_type], axis=0)
|
|
1764
1842
|
|
|
1765
1843
|
return ScadaData(**result,
|
|
1766
|
-
sensor_config=self.
|
|
1767
|
-
sensor_reading_events=self.
|
|
1768
|
-
sensor_noise=self.
|
|
1844
|
+
sensor_config=self._sensor_config,
|
|
1845
|
+
sensor_reading_events=self._sensor_reading_events,
|
|
1846
|
+
sensor_noise=self._sensor_noise,
|
|
1769
1847
|
frozen_sensor_config=frozen_sensor_config)
|
|
1770
1848
|
|
|
1771
1849
|
def run_basic_quality_simulation_as_generator(self, hyd_file_in: str, verbose: bool = False,
|
|
@@ -1851,12 +1929,12 @@ class ScenarioSimulator():
|
|
|
1851
1929
|
"link_quality_data_raw": quality_link_data,
|
|
1852
1930
|
"sensor_readings_time": np.array([total_time])}
|
|
1853
1931
|
else:
|
|
1854
|
-
data = ScadaData(sensor_config=self.
|
|
1932
|
+
data = ScadaData(sensor_config=self._sensor_config,
|
|
1855
1933
|
node_quality_data_raw=quality_node_data,
|
|
1856
1934
|
link_quality_data_raw=quality_link_data,
|
|
1857
1935
|
sensor_readings_time=np.array([total_time]),
|
|
1858
|
-
sensor_reading_events=self.
|
|
1859
|
-
sensor_noise=self.
|
|
1936
|
+
sensor_reading_events=self._sensor_reading_events,
|
|
1937
|
+
sensor_noise=self._sensor_noise,
|
|
1860
1938
|
frozen_sensor_config=frozen_sensor_config)
|
|
1861
1939
|
|
|
1862
1940
|
if support_abort is True: # Can the simulation be aborted? If so, handle it.
|
|
@@ -1905,7 +1983,7 @@ class ScenarioSimulator():
|
|
|
1905
1983
|
if self.__running_simulation is True:
|
|
1906
1984
|
raise RuntimeError("A simulation is already running.")
|
|
1907
1985
|
|
|
1908
|
-
self.
|
|
1986
|
+
self._adapt_to_network_changes()
|
|
1909
1987
|
|
|
1910
1988
|
result = None
|
|
1911
1989
|
|
|
@@ -1927,9 +2005,9 @@ class ScenarioSimulator():
|
|
|
1927
2005
|
result[data_type] = np.concatenate(result[data_type], axis=0)
|
|
1928
2006
|
|
|
1929
2007
|
result = ScadaData(**result,
|
|
1930
|
-
sensor_config=self.
|
|
1931
|
-
sensor_reading_events=self.
|
|
1932
|
-
sensor_noise=self.
|
|
2008
|
+
sensor_config=self._sensor_config,
|
|
2009
|
+
sensor_reading_events=self._sensor_reading_events,
|
|
2010
|
+
sensor_noise=self._sensor_noise,
|
|
1933
2011
|
frozen_sensor_config=frozen_sensor_config)
|
|
1934
2012
|
|
|
1935
2013
|
return result
|
|
@@ -1984,9 +2062,9 @@ class ScenarioSimulator():
|
|
|
1984
2062
|
if self.__running_simulation is True:
|
|
1985
2063
|
raise RuntimeError("A simulation is already running.")
|
|
1986
2064
|
|
|
1987
|
-
self.
|
|
2065
|
+
self._adapt_to_network_changes()
|
|
1988
2066
|
|
|
1989
|
-
self.
|
|
2067
|
+
self._prepare_simulation()
|
|
1990
2068
|
|
|
1991
2069
|
self.__running_simulation = True
|
|
1992
2070
|
|
|
@@ -2024,7 +2102,7 @@ class ScenarioSimulator():
|
|
|
2024
2102
|
|
|
2025
2103
|
# Apply system events in a regular time interval only!
|
|
2026
2104
|
if (total_time + tstep) % requested_time_step == 0:
|
|
2027
|
-
for event in self.
|
|
2105
|
+
for event in self._system_events:
|
|
2028
2106
|
event.step(total_time + tstep)
|
|
2029
2107
|
|
|
2030
2108
|
# Compute current time step
|
|
@@ -2048,7 +2126,7 @@ class ScenarioSimulator():
|
|
|
2048
2126
|
link_valve_idx = self.epanet_api.getLinkValveIndex()
|
|
2049
2127
|
valves_state_data = self.epanet_api.getLinkStatus(link_valve_idx).reshape(1, -1)
|
|
2050
2128
|
|
|
2051
|
-
scada_data = ScadaData(sensor_config=self.
|
|
2129
|
+
scada_data = ScadaData(sensor_config=self._sensor_config,
|
|
2052
2130
|
pressure_data_raw=pressure_data,
|
|
2053
2131
|
flow_data_raw=flow_data,
|
|
2054
2132
|
demand_data_raw=demand_data,
|
|
@@ -2060,8 +2138,8 @@ class ScenarioSimulator():
|
|
|
2060
2138
|
pumps_energy_usage_data_raw=pumps_energy_usage_data,
|
|
2061
2139
|
pumps_efficiency_data_raw=pumps_efficiency_data,
|
|
2062
2140
|
sensor_readings_time=np.array([total_time]),
|
|
2063
|
-
sensor_reading_events=self.
|
|
2064
|
-
sensor_noise=self.
|
|
2141
|
+
sensor_reading_events=self._sensor_reading_events,
|
|
2142
|
+
sensor_noise=self._sensor_noise,
|
|
2065
2143
|
frozen_sensor_config=frozen_sensor_config)
|
|
2066
2144
|
|
|
2067
2145
|
# Yield results in a regular time interval only!
|
|
@@ -2089,7 +2167,7 @@ class ScenarioSimulator():
|
|
|
2089
2167
|
yield data
|
|
2090
2168
|
|
|
2091
2169
|
# Apply control modules
|
|
2092
|
-
for control in self.
|
|
2170
|
+
for control in self._controls:
|
|
2093
2171
|
control.step(scada_data)
|
|
2094
2172
|
|
|
2095
2173
|
# Next
|
|
@@ -2107,6 +2185,16 @@ class ScenarioSimulator():
|
|
|
2107
2185
|
self.__running_simulation = False
|
|
2108
2186
|
raise ex
|
|
2109
2187
|
|
|
2188
|
+
def run_simulation_as_generator(self, hyd_export: str = None, verbose: bool = False,
|
|
2189
|
+
support_abort: bool = False,
|
|
2190
|
+
return_as_dict: bool = False,
|
|
2191
|
+
frozen_sensor_config: bool = False,
|
|
2192
|
+
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
2193
|
+
warnings.warn("'run_simulation_as_generator' is deprecated and will be removed in " +
|
|
2194
|
+
"future releases -- use 'run_hydraulic_simulation_as_generator' instead")
|
|
2195
|
+
return self.run_hydraulic_simulation_as_generator(hyd_export, verbose, support_abort,
|
|
2196
|
+
return_as_dict, frozen_sensor_config)
|
|
2197
|
+
|
|
2110
2198
|
def run_simulation(self, hyd_export: str = None, verbose: bool = False,
|
|
2111
2199
|
frozen_sensor_config: bool = False) -> ScadaData:
|
|
2112
2200
|
"""
|
|
@@ -2139,7 +2227,7 @@ class ScenarioSimulator():
|
|
|
2139
2227
|
if self.__running_simulation is True:
|
|
2140
2228
|
raise RuntimeError("A simulation is already running.")
|
|
2141
2229
|
|
|
2142
|
-
self.
|
|
2230
|
+
self._adapt_to_network_changes()
|
|
2143
2231
|
|
|
2144
2232
|
result = None
|
|
2145
2233
|
|
|
@@ -2182,14 +2270,14 @@ class ScenarioSimulator():
|
|
|
2182
2270
|
if self.__running_simulation is True:
|
|
2183
2271
|
raise RuntimeError("Can not set uncertainties when simulation is running.")
|
|
2184
2272
|
|
|
2185
|
-
self.
|
|
2273
|
+
self._adapt_to_network_changes()
|
|
2186
2274
|
|
|
2187
2275
|
if not isinstance(model_uncertainty, ModelUncertainty):
|
|
2188
2276
|
raise TypeError("'model_uncertainty' must be an instance of " +
|
|
2189
2277
|
"'epyt_flow.uncertainty.ModelUncertainty' but not of " +
|
|
2190
2278
|
f"'{type(model_uncertainty)}'")
|
|
2191
2279
|
|
|
2192
|
-
self.
|
|
2280
|
+
self._model_uncertainty = model_uncertainty
|
|
2193
2281
|
|
|
2194
2282
|
def set_sensor_noise(self, sensor_noise: SensorNoise) -> None:
|
|
2195
2283
|
"""
|
|
@@ -2203,14 +2291,14 @@ class ScenarioSimulator():
|
|
|
2203
2291
|
if self.__running_simulation is True:
|
|
2204
2292
|
raise RuntimeError("Can not set sensor noise when simulation is running.")
|
|
2205
2293
|
|
|
2206
|
-
self.
|
|
2294
|
+
self._adapt_to_network_changes()
|
|
2207
2295
|
|
|
2208
2296
|
if not isinstance(sensor_noise, SensorNoise):
|
|
2209
2297
|
raise TypeError("'sensor_noise' must be an instance of " +
|
|
2210
2298
|
"'epyt_flow.uncertainties.SensorNoise' but not of " +
|
|
2211
2299
|
f"'{type(sensor_noise)}'")
|
|
2212
2300
|
|
|
2213
|
-
self.
|
|
2301
|
+
self._sensor_noise = sensor_noise
|
|
2214
2302
|
|
|
2215
2303
|
def set_general_parameters(self, demand_model: dict = None, simulation_duration: int = None,
|
|
2216
2304
|
hydraulic_time_step: int = None, quality_time_step: int = None,
|
|
@@ -2288,7 +2376,7 @@ class ScenarioSimulator():
|
|
|
2288
2376
|
if self.__running_simulation is True:
|
|
2289
2377
|
raise RuntimeError("Can not change general parameters when simulation is running.")
|
|
2290
2378
|
|
|
2291
|
-
self.
|
|
2379
|
+
self._adapt_to_network_changes()
|
|
2292
2380
|
|
|
2293
2381
|
if flow_units_id is not None:
|
|
2294
2382
|
if flow_units_id == ToolkitConstants.EN_CFS:
|
|
@@ -2329,7 +2417,7 @@ class ScenarioSimulator():
|
|
|
2329
2417
|
if not isinstance(hydraulic_time_step, int) or hydraulic_time_step <= 0:
|
|
2330
2418
|
raise ValueError("'hydraulic_time_step' must be a positive integer specifying " +
|
|
2331
2419
|
"the time steps of the hydraulic simulation")
|
|
2332
|
-
if len(self.
|
|
2420
|
+
if len(self._system_events) != 0:
|
|
2333
2421
|
raise RuntimeError("Hydraulic time step cannot be changed after system events " +
|
|
2334
2422
|
"such as leakages have been added to the scenario")
|
|
2335
2423
|
self.epanet_api.setTimeHydraulicStep(hydraulic_time_step)
|
|
@@ -2391,10 +2479,10 @@ class ScenarioSimulator():
|
|
|
2391
2479
|
events_times.append(cur_time)
|
|
2392
2480
|
cur_time += hyd_time_step
|
|
2393
2481
|
|
|
2394
|
-
for event in self.
|
|
2482
|
+
for event in self._sensor_reading_events:
|
|
2395
2483
|
__process_event(event)
|
|
2396
2484
|
|
|
2397
|
-
for event in self.
|
|
2485
|
+
for event in self._system_events:
|
|
2398
2486
|
__process_event(event)
|
|
2399
2487
|
|
|
2400
2488
|
return list(set(events_times))
|
|
@@ -2413,7 +2501,7 @@ class ScenarioSimulator():
|
|
|
2413
2501
|
if self.__running_simulation is True:
|
|
2414
2502
|
raise RuntimeError("Can not change general parameters when simulation is running.")
|
|
2415
2503
|
|
|
2416
|
-
self.
|
|
2504
|
+
self._adapt_to_network_changes()
|
|
2417
2505
|
|
|
2418
2506
|
self.__warn_if_quality_set()
|
|
2419
2507
|
self.set_general_parameters(quality_model={"type": "AGE"})
|
|
@@ -2444,7 +2532,7 @@ class ScenarioSimulator():
|
|
|
2444
2532
|
if self.__running_simulation is True:
|
|
2445
2533
|
raise RuntimeError("Can not change general parameters when simulation is running.")
|
|
2446
2534
|
|
|
2447
|
-
self.
|
|
2535
|
+
self._adapt_to_network_changes()
|
|
2448
2536
|
|
|
2449
2537
|
self.__warn_if_quality_set()
|
|
2450
2538
|
self.set_general_parameters(quality_model={"type": "CHEM", "chemical_name": chemical_name,
|
|
@@ -2491,12 +2579,12 @@ class ScenarioSimulator():
|
|
|
2491
2579
|
if self.__running_simulation is True:
|
|
2492
2580
|
raise RuntimeError("Can not change general parameters when simulation is running.")
|
|
2493
2581
|
|
|
2494
|
-
self.
|
|
2582
|
+
self._adapt_to_network_changes()
|
|
2495
2583
|
|
|
2496
2584
|
if self.epanet_api.getQualityInfo().QualityCode != ToolkitConstants.EN_CHEM:
|
|
2497
2585
|
raise RuntimeError("Chemical analysis is not enabled -- " +
|
|
2498
2586
|
"call 'enable_chemical_analysis()' before calling this function.")
|
|
2499
|
-
if node_id not in self.
|
|
2587
|
+
if node_id not in self._sensor_config.nodes:
|
|
2500
2588
|
raise ValueError(f"Unknown node '{node_id}'")
|
|
2501
2589
|
if not isinstance(pattern, np.ndarray):
|
|
2502
2590
|
raise TypeError("'pattern' must be an instance of 'numpy.ndarray' " +
|
|
@@ -2530,9 +2618,9 @@ class ScenarioSimulator():
|
|
|
2530
2618
|
if self.__running_simulation is True:
|
|
2531
2619
|
raise RuntimeError("Can not change general parameters when simulation is running.")
|
|
2532
2620
|
|
|
2533
|
-
self.
|
|
2621
|
+
self._adapt_to_network_changes()
|
|
2534
2622
|
|
|
2535
|
-
if trace_node_id not in self.
|
|
2623
|
+
if trace_node_id not in self._sensor_config.nodes:
|
|
2536
2624
|
raise ValueError(f"Invalid node ID '{trace_node_id}'")
|
|
2537
2625
|
|
|
2538
2626
|
self.__warn_if_quality_set()
|