1"""
2Module provides a class for specifying scenario configurations.
3"""
4from typing import Any
5from copy import deepcopy
6import os
7import json
8from pathlib import Path
9import numpy as np
10
11from ..uncertainty import AbsoluteGaussianUncertainty, RelativeGaussianUncertainty, \
12 AbsoluteUniformUncertainty, RelativeUniformUncertainty, ModelUncertainty, \
13 SensorNoise, Uncertainty
14from .sensor_config import SensorConfig
15from .scada import CustomControlModule, SimpleControlModule, ComplexControlModule
16from .events import SystemEvent, SensorReadingEvent
17from .events.sensor_faults import SensorFaultConstant, SensorFaultDrift, SensorFaultGaussian, \
18 SensorFaultPercentage, SensorFaultStuckZero
19from .events.leakages import AbruptLeakage, IncipientLeakage
20from ..serialization import serializable, Serializable, SCENARIO_CONFIG_ID
21from ..topology import NetworkTopology
22
23
[docs]
24@serializable(SCENARIO_CONFIG_ID, ".epytflow_scenario_config")
25class ScenarioConfig(Serializable):
26 """
27 Configuration of a scenario.
28
29 Parameters
30 ----------
31 scenario_config : :class:`~epyt_flow.simulation.scenario_config.ScenarioConfig`, optional
32 Uses the given scenario configuration to create this instance --
33 other attributes passed to this constructor override the attributes in 'scenario_config'.
34
35 Note that if 'scenario_config' is None then 'f_inp_in' can not be None --
36 i.e. either 'scenario_config' or 'f_inp_in' must be given.
37
38 The default is None.
39 f_inp_in : `str`, optional
40 Path to the .inp file.
41
42 Note that if 'f_inp_in' is None then 'scenario_config' can not be None --
43 i.e. either 'scenario_config' or 'f_inp_in' must be given.
44
45 The default is None.
46 f_msx_in : `str`, optional
47 Path to the .msx file -- optional, only necessary if EPANET-MSX is used.
48
49 The default is None
50 network_topology : :class:`~epyt_flow.topology.NetworkTopology`, optional
51 Specification of the network topology -- necessary if .inp file does not exist
52 or is not shared.
53
54 The default is None.
55 general_params : `dict`, optional
56 General parameters such as the demand model, hydraulic time steps, etc.
57
58 The default is None
59 sensor_config : :class:`~epyt_flow.simulation.sensor_config.SensorConfig`, optional
60 Specification of all sensors.
61
62 The default is None
63 memory_consumption_estimate : float, optional
64 Estimated memory consumption of this scenario in MB -- i.e. the amount of memory that is
65 needed on the hard disk as well as in RAM.
66
67 The default is None.
68 sensor_noise : :class:`~epyt_flow.uncertainty.sensor_noise.SensorNoise`, optional
69 Speciation of sensor noise -- i.e. noise/uncertainty affecting the sensor readings.
70
71 The default is None
72 csutom_controls : list[:class:`~epyt_flow.simulation.scada.custom_control.CustomControlModule`], optional
73 List of custom control modules that are active during the simulation.
74
75 The default is an empty list.
76 simple_controls : list[:class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`], optional
77 List of EPANET control rules that are active during the simulation.
78
79 The default is an empty list.
80 complex_controls : list[:class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`], optional
81 List of complex (i.e. IF-THEN-ELSE) EPANET control rules that are active during the simulation.
82
83 The default is an empty list.
84 model_uncertainty : :class:`~epyt_flow.uncertainty.model_uncertainty.ModelUncertainty`, optional
85 Specification of model uncertainty.
86 system_events : list[:class:`~epyt_flow.simulation.events.system_event.SystemEvent`], optional
87 List of system events -- i.e. events that directly affect the simulation (e.g. leakages).
88
89 The default is an empty list.
90 sensor_reading_events : list[:class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`], optional
91 List of sensor reading events -- i.e. events that affect the readings of sensors.
92
93 The default is an empty list.
94 """
95
96 def __init__(self, scenario_config: Any = None, f_inp_in: str = None, f_msx_in: str = None,
97 network_topology: NetworkTopology = None,
98 general_params: dict = None, sensor_config: SensorConfig = None,
99 memory_consumption_estimate: float = None,
100 custom_controls: list[CustomControlModule] = [],
101 simple_controls: list[SimpleControlModule] = [],
102 complex_controls: list[ComplexControlModule] = [],
103 sensor_noise: SensorNoise = None,
104 model_uncertainty: ModelUncertainty = None,
105 system_events: list[SystemEvent] = [],
106 sensor_reading_events: list[SensorReadingEvent] = [], **kwds):
107 if f_inp_in is None and scenario_config is None:
108 raise ValueError("Either 'f_inp_in' or 'scenario_config' must be given")
109 if scenario_config is not None:
110 if not isinstance(scenario_config, ScenarioConfig):
111 raise TypeError("'scenario_config' must be an instance of " +
112 "'epyt_flow.simulation.ScenarioConfig' but not of " +
113 f"'{type(scenario_config)}'")
114 if f_inp_in is not None:
115 if not isinstance(f_inp_in, str):
116 raise TypeError("'f_inp_in' must be an instance of 'str' " +
117 f"but no of '{type(f_inp_in)}'")
118 if f_msx_in is not None:
119 if not isinstance(f_msx_in, str):
120 raise TypeError("'f_msx_in' must be an instance of 'str' " +
121 f"but no of '{type(f_msx_in)}'")
122 if network_topology is not None:
123 if not isinstance(network_topology, NetworkTopology):
124 raise TypeError("'network_topology' msut be an instance of 'NetworkTopology' " +
125 f"but not of '{type(network_topology)}'")
126 if general_params is not None:
127 if not isinstance(general_params, dict):
128 raise TypeError("'general_params' must be an instance of 'dict' " +
129 f"but not of '{type(general_params)}'")
130 if sensor_config is not None:
131 if not isinstance(sensor_config, SensorConfig):
132 raise TypeError("'sensor_config' must be an instance of " +
133 "'epyt_flow.simulation.SensorConfig' but not of " +
134 f"'{type(sensor_config)}'")
135 if memory_consumption_estimate is not None:
136 if not isinstance(memory_consumption_estimate, float) or \
137 memory_consumption_estimate <= 0:
138 raise ValueError("'memory_consumption_estimate' must be a positive integer")
139 if len(custom_controls) != 0:
140 if any(not isinstance(c, CustomControlModule) for c in custom_controls):
141 raise TypeError("Each item in 'custom_controls' must be an instance of " +
142 "'epyt_flow.simulation.scada.CustomControlModule'")
143 if not isinstance(simple_controls, list):
144 raise TypeError("'simple_controls' must be an instance of " +
145 "'list[epyt_flow.simulation.scada.SimpleControlModule]' but no of " +
146 f"'{type(simple_controls)}'")
147 if len(simple_controls) != 0:
148 if any(not isinstance(c, SimpleControlModule) for c in simple_controls):
149 raise TypeError("Each item in 'simple_controls' must be an instance of " +
150 "'epyt_flow.simulation.scada.SimppleControlModule'")
151 if len(complex_controls) != 0:
152 if any(not isinstance(c, ComplexControlModule) for c in complex_controls):
153 raise TypeError("Each item in 'complex_controls' must be an instance of " +
154 "'epyt_flow.simulation.scada.ComplexControlModule'")
155 if sensor_noise is not None:
156 if not isinstance(sensor_noise, SensorNoise):
157 raise TypeError("'sensor_noise' must be an instance of " +
158 "'epyt_flow.uncertainty.SensorNoise' but not of " +
159 f"'{type(sensor_noise)}'")
160 if model_uncertainty is not None:
161 if not isinstance(model_uncertainty, ModelUncertainty):
162 raise TypeError("'model_uncertainty' must be an instance of " +
163 "'epyt_flow.uncertainty.ModelUncertainty' but not of " +
164 f"'{type(model_uncertainty)}'")
165 if not isinstance(system_events, list):
166 raise TypeError("'system_events' must be an instance of " +
167 "'list[epyt_flow.simulation.events.SystemEvent]' but no of " +
168 f"'{type(system_events)}'")
169 if len(system_events) != 0:
170 if any(not isinstance(c, SystemEvent) for c in system_events):
171 raise TypeError("Each item in 'system_events' must be an instance of " +
172 "'epyt_flow.simulation.events.SystemEvent'")
173 if not isinstance(sensor_reading_events, list):
174 raise TypeError("'sensor_reading_events' must be an instance of " +
175 "'list[epyt_flow.simulation.events.SensorReadingEvent]' but not of " +
176 f"'{type(sensor_reading_events)}'")
177 if len(sensor_reading_events) != 0:
178 if any(not isinstance(c, SensorReadingEvent) for c in sensor_reading_events):
179 raise TypeError("Each item in 'sensor_reading_events' must be an instance of " +
180 "'epyt_flow.simulation.events.SensorReadingEvent'")
181
182 if scenario_config is not None:
183 self.__f_inp_in = scenario_config.f_inp_in
184 self.__f_msx_in = scenario_config.f_msx_in if f_msx_in is None else f_msx_in
185
186 if network_topology is None:
187 self.__network_topology = scenario_config.network_topology
188 else:
189 self.__network_topology = network_topology
190
191 if general_params is None:
192 self.__general_params = scenario_config.general_params
193 else:
194 self.__general_params = general_params
195
196 if sensor_config is None:
197 self.__sensor_config = scenario_config.sensor_config
198 else:
199 self.__sensor_config = sensor_config
200
201 if memory_consumption_estimate is None:
202 self.__memory_consumption_estimate = scenario_config.memory_consumption_estimate
203 else:
204 self.__memory_consumption_estimate = memory_consumption_estimate
205
206 if len(custom_controls) == 0:
207 self.__custom_controls = scenario_config.custom_controls
208 else:
209 self.__custom_controls = custom_controls
210
211 if len(simple_controls) == 0:
212 self.__simple_controls = scenario_config.simple_controls
213 else:
214 self.__simple_controls = simple_controls
215
216 if len(complex_controls) == 0:
217 self.__complex_controls = scenario_config.complex_controls
218 else:
219 self.__complex_controls = complex_controls
220
221 if sensor_noise is None:
222 self.__sensor_noise = scenario_config.sensor_noise
223 else:
224 self.__sensor_noise = sensor_noise
225
226 if model_uncertainty is None:
227 self.__model_uncertainty = scenario_config.model_uncertainty
228 else:
229 self.__model_uncertainty = model_uncertainty
230
231 if len(system_events) == 0:
232 self.__system_events = scenario_config.system_events
233 else:
234 self.__system_events = system_events
235
236 if len(sensor_reading_events) == 0:
237 self.__sensor_reading_events = scenario_config.sensor_reading_events
238 else:
239 self.__sensor_reading_events = sensor_reading_events
240 else:
241 self.__f_inp_in = f_inp_in
242 self.__f_msx_in = f_msx_in
243 self.__network_topology = network_topology
244 self.__general_params = general_params
245 self.__sensor_config = sensor_config
246 self.__memory_consumption_estimate = memory_consumption_estimate
247 self.__custom_controls = custom_controls
248 self.__simple_controls = simple_controls
249 self.__complex_controls = complex_controls
250 self.__sensor_noise = sensor_noise
251 self.__system_events = system_events
252 self.__sensor_reading_events = sensor_reading_events
253
254 if model_uncertainty is not None:
255 self.__model_uncertainty = model_uncertainty
256 else:
257 self.__model_uncertainty = ModelUncertainty()
258
259 super().__init__(**kwds)
260
261 @property
262 def f_inp_in(self) -> str:
263 """
264 Gets the path to the .inp file.
265
266 Returns
267 -------
268 `str`
269 Path to the .inp file.
270 """
271 if Path(self.__f_inp_in).is_absolute():
272 return self.__f_inp_in
273 elif Path(self.__f_inp_in).name == self.__f_inp_in:
274 return os.path.join(self._parent_path, self.__f_inp_in)
275 else:
276 return self.__f_inp_in
277
278 @property
279 def f_msx_in(self) -> str:
280 """
281 Gets the path to the .msx file.
282
283 Returns
284 -------
285 `str`
286 Path to the .msx file.
287 """
288 if self.__f_msx_in is None:
289 return None
290 else:
291 if Path(self.__f_msx_in).is_absolute():
292 return self.__f_msx_in
293 elif Path(self.__f_msx_in).name == self.__f_msx_in:
294 return os.path.join(self._parent_path, self.__f_msx_in)
295 else:
296 return self.__f_msx_in
297
298 @property
299 def network_topology(self) -> NetworkTopology:
300 """
301 Returns the specification of the network topology.
302
303 Returns
304 -------
305 :class:`~epyt_flow.topology.NetworkTopology`
306 Network topology.
307 """
308 return deepcopy(self.__network_topology)
309
310 @property
311 def general_params(self) -> dict:
312 """
313 Gets general parameters such as hydraulic time step, etc.
314
315 Returns
316 -------
317 `dict`
318 All general parameters as dictionary -- the parameter name serves as a key.
319 """
320 return deepcopy(self.__general_params)
321
322 @property
323 def sensor_config(self) -> SensorConfig:
324 """
325 Gets the sensor configuration.
326
327 Returns
328 -------
329 :class:`~epyt_flow.simulation.sensor_config.SensorConfig`
330 Sensor configuration.
331 """
332 return deepcopy(self.__sensor_config)
333
334 @property
335 def memory_consumption_estimate(self) -> float:
336 """
337 Gets the estimated memory consumption of this scenario -- i.e. the amount of memory that is
338 needed on the hard disk as well as in RAM.
339
340 Returns
341 -------
342 `float`
343 Estimated memory consumption in MB.
344 """
345 return self.__memory_consumption_estimate
346
347 @property
348 def custom_controls(self) -> list[CustomControlModule]:
349 """
350 Returns the list of all custom control modules that are active during the simulation.
351
352 Returns
353 -------
354 list[:class:`~epyt_flow.simulation.scada.custom_control.CustomControlModule`]
355 List of all custom control modules that are active during the simulation.
356 """
357 return deepcopy(self.__custom_controls)
358
359 @property
360 def simple_controls(self) -> list[SimpleControlModule]:
361 """
362 Gets the list of all EPANET control rules that are active during the simulation.
363
364 Returns
365 -------
366 list[:class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`]
367 List of all EPANET control rules that are active during the simulation.
368 """
369 return deepcopy(self.__simple_controls)
370
371 @property
372 def complex_controls(self) -> list[ComplexControlModule]:
373 """
374 Gets the list of all complex (i.e. IF-THEN-ELSE) EPANET control rules
375 that are active during the simulation.
376
377 Returns
378 -------
379 list[:class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`]
380 List of all complex EPANET control rules that are active during the simulation.
381 """
382 return deepcopy(self.__complex_controls)
383
384 @property
385 def sensor_noise(self) -> SensorNoise:
386 """
387 Gets the sensor noise/uncertainty specification.
388
389 Returns
390 -------
391 :class:`~epyt_flow.uncertainty.sensor_noise.SensorNoise`
392 Sensor noise/uncertainty.
393 """
394 return deepcopy(self.__sensor_noise)
395
396 @property
397 def model_uncertainty(self) -> ModelUncertainty:
398 """
399 Gets the model uncertainty specification.
400
401 Returns
402 -------
403 :class:`~epyt_flow.uncertainty.model_uncertainty.ModelUncertainty`
404 Model uncertainty specification.
405 """
406 return deepcopy(self.__model_uncertainty)
407
408 @property
409 def system_events(self) -> list[SystemEvent]:
410 """
411 Gets all system events.
412
413 Returns
414 -------
415 list[:class:`~epyt_flow.simulation.events.system_event.SystemEvent`]
416 All system events.
417 """
418 return deepcopy(self.__system_events)
419
420 @property
421 def sensor_reading_events(self) -> list[SensorReadingEvent]:
422 """
423 Gets all sensor reading events.
424
425 Returns
426 -------
427 list[:class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`]
428 All sensor reading events.
429 """
430 return deepcopy(self.__sensor_reading_events)
431
[docs]
432 def get_attributes(self) -> dict:
433 my_attributes = {"f_inp_in": self.__f_inp_in, "f_msx_in": self.__f_msx_in,
434 "network_topology": self.__network_topology,
435 "general_params": self.__general_params,
436 "sensor_config": self.__sensor_config,
437 "memory_consumption_estimate": self.__memory_consumption_estimate,
438 "custom_controls": self.__custom_controls,
439 "simple_controls": self.__simple_controls,
440 "complex_controls": self.__complex_controls,
441 "sensor_noise": self.__sensor_noise,
442 "model_uncertainty": self.__model_uncertainty,
443 "system_events": self.__system_events,
444 "sensor_reading_events": self.__sensor_reading_events}
445
446 return super().get_attributes() | my_attributes
447
448 def __eq__(self, other) -> bool:
449 if not isinstance(other, ScenarioConfig):
450 raise TypeError("Can not compare 'ScenarioConfig' instance " +
451 f"with '{type(other)}' instance")
452
453 return self.__f_inp_in == other.f_inp_in and self.__f_msx_in == other.f_msx_in \
454 and self.__network_topology == other.network_topology \
455 and self.__general_params == other.general_params \
456 and self.__memory_consumption_estimate == other.memory_consumption_estimate \
457 and self.__sensor_config == other.sensor_config \
458 and np.all(self.__custom_controls == other.custom_controls) \
459 and np.all(self.__simple_controls == other.simple_controls) \
460 and np.all(self.__complex_controls == other.complex_controls) \
461 and self.__model_uncertainty == other.model_uncertainty \
462 and np.all(self.__system_events == other.system_events) \
463 and np.all(self.__sensor_reading_events == other.sensor_reading_events)
464
465 def __str__(self) -> str:
466 return f"f_inp_in: {self.f_inp_in} f_msx_in: {self.f_msx_in} " + \
467 f"general_params: {self.general_params} sensor_config: {self.sensor_config} " + \
468 f"memory_consumption_estimate: {self.memory_consumption_estimate} " + \
469 f"simple_controls: {self.simple_controls} " + \
470 f"complex_controls: {self.__complex_controls} " + \
471 f"custom_controls: {self.__custom_controls}" + \
472 f"sensor_noise: {self.sensor_noise} model_uncertainty: {self.model_uncertainty} " + \
473 f"system_events: {','.join(map(str, self.system_events))} " + \
474 f"sensor_reading_events: {','.join(map(str, self.sensor_reading_events))}"
475
[docs]
476 @staticmethod
477 def load_from_json_file(f_json_in: str) -> Any:
478 """
479 Loads a scenario configuration from a given JSON file.
480
481 Parameters
482 ----------
483 f_json_in : `str`
484 Path to JSON configuration file.
485
486 Returns
487 -------
488 :class:`~epyt_flow.simulation.scenario_config.ScenarioConfig`
489 Loaded scenario configuration.
490 """
491 with open(f_json_in, "r", encoding="utf-8") as f:
492 return ScenarioConfig.load_from_json(f.read())
493
[docs]
494 @staticmethod
495 def load_from_json(config_data: str) -> Any:
496 """
497 Loads a scenario configuration from a given JSON string.
498
499 Parameters
500 ----------
501 config_data : `str`
502 JSON data.
503
504 Returns
505 -------
506 :class:`~epyt_flow.simulation.scenario_config.ScenarioConfig`
507 Loaded scenario configuration.
508 """
509 data = json.loads(config_data)
510
511 # General parameters and sensor configuration
512 general_settings = data["general"]
513 f_inp_in = general_settings["file_inp"]
514 f_msx_in = general_settings["file_msx"] if "file_msx" in general_settings.keys() else None
515
516 general_params = {"simulation_duration": general_settings["simulation_duration"],
517 "hydraulic_time_step": general_settings["hydraulic_time_step"],
518 "quality_time_step": general_settings["quality_time_step"]}
519 if "reporting_time_step" in general_settings.keys():
520 general_params["reporting_time_step"] = general_settings["reporting_time_step"]
521 if "reporting_time_start" in general_settings.keys():
522 general_params["reporting_time_start"] = general_settings["reporting_time_start"]
523 if "demand_model" in general_settings.keys():
524 general_params["demand_model"] = general_settings["demand_model"]
525 if "quality_model" in general_settings.keys():
526 general_params["quality_model"] = general_settings["quality_model"]
527 if "flow_units_id" in general_settings.keys():
528 general_params["flow_units_id"] = general_settings["flow_units_id"]
529 if "pressure_units_id" in general_settings.keys():
530 general_params["pressure_units_id"] = general_settings["pressure_units_id"]
531
532 sensor_config = data["sensors"]
533
534 if "pressure_sensors" in sensor_config.keys():
535 pressure_sensors = sensor_config["pressure_sensors"]
536 else:
537 pressure_sensors = []
538
539 if "flow_sensors" in sensor_config.keys():
540 flow_sensors = sensor_config["flow_sensors"]
541 else:
542 flow_sensors = []
543
544 if "demand_sensors" in sensor_config.keys():
545 demand_sensors = sensor_config["demand_sensors"]
546 else:
547 demand_sensors = []
548
549 if "node_quality_sensors" in sensor_config.keys():
550 node_quality_sensors = sensor_config["node_quality_sensors"]
551 else:
552 node_quality_sensors = []
553
554 if "link_quality_sensors" in sensor_config.keys():
555 link_quality_sensors = sensor_config["link_quality_sensors"]
556 else:
557 link_quality_sensors = []
558
559 if "tank_volume_sensors" in sensor_config.keys():
560 tank_volume_sensors = sensor_config["tank_volume_sensors"]
561 else:
562 tank_volume_sensors = []
563
564 if "valve_state_sensors" in sensor_config.keys():
565 valve_state_sensors = sensor_config["valve_state_sensors"]
566 else:
567 valve_state_sensors = []
568
569 if "pump_state_sensors" in sensor_config.keys():
570 pump_state_sensors = sensor_config["pump_state_sensors"]
571 else:
572 pump_state_sensors = []
573
574 if "bulk_species_node_sensors" in sensor_config.keys():
575 bulk_species_node_sensors = sensor_config["bulk_species_node_sensors"]
576 else:
577 bulk_species_node_sensors = {}
578
579 if "bulk_species_link_sensors" in sensor_config.keys():
580 bulk_species_link_sensors = sensor_config["bulk_species_link_sensors"]
581 else:
582 bulk_species_link_sensors = {}
583
584 if "surface_species_sensors" in sensor_config.keys():
585 surface_species_sensors = sensor_config["surface_species_sensors"]
586 else:
587 surface_species_sensors = {}
588
589 # Uncertainties
590 if "uncertainties" in data.keys():
591 def parse_uncertantiy(uncertainty_desc: dict) -> Uncertainty:
592 uncertainty_type = uncertainty_desc["type"]
593 del uncertainty_desc["type"]
594
595 if uncertainty_type == "absolute_gaussian":
596 return AbsoluteGaussianUncertainty(**uncertainty_desc)
597 elif uncertainty_type == "relative_gaussian":
598 return RelativeGaussianUncertainty(**uncertainty_desc)
599 elif uncertainty_type == "absolute_uniform":
600 return AbsoluteUniformUncertainty(**uncertainty_desc)
601 elif uncertainty_type == "relative_uniform":
602 return RelativeUniformUncertainty(**uncertainty_desc)
603 else:
604 raise ValueError(f"Unknown uncertainty '{uncertainty_type}'")
605
606 uncertanties = data["uncertainties"]
607 if "pipe_length_uncertainty" in uncertanties.keys():
608 pipe_length_uncertainty = parse_uncertantiy(uncertanties["pipe_length_uncertainty"])
609 else:
610 pipe_length_uncertainty = None
611 if "pipe_roughness_uncertainty" in uncertanties.keys():
612 pipe_roughness_uncertainty = parse_uncertantiy(
613 uncertanties["pipe_roughness_uncertainty"])
614 else:
615 pipe_roughness_uncertainty = None
616 if "pipe_diameter_uncertainty" in uncertanties.keys():
617 pipe_diameter_uncertainty = parse_uncertantiy(
618 uncertanties["pipe_diameter_uncertainty"])
619 else:
620 pipe_diameter_uncertainty = None
621 if "demand_base_uncertainty" in uncertanties.keys():
622 demand_base_uncertainty = parse_uncertantiy(uncertanties["demand_base_uncertainty"])
623 else:
624 demand_base_uncertainty = None
625 if "demand_pattern_uncertainty" in uncertanties.keys():
626 demand_pattern_uncertainty = parse_uncertantiy(
627 uncertanties["demand_pattern_uncertainty"])
628 else:
629 demand_pattern_uncertainty = None
630 if "elevation_uncertainty" in uncertanties.keys():
631 elevation_uncertainty = parse_uncertantiy(uncertanties["elevation_uncertainty"])
632 else:
633 elevation_uncertainty = None
634 if "constants_uncertainty" in uncertanties.keys():
635 constants_uncertainty = parse_uncertantiy(uncertanties["constants_uncertainty"])
636 else:
637 constants_uncertainty = None
638 if "parameters_uncertainty" in uncertanties.keys():
639 parameters_uncertainty = parse_uncertantiy(uncertanties["parameters_uncertainty"])
640 else:
641 parameters_uncertainty = None
642
643 model_uncertainty = ModelUncertainty(pipe_length_uncertainty,
644 pipe_roughness_uncertainty,
645 pipe_diameter_uncertainty, demand_base_uncertainty,
646 demand_pattern_uncertainty, elevation_uncertainty,
647 constants_uncertainty, parameters_uncertainty)
648
649 if "sensor_noise" in uncertanties.keys():
650 sensor_noise = SensorNoise(parse_uncertantiy(uncertanties["sensor_noise"]))
651 else:
652 sensor_noise = None
653
654 # Events
655 leakages = []
656 if "leakages" in data.keys():
657 def parse_leak(leak_desc):
658 leak_type = leak_desc["type"]
659 del leak_desc["type"]
660
661 if leak_type == "abrupt":
662 return AbruptLeakage(**leak_desc)
663 elif leak_type == "incipient":
664 return IncipientLeakage(**leak_desc)
665 else:
666 raise ValueError(f"Unknown leakage type '{leak_type}'")
667
668 leakages = [parse_leak(leak) for leak in data["leakages"]]
669
670 sensor_faults = []
671 if "sensor_faults" in data.keys():
672 def parse_sensor_fault(sensor_fault_desc):
673 fault_type = sensor_fault_desc["type"]
674 del sensor_fault_desc["type"]
675
676 if fault_type == "constant":
677 return SensorFaultConstant(**sensor_fault_desc)
678 elif fault_type == "drift":
679 return SensorFaultDrift(**sensor_fault_desc)
680 elif fault_type == "gaussian":
681 return SensorFaultGaussian(**sensor_fault_desc)
682 elif fault_type == "percentage":
683 return SensorFaultPercentage(**sensor_fault_desc)
684 elif fault_type == "stuckatzero":
685 return SensorFaultStuckZero(**sensor_fault_desc)
686 else:
687 raise ValueError(f"Unknown sensor fault '{fault_type}'")
688
689 sensor_faults = [parse_sensor_fault(sensor_fault)
690 for sensor_fault in data["sensor_faults"]]
691
692 # Load .inp file to get a list of all nodes and links/pipes
693 sensor_config = None
694 from .scenario_simulator import ScenarioSimulator
695 with ScenarioSimulator(f_inp_in) as scenario:
696 sensor_config = SensorConfig.create_empty_sensor_config(scenario.sensor_config)
697 sensor_config.pressure_sensors = pressure_sensors
698 sensor_config.flow_sensors = flow_sensors
699 sensor_config.demand_sensors = demand_sensors
700 sensor_config.quality_node_sensors = node_quality_sensors
701 sensor_config.quality_link_sensors = link_quality_sensors
702 sensor_config.valve_state_sensors = valve_state_sensors
703 sensor_config.pump_state_sensors = pump_state_sensors
704 sensor_config.tank_volume_sensors = tank_volume_sensors
705 sensor_config.bulk_species_node_sensors = bulk_species_node_sensors
706 sensor_config.bulk_species_link_sensors = bulk_species_link_sensors
707 sensor_config.surface_species_sensors = surface_species_sensors
708
709 # Create final scenario configuration
710 return ScenarioConfig(f_inp_in=f_inp_in, f_msx_in=f_msx_in, general_params=general_params,
711 sensor_config=sensor_config, sensor_noise=sensor_noise,
712 model_uncertainty=model_uncertainty, system_events=leakages,
713 sensor_reading_events=sensor_faults)