epyt-flow 0.11.0__py3-none-any.whl → 0.13.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.
Files changed (32) hide show
  1. epyt_flow/EPANET/EPANET-MSX/Src/msxtoolkit.c +1 -1
  2. epyt_flow/VERSION +1 -1
  3. epyt_flow/data/benchmarks/gecco_water_quality.py +2 -2
  4. epyt_flow/data/benchmarks/leakdb.py +40 -5
  5. epyt_flow/data/benchmarks/water_usage.py +4 -3
  6. epyt_flow/gym/__init__.py +0 -3
  7. epyt_flow/gym/scenario_control_env.py +5 -12
  8. epyt_flow/rest_api/scenario/control_handlers.py +118 -0
  9. epyt_flow/rest_api/scenario/event_handlers.py +114 -1
  10. epyt_flow/rest_api/scenario/handlers.py +33 -0
  11. epyt_flow/rest_api/server.py +14 -2
  12. epyt_flow/simulation/backend/__init__.py +1 -0
  13. epyt_flow/simulation/backend/my_epyt.py +1056 -0
  14. epyt_flow/simulation/events/quality_events.py +3 -1
  15. epyt_flow/simulation/scada/scada_data.py +201 -12
  16. epyt_flow/simulation/scenario_simulator.py +179 -87
  17. epyt_flow/topology.py +8 -7
  18. epyt_flow/uncertainty/sensor_noise.py +2 -9
  19. epyt_flow/utils.py +30 -0
  20. epyt_flow/visualization/scenario_visualizer.py +159 -69
  21. epyt_flow/visualization/visualization_utils.py +144 -17
  22. {epyt_flow-0.11.0.dist-info → epyt_flow-0.13.0.dist-info}/METADATA +4 -4
  23. {epyt_flow-0.11.0.dist-info → epyt_flow-0.13.0.dist-info}/RECORD +26 -29
  24. {epyt_flow-0.11.0.dist-info → epyt_flow-0.13.0.dist-info}/WHEEL +1 -1
  25. epyt_flow/gym/control_gyms.py +0 -55
  26. epyt_flow/metrics.py +0 -471
  27. epyt_flow/models/__init__.py +0 -2
  28. epyt_flow/models/event_detector.py +0 -36
  29. epyt_flow/models/sensor_interpolation_detector.py +0 -123
  30. epyt_flow/simulation/scada/advanced_control.py +0 -138
  31. {epyt_flow-0.11.0.dist-info → epyt_flow-0.13.0.dist-info/licenses}/LICENSE +0 -0
  32. {epyt_flow-0.11.0.dist-info → epyt_flow-0.13.0.dist-info}/top_level.txt +0 -0
@@ -27,6 +27,8 @@ class SpeciesInjectionEvent(SystemEvent, JsonSerializable):
27
27
  profile : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
28
28
  Injection strength profile -- i.e. every entry corresponds to the strength of the injection
29
29
  at a point in time. Pattern will repeat if it is shorter than the total injection time.
30
+
31
+ Note that the pattern time step is equivalent to the EPANET pattern time step.
30
32
  source_type : `int`
31
33
  Type of the bulk species injection source -- must be one of
32
34
  the following EPANET toolkit constants:
@@ -198,7 +200,7 @@ class SpeciesInjectionEvent(SystemEvent, JsonSerializable):
198
200
  pattern_id)
199
201
 
200
202
  def cleanup(self) -> None:
201
- warnings.warn("Can not undo SpeciedInjectionEvent -- " +
203
+ warnings.warn("Can not undo SpeciesInjectionEvent -- " +
202
204
  "EPANET-MSX does not support removing patterns")
203
205
 
204
206
  def apply(self, cur_time: int) -> None:
@@ -41,6 +41,10 @@ class ScadaData(Serializable):
41
41
 
42
42
  This parameter is expected to be a 1d array with the same size as
43
43
  the number of rows in `sensor_readings_data_raw`.
44
+ network_topo : :class:`~epyt_flow.topology.NetworkTopology`
45
+ Topology of the water distribution network.
46
+ warnings_code : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
47
+ Codes/IDs of EPANET errors/warnings (if any) for each time step.
44
48
  pressure_data_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
45
49
  Raw pressure values of all nodes as a two-dimensional array --
46
50
  first dimension encodes time, second dimension pressure at nodes.
@@ -128,10 +132,9 @@ class ScadaData(Serializable):
128
132
  will be stored -- this usually leads to a significant reduction in memory consumption.
129
133
 
130
134
  The default is False.
131
- network_topo : :class:`~epyt_flow.topology.NetworkTopology`
132
- Topology of the water distribution network.
133
135
  """
134
136
  def __init__(self, sensor_config: SensorConfig, sensor_readings_time: np.ndarray,
137
+ network_topo: NetworkTopology, warnings_code: np.ndarray = None,
135
138
  pressure_data_raw: Union[np.ndarray, bsr_array] = None,
136
139
  flow_data_raw: Union[np.ndarray, bsr_array] = None,
137
140
  demand_data_raw: Union[np.ndarray, bsr_array] = None,
@@ -151,17 +154,11 @@ class ScadaData(Serializable):
151
154
  sensor_reading_attacks: list[SensorReadingAttack] = [],
152
155
  sensor_reading_events: list[SensorReadingEvent] = [],
153
156
  sensor_noise: SensorNoise = None, frozen_sensor_config: bool = False,
154
- network_topo: NetworkTopology = None,
155
157
  **kwds):
156
- if network_topo is not None:
157
- if not isinstance(network_topo, NetworkTopology):
158
- raise TypeError("'network_topo' must be an instance of " +
159
- "'epyt_flow.topology.NetworkTopology' but not " +
160
- f"of '{type(network_topo)}'")
161
- else:
162
- warnings.warn("You are loading a SCADA data instance that was created with an " +
163
- "outdated version of EPyT-Flow. Future releases will require " +
164
- "'network_topo' != None. Please upgrade!")
158
+ if not isinstance(network_topo, NetworkTopology):
159
+ raise TypeError("'network_topo' must be an instance of " +
160
+ "'epyt_flow.topology.NetworkTopology' but not " +
161
+ f"of '{type(network_topo)}'")
165
162
  if not isinstance(sensor_config, SensorConfig):
166
163
  raise TypeError("'sensor_config' must be an instance of " +
167
164
  "'epyt_flow.simulation.SensorConfig' but not of " +
@@ -169,6 +166,14 @@ class ScadaData(Serializable):
169
166
  if not isinstance(sensor_readings_time, np.ndarray):
170
167
  raise TypeError("'sensor_readings_time' must be an instance of 'numpy.ndarray' " +
171
168
  f"but not of '{type(sensor_readings_time)}'")
169
+ if warnings_code is None:
170
+ warnings.warn("Loading a file that was created with an outdated version of EPyT-Flow" +
171
+ " -- support of such old files will be removed in the next release!",
172
+ DeprecationWarning)
173
+ else:
174
+ if not isinstance(warnings_code, np.ndarray):
175
+ raise TypeError("'warnings_code' must be an instance of 'numpy.ndarray' " +
176
+ f"but not of '{type(warnings_code)}'")
172
177
  if pressure_data_raw is not None:
173
178
  if not isinstance(pressure_data_raw, np.ndarray) and \
174
179
  not isinstance(pressure_data_raw, bsr_array):
@@ -306,6 +311,9 @@ class ScadaData(Serializable):
306
311
  "must match number of raw measurements.")
307
312
 
308
313
  n_time_steps = sensor_readings_time.shape[0]
314
+ if warnings_code is not None:
315
+ if warnings_code.shape[0] != n_time_steps:
316
+ __raise_shape_mismatch("warnings_code")
309
317
  if pressure_data_raw is not None:
310
318
  if pressure_data_raw.shape[0] != n_time_steps:
311
319
  __raise_shape_mismatch("pressure_data_raw")
@@ -360,6 +368,7 @@ class ScadaData(Serializable):
360
368
 
361
369
  self.__network_topo = network_topo
362
370
  self.__sensor_config = sensor_config
371
+ self.__warnings_code = warnings_code
363
372
  self.__sensor_noise = sensor_noise
364
373
  self.__sensor_reading_events = sensor_faults + sensor_reading_attacks + \
365
374
  sensor_reading_events
@@ -1045,6 +1054,7 @@ class ScadaData(Serializable):
1045
1054
  surface_species_area_unit=new_surface_species_area_unit)
1046
1055
 
1047
1056
  return ScadaData(network_topo=self.network_topo,
1057
+ warnings_code=self.warnings_code,
1048
1058
  sensor_config=sensor_config,
1049
1059
  sensor_readings_time=self.sensor_readings_time,
1050
1060
  sensor_reading_events=self.sensor_reading_events,
@@ -1076,6 +1086,19 @@ class ScadaData(Serializable):
1076
1086
  """
1077
1087
  return deepcopy(self.__network_topo)
1078
1088
 
1089
+ @property
1090
+ def warnings_code(self) -> np.ndarray:
1091
+ """
1092
+ Returns the codes/IDs of EPANET errors/warnings (if any) for each time step.
1093
+ Note that zero denotes the absence of any error/warning.
1094
+
1095
+ Returns:
1096
+ --------
1097
+ `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1098
+ Codes/IDs of EPANET errors/warnings (if any) for each time step.
1099
+ """
1100
+ return deepcopy(self.__warnings_code)
1101
+
1079
1102
  @property
1080
1103
  def frozen_sensor_config(self) -> bool:
1081
1104
  """
@@ -1402,6 +1425,7 @@ class ScadaData(Serializable):
1402
1425
 
1403
1426
  def get_attributes(self) -> dict:
1404
1427
  attr = {"network_topo": self.__network_topo,
1428
+ "warnings_code": self.__warnings_code,
1405
1429
  "sensor_config": self.__sensor_config,
1406
1430
  "frozen_sensor_config": self.__frozen_sensor_config,
1407
1431
  "sensor_noise": self.__sensor_noise,
@@ -1554,6 +1578,7 @@ class ScadaData(Serializable):
1554
1578
 
1555
1579
  try:
1556
1580
  return self.__network_topo == other.network_topo \
1581
+ and np.all(self.__warnings_code == other.warnings_code) \
1557
1582
  and self.__sensor_config == other.sensor_config \
1558
1583
  and self.__frozen_sensor_config == other.frozen_sensor_config \
1559
1584
  and self.__sensor_noise == other.sensor_noise \
@@ -1583,6 +1608,7 @@ class ScadaData(Serializable):
1583
1608
 
1584
1609
  def __str__(self) -> str:
1585
1610
  return f"network_topo: {self.__network_topo} sensor_config: {self.__sensor_config} " + \
1611
+ f"warnings_code: {self.__warnings_code} " + \
1586
1612
  f"frozen_sensor_config: {self.__frozen_sensor_config} " + \
1587
1613
  f"sensor_noise: {self.__sensor_noise} " + \
1588
1614
  f"sensor_reading_events: {self.__sensor_reading_events} " + \
@@ -1782,6 +1808,7 @@ class ScadaData(Serializable):
1782
1808
 
1783
1809
  return ScadaData(network_topo=self.network_topo, sensor_config=self.sensor_config,
1784
1810
  sensor_readings_time=self.sensor_readings_time[start_idx:end_idx],
1811
+ warnings_code=self.__warnings_code[start_idx:end_idx],
1785
1812
  frozen_sensor_config=self.frozen_sensor_config,
1786
1813
  sensor_noise=self.sensor_noise,
1787
1814
  sensor_reading_events=self.sensor_reading_events,
@@ -1940,6 +1967,8 @@ class ScadaData(Serializable):
1940
1967
  self.__sensor_readings_time = np.concatenate(
1941
1968
  (self.__sensor_readings_time, other.sensor_readings_time), axis=0)
1942
1969
 
1970
+ self.__warnings_code = np.concatenate((self.__warnings_code, other.warnings_code), axis=0)
1971
+
1943
1972
  if self.__pressure_data_raw is not None:
1944
1973
  self.__pressure_data_raw = np.concatenate(
1945
1974
  (self.__pressure_data_raw, other.pressure_data_raw), axis=0)
@@ -2928,6 +2957,38 @@ class ScadaData(Serializable):
2928
2957
  for s_id in sensor_locations]
2929
2958
  return self.__sensor_readings[:, idx]
2930
2959
 
2960
+ def get_data_pumps_state_as_node_features(self,
2961
+ default_missing_value: float = 0.
2962
+ ) -> tuple[np.ndarray, np.ndarray]:
2963
+ """
2964
+ Returns the pump state as node features together with a boolean mask indicating the
2965
+ presence of a sensor.
2966
+
2967
+ Parameters
2968
+ ----------
2969
+ default_missing_value : `float`, optional
2970
+ Default value (i.e. missing value) for nodes where no pump state sensor is installed.
2971
+
2972
+ The default is 0.
2973
+
2974
+ Returns
2975
+ -------
2976
+ tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
2977
+ Pump state as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
2978
+ """
2979
+ mask = np.zeros(len(self.__sensor_config.pumps))
2980
+ pump_features = np.array([[default_missing_value] * len(self.__sensor_config.pumps)
2981
+ for _ in range(len(self.__sensor_readings_time))])
2982
+ pumps_id = self.__network_topo.get_all_pumps()
2983
+
2984
+ state_readings = self.get_data_pumps_state()
2985
+ for pumps_state_idx, pump_id in enumerate(self.__sensor_config.pump_state_sensors):
2986
+ idx = pumps_id.index(pump_id)
2987
+ pump_features[:, idx] = state_readings[:, pumps_state_idx]
2988
+ mask[idx] = 1
2989
+
2990
+ return pump_features, mask
2991
+
2931
2992
  def plot_pumps_state(self, sensor_locations: list[str] = None, show: bool = True,
2932
2993
  save_to_file: str = None, ax: matplotlib.axes.Axes = None
2933
2994
  ) -> matplotlib.axes.Axes:
@@ -3015,6 +3076,38 @@ class ScadaData(Serializable):
3015
3076
  for s_id in sensor_locations]
3016
3077
  return self.__sensor_readings[:, idx]
3017
3078
 
3079
+ def get_data_pumps_efficiency_as_node_features(self,
3080
+ default_missing_value: float = 0.
3081
+ ) -> tuple[np.ndarray, np.ndarray]:
3082
+ """
3083
+ Returns the pump efficiency as node features together with a boolean mask indicating the
3084
+ presence of a sensor.
3085
+
3086
+ Parameters
3087
+ ----------
3088
+ default_missing_value : `float`, optional
3089
+ Default value (i.e. missing value) for nodes where no pump efficiency sensor is installed.
3090
+
3091
+ The default is 0.
3092
+
3093
+ Returns
3094
+ -------
3095
+ tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
3096
+ Pump efficiencies as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
3097
+ """
3098
+ mask = np.zeros(len(self.__sensor_config.pumps))
3099
+ pump_features = np.array([[default_missing_value] * len(self.__sensor_config.pumps)
3100
+ for _ in range(len(self.__sensor_readings_time))])
3101
+ pumps_id = self.__network_topo.get_all_pumps()
3102
+
3103
+ efficiency_readings = self.get_data_pumps_efficiency()
3104
+ for pumps_efficiency_idx, pump_id in enumerate(self.__sensor_config.pump_efficiency_sensors):
3105
+ idx = pumps_id.index(pump_id)
3106
+ pump_features[:, idx] = efficiency_readings[:, pumps_efficiency_idx]
3107
+ mask[idx] = 1
3108
+
3109
+ return pump_features, mask
3110
+
3018
3111
  def plot_pumps_efficiency(self, sensor_locations: list[str] = None, show: bool = True,
3019
3112
  save_to_file: str = None, ax: matplotlib.axes.Axes = None
3020
3113
  ) -> matplotlib.axes.Axes:
@@ -3103,6 +3196,38 @@ class ScadaData(Serializable):
3103
3196
  for s_id in sensor_locations]
3104
3197
  return self.__sensor_readings[:, idx]
3105
3198
 
3199
+ def get_data_pumps_energyconsumption_as_node_features(self,
3200
+ default_missing_value: float = 0.
3201
+ ) -> tuple[np.ndarray, np.ndarray]:
3202
+ """
3203
+ Returns the pump energy consumption as node features together with a boolean mask indicating the
3204
+ presence of a sensor.
3205
+
3206
+ Parameters
3207
+ ----------
3208
+ default_missing_value : `float`, optional
3209
+ Default value (i.e. missing value) for nodes where no pump energy consumption sensor is installed.
3210
+
3211
+ The default is 0.
3212
+
3213
+ Returns
3214
+ -------
3215
+ tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
3216
+ Pump energy consumptions as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
3217
+ """
3218
+ mask = np.zeros(len(self.__sensor_config.pumps))
3219
+ pump_features = np.array([[default_missing_value] * len(self.__sensor_config.pumps)
3220
+ for _ in range(len(self.__sensor_readings_time))])
3221
+ pumps_id = self.__network_topo.get_all_pumps()
3222
+
3223
+ energyconsumption_readings = self.get_data_pumps_energyconsumption()
3224
+ for pumps_energyconsumption_idx, pump_id in enumerate(self.__sensor_config.pump_energyconsumption_sensors):
3225
+ idx = pumps_id.index(pump_id)
3226
+ pump_features[:, idx] = energyconsumption_readings[:, pumps_energyconsumption_idx]
3227
+ mask[idx] = 1
3228
+
3229
+ return pump_features, mask
3230
+
3106
3231
  def plot_pumps_energyconsumption(self, sensor_locations: list[str] = None, show: bool = True,
3107
3232
  save_to_file: str = None, ax: matplotlib.axes.Axes = None
3108
3233
  ) -> matplotlib.axes.Axes:
@@ -3190,6 +3315,38 @@ class ScadaData(Serializable):
3190
3315
  for s_id in sensor_locations]
3191
3316
  return self.__sensor_readings[:, idx]
3192
3317
 
3318
+ def get_data_valves_state_as_node_features(self,
3319
+ default_missing_value: float = 0.
3320
+ ) -> tuple[np.ndarray, np.ndarray]:
3321
+ """
3322
+ Returns the valves state as node features together with a boolean mask indicating the
3323
+ presence of a sensor.
3324
+
3325
+ Parameters
3326
+ ----------
3327
+ default_missing_value : `float`, optional
3328
+ Default value (i.e. missing value) for nodes where no valves state sensor is installed.
3329
+
3330
+ The default is 0.
3331
+
3332
+ Returns
3333
+ -------
3334
+ tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
3335
+ Valves state as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
3336
+ """
3337
+ mask = np.zeros(len(self.__sensor_config.valves))
3338
+ valve_features = np.array([[default_missing_value] * len(self.__sensor_config.valves)
3339
+ for _ in range(len(self.__sensor_readings_time))])
3340
+ valves_id = self.__network_topo.get_all_valves()
3341
+
3342
+ state_readings = self.get_data_valves_state()
3343
+ for valves_state_idx, valve_id in enumerate(self.__sensor_config.valve_state_sensors):
3344
+ idx = valves_id.index(valve_id)
3345
+ valve_features[:, idx] = state_readings[:, valves_state_idx]
3346
+ mask[idx] = 1
3347
+
3348
+ return valve_features, mask
3349
+
3193
3350
  def plot_valves_state(self, sensor_locations: list[str] = None, show: bool = True,
3194
3351
  save_to_file: str = None, ax: matplotlib.axes.Axes = None
3195
3352
  ) -> matplotlib.axes.Axes:
@@ -3277,6 +3434,38 @@ class ScadaData(Serializable):
3277
3434
  for s_id in sensor_locations]
3278
3435
  return self.__sensor_readings[:, idx]
3279
3436
 
3437
+ def get_data_tanks_water_volume_as_node_features(self,
3438
+ default_missing_value: float = 0.
3439
+ ) -> tuple[np.ndarray, np.ndarray]:
3440
+ """
3441
+ Returns the tank water volume as node features together with a boolean mask indicating the
3442
+ presence of a sensor.
3443
+
3444
+ Parameters
3445
+ ----------
3446
+ default_missing_value : `float`, optional
3447
+ Default value (i.e. missing value) for nodes where no tank water volume sensor is installed.
3448
+
3449
+ The default is 0.
3450
+
3451
+ Returns
3452
+ -------
3453
+ tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
3454
+ Tank water volumes as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
3455
+ """
3456
+ mask = np.zeros(len(self.__sensor_config.tanks))
3457
+ tank_features = np.array([[default_missing_value] * len(self.__sensor_config.tanks)
3458
+ for _ in range(len(self.__sensor_readings_time))])
3459
+ tanks_id = self.__network_topo.get_all_tanks()
3460
+
3461
+ water_volume_readings = self.get_data_tanks_water_volume()
3462
+ for tanks_water_volume_idx, tank_id in enumerate(self.__sensor_config.tank_volume_sensors):
3463
+ idx = tanks_id.index(tank_id)
3464
+ tank_features[:, idx] = water_volume_readings[:, tanks_water_volume_idx]
3465
+ mask[idx] = 1
3466
+
3467
+ return tank_features, mask
3468
+
3280
3469
  def plot_tanks_water_volume(self, sensor_locations: list[str] = None, show: bool = True,
3281
3470
  save_to_file: str = None, ax: matplotlib.axes.Axes = None
3282
3471
  ) -> matplotlib.axes.Axes: