epyt-flow 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
epyt_flow/VERSION CHANGED
@@ -1 +1 @@
1
- 0.5.0
1
+ 0.6.0
@@ -458,10 +458,14 @@ def load_scenario(return_test_scenario: bool, download_dir: str = None,
458
458
  ltown_config = load_ltown(use_realistic_demands=True, include_default_sensor_placement=True,
459
459
  verbose=verbose)
460
460
 
461
- # Set simulation duration
461
+ # Set simulation duration and other general parameters such as the demand model
462
462
  general_params = {"simulation_duration": to_seconds(days=365), # One year
463
463
  "hydraulic_time_step": to_seconds(minutes=5), # 5min time steps
464
- "reporting_time_step": to_seconds(minutes=5)} | ltown_config.general_params
464
+ "reporting_time_step": to_seconds(minutes=5),
465
+ "demand_model": {"type": "PDA", "pressure_min": 0,
466
+ "pressure_required": 0.1,
467
+ "pressure_exponent": 0.5}
468
+ } | ltown_config.general_params
465
469
 
466
470
  # Add events
467
471
  start_time = START_TIME_TEST if return_test_scenario is True else START_TIME_TRAIN
@@ -30,7 +30,7 @@ from .leakdb_data import NET1_LEAKAGES, HANOI_LEAKAGES
30
30
  from ...utils import get_temp_folder, to_seconds, unpack_zip_archive, create_path_if_not_exist, \
31
31
  download_if_necessary
32
32
  from ...metrics import f1_score, true_positive_rate, true_negative_rate
33
- from ...simulation import ScenarioSimulator
33
+ from ...simulation import ScenarioSimulator, ToolkitConstants
34
34
  from ...simulation.events import AbruptLeakage, IncipientLeakage
35
35
  from ...simulation import ScenarioConfig
36
36
  from ...simulation.scada import ScadaData
@@ -424,14 +424,19 @@ def load_scenarios(scenarios_id: list[int], use_net1: bool = True,
424
424
  download_dir = download_dir if download_dir is not None else get_temp_folder()
425
425
  network_config = load_network(download_dir)
426
426
 
427
- # Set simulation duration
427
+ # Set simulation duration and other general parameters such as the demand model and flow units
428
428
  hydraulic_time_step = to_seconds(minutes=30) # 30min time steps
429
429
  general_params = {"simulation_duration": to_seconds(days=365), # One year
430
430
  "hydraulic_time_step": hydraulic_time_step,
431
- "reporting_time_step": hydraulic_time_step} | network_config.general_params
431
+ "reporting_time_step": hydraulic_time_step,
432
+ "flow_units_id": ToolkitConstants.EN_CMH,
433
+ "demand_model": {"type": "PDA", "pressure_min": 0,
434
+ "pressure_required": 0.1,
435
+ "pressure_exponent": 0.5}
436
+ } | network_config.general_params
432
437
 
433
438
  # Add demand patterns
434
- def gen_dem(download_dir, use_net1):
439
+ def gen_dem(download_dir):
435
440
  # Taken from https://github.com/KIOS-Research/LeakDB/blob/master/CCWI-WDSA2018/Dataset_Generator_Py3/demandGenerator.py
436
441
  week_pat = scipy.io.loadmat(os.path.join(download_dir, "weekPat_30min.mat"))
437
442
  a_w = week_pat['Aw']
@@ -503,10 +508,7 @@ def load_scenarios(scenarios_id: list[int], use_net1: bool = True,
503
508
 
504
509
  if not os.path.exists(f_inp_in):
505
510
  with ScenarioSimulator(f_inp_in=network_config.f_inp_in) as wdn:
506
- wdn.epanet_api.setTimeHydraulicStep(general_params["hydraulic_time_step"])
507
- wdn.epanet_api.setTimeSimulationDuration(general_params["simulation_duration"])
508
- wdn.epanet_api.setTimePatternStep(general_params["hydraulic_time_step"])
509
- wdn.epanet_api.setFlowUnitsCMH()
511
+ wdn.set_general_parameters(**general_params)
510
512
 
511
513
  wdn.epanet_api.deletePatternsAll()
512
514
 
@@ -519,7 +521,7 @@ def load_scenarios(scenarios_id: list[int], use_net1: bool = True,
519
521
  node_idx = wdn.epanet_api.getNodeIndex(node_id)
520
522
  base_demand = wdn.epanet_api.getNodeBaseDemands(node_idx)[1][0]
521
523
 
522
- my_demand_pattern = np.array(gen_dem(download_dir, use_net1))
524
+ my_demand_pattern = np.array(gen_dem(download_dir))
523
525
 
524
526
  wdn.set_node_demand_pattern(node_id=node_id, base_demand=base_demand,
525
527
  demand_pattern_id=f"demand_{node_id}",
@@ -56,8 +56,7 @@ def get_default_hydraulic_options(flow_units_id: int = None) -> dict:
56
56
  Dictionary with default hydraulics options that can be passed to
57
57
  :func:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator.set_general_parameters`.
58
58
  """
59
- params = {"demand_model": {"type": "PDA", "pressure_min": 0, "pressure_required": 0.1,
60
- "pressure_exponent": 0.5}}
59
+ params = {}
61
60
  if flow_units_id is not None:
62
61
  params |= {"flow_units_id": flow_units_id}
63
62
 
@@ -2,7 +2,9 @@
2
2
  Module provides a base class for control environments.
3
3
  """
4
4
  from abc import abstractmethod, ABC
5
- from copy import deepcopy
5
+ from typing import Union
6
+ import warnings
7
+ import numpy as np
6
8
 
7
9
  from ..simulation import ScenarioSimulator, ScenarioConfig, ScadaData
8
10
 
@@ -32,8 +34,13 @@ class ScenarioControlEnv(ABC):
32
34
  def autoreset(self) -> bool:
33
35
  """
34
36
  True, if environment automatically resets after it terminated.
37
+
38
+ Returns
39
+ -------
40
+ `bool`
41
+ True, if environment automatically resets after it terminated.
35
42
  """
36
- return deepcopy(self.__autoreset)
43
+ return self.__autoreset
37
44
 
38
45
  def __enter__(self):
39
46
  return self
@@ -58,6 +65,11 @@ class ScenarioControlEnv(ABC):
58
65
  def reset(self) -> ScadaData:
59
66
  """
60
67
  Resets the environment (i.e. simulation).
68
+
69
+ Returns
70
+ -------
71
+ :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
72
+ Current SCADA data (i.e. sensor readings).
61
73
  """
62
74
  if self._scenario_sim is not None:
63
75
  self._scenario_sim.close()
@@ -83,19 +95,102 @@ class ScenarioControlEnv(ABC):
83
95
  else:
84
96
  return None, True
85
97
 
98
+ def set_pump_status(self, pump_id: str, status: int) -> None:
99
+ """
100
+ Sets the status of a pump.
101
+
102
+ Parameters
103
+ ----------
104
+ pump_id : `str`
105
+ ID of the pump for which the status is set.
106
+ status : `int`
107
+ New status of the pump -- either active (i.e. open) or inactive (i.e. closed).
108
+
109
+ Must be one of the following constants defined in
110
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
111
+
112
+ - EN_CLOSED = 0
113
+ - EN_OPEN = 1
114
+ """
115
+ pump_idx = self._scenario_sim.epanet_api.getLinkPumpNameID().index(pump_id)
116
+ pump_link_idx = self._scenario_sim.epanet_api.getLinkPumpIndex(pump_idx + 1)
117
+ self._scenario_sim.epanet_api.setLinkStatus(pump_link_idx, status)
118
+
119
+ def set_pump_speed(self, pump_id: str, speed: float) -> None:
120
+ """
121
+ Sets the speed of a pump.
122
+
123
+ Parameters
124
+ ----------
125
+ pump_id : `str`
126
+ ID of the pump for which the pump speed is set.
127
+ speed : `float`
128
+ New pump speed.
129
+ """
130
+ pump_idx = self._scenario_sim.epanet_api.getLinkPumpNameID().index(pump_id)
131
+ pattern_idx = self._scenario_sim.epanet_api.getLinkPumpPatternIndex(pump_idx + 1)
132
+
133
+ if pattern_idx == 0:
134
+ warnings.warn(f"No pattern for pump '{pump_id}' found -- a new pattern is created")
135
+ pattern_idx = self._scenario_sim.epanet_api.addPattern(f"pump_speed_{pump_id}")
136
+ self._scenario_sim.epanet_api.setLinkPumpPatternIndex(pattern_idx)
137
+
138
+ self._scenario_sim.epanet_api.setPattern(pattern_idx, np.array([speed]))
139
+
140
+ def set_valve_status(self, valve_id: str, status: int) -> None:
141
+ """
142
+ Sets the status of a valve.
143
+
144
+ Parameters
145
+ ----------
146
+ valve_id : `str`
147
+ ID of the valve for which the status is set.
148
+ status : `int`
149
+ New status of the valve -- either open or closed.
150
+
151
+ Must be one of the following constants defined in
152
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
153
+
154
+ - EN_CLOSED = 0
155
+ - EN_OPEN = 1
156
+ """
157
+ valve_idx = self._scenario_sim.epanet_api.getLinkValveNameID().index(valve_id)
158
+ valve_link_idx = self._scenario_sim.epanet_api.getLinkValveIndex()[valve_idx]
159
+ self._scenario_sim.epanet_api.setLinkStatus(valve_link_idx, status)
160
+
161
+ def set_node_quality_source_value(self, node_id: str, pattern_id: str,
162
+ qual_value: float) -> None:
163
+ """
164
+ Sets the quality source at a particular node to a specific value -- e.g.
165
+ setting the chlorine concentration injection to a specified value.
166
+
167
+ Parameters
168
+ ----------
169
+ node_id : `str`
170
+ ID of the node.
171
+ pattern_id : `str`
172
+ ID of the quality pattern at the specific node.
173
+ qual_value : `float`
174
+ New quality source value.
175
+ """
176
+ node_idx = self._scenario_sim.epanet_api.getNodeIndex(node_id)
177
+ pattern_idx = self._scenario_sim.epanet_api.getPatternIndex(pattern_id)
178
+ self._scenario_sim.epanet_api.setNodeSourceQuality(node_idx, 1)
179
+ self._scenario_sim.epanet_api.setPattern(pattern_idx, np.array([qual_value]))
180
+
86
181
  @abstractmethod
87
- def step(self) -> tuple[ScadaData, float, bool]:
182
+ def step(self, *actions) -> Union[tuple[ScadaData, float, bool], tuple[ScadaData, float]]:
88
183
  """
89
184
  Performs the next step by applying an action and observing
90
185
  the consequences (SCADA data, reward, terminated).
91
186
 
92
187
  Note that `terminated` is only returned if `autoreset=False` otherwise
93
- only SCADA data and reward are returned.
188
+ only the current SCADA data and reward are returned.
94
189
 
95
190
  Returns
96
191
  -------
97
- `(ScadaData, float, bool)`
98
- Triple of observations (:class:`~epyt_flow.simuation.scada.scada_data.ScadaData`),
192
+ `(` :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` `, float, bool)` or `(` :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` `, float)`
193
+ Triple or tuple of observations (:class:`~epyt_flow.simulation.scada.scada_data.ScadaData`),
99
194
  reward (`float`), and terminated (`bool`).
100
195
  """
101
196
  raise NotImplementedError()
@@ -4,6 +4,7 @@ Module provides functions and classes for serialization.
4
4
  from typing import Any, Union
5
5
  from abc import abstractmethod, ABC
6
6
  from io import BufferedIOBase
7
+ import pathlib
7
8
  import importlib
8
9
  import json
9
10
  import gzip
@@ -97,7 +98,9 @@ class Serializable(ABC):
97
98
  Base class for a serializable class -- must be used in conjunction with the
98
99
  :func:`~epyt_flow.serialization.serializable` decorator.
99
100
  """
100
- def __init__(self, **kwds):
101
+ def __init__(self, _parent_path: str = "", **kwds):
102
+ self._parent_path = _parent_path
103
+
101
104
  super().__init__(**kwds)
102
105
 
103
106
  @abstractmethod
@@ -376,12 +379,19 @@ def load_from_file(f_in: str, use_compression: bool = True) -> Any:
376
379
  `Any`
377
380
  Deserialized data.
378
381
  """
382
+ inst = None
383
+
379
384
  if use_compression is False:
380
385
  with open(f_in, "rb") as f:
381
- return umsgpack.unpack(f, ext_handlers=ext_handler_unpack)
386
+ inst = load(f.read())
382
387
  else:
383
388
  with gzip.open(f_in, "rb") as f:
384
- return load(f.read())
389
+ inst = load(f.read())
390
+
391
+ if isinstance(inst, Serializable):
392
+ inst._parent_path = pathlib.Path(f_in).parent.resolve()
393
+
394
+ return inst
385
395
 
386
396
 
387
397
  def save_to_file(f_out: str, data: Any, use_compression: bool = True) -> None:
@@ -98,7 +98,7 @@ class AdvancedControlModule(ABC):
98
98
  - EN_OPEN = 1
99
99
  """
100
100
  valve_idx = self._epanet_api.getLinkValveNameID().index(valve_id)
101
- valve_link_idx = self._epanet_api.getLinkValveIndex(valve_idx + 1)
101
+ valve_link_idx = self._epanet_api.getLinkValveIndex()[valve_idx]
102
102
  self._epanet_api.setLinkStatus(valve_link_idx, status)
103
103
 
104
104
  def set_node_quality_source_value(self, node_id: str, pattern_id: str,
@@ -12,6 +12,7 @@ from ..sensor_config import SensorConfig, is_flowunit_simetric, massunit_to_str,
12
12
  AREA_UNIT_CM2, AREA_UNIT_FT2, AREA_UNIT_M2, \
13
13
  SENSOR_TYPE_LINK_FLOW, SENSOR_TYPE_LINK_QUALITY, SENSOR_TYPE_NODE_DEMAND, \
14
14
  SENSOR_TYPE_NODE_PRESSURE, SENSOR_TYPE_NODE_QUALITY, SENSOR_TYPE_PUMP_STATE, \
15
+ SENSOR_TYPE_PUMP_EFFICIENCY, SENSOR_TYPE_PUMP_ENERGYCONSUMPTION, \
15
16
  SENSOR_TYPE_TANK_VOLUME, SENSOR_TYPE_VALVE_STATE, SENSOR_TYPE_NODE_BULK_SPECIES, \
16
17
  SENSOR_TYPE_LINK_BULK_SPECIES, SENSOR_TYPE_SURFACE_SPECIES
17
18
  from ..events import SensorFault, SensorReadingAttack, SensorReadingEvent
@@ -92,11 +93,11 @@ class ScadaData(Serializable):
92
93
  third dimension denotes species concentrations at nodes.
93
94
 
94
95
  The default is None.
95
- pump_energy_usage_data : `numpy.ndarray`, optional
96
+ pumps_energy_usage_data_raw : `numpy.ndarray`, optional
96
97
  Energy usage data of each pump.
97
98
 
98
99
  The default is None.
99
- pump_efficiency_data : `numpy.ndarray`, optional
100
+ pumps_efficiency_data_raw : `numpy.ndarray`, optional
100
101
  Pump efficiency data of each pump.
101
102
 
102
103
  The default is None.
@@ -130,8 +131,10 @@ class ScadaData(Serializable):
130
131
  surface_species_concentration_raw: np.ndarray = None,
131
132
  bulk_species_node_concentration_raw: np.ndarray = None,
132
133
  bulk_species_link_concentration_raw: np.ndarray = None,
133
- pump_energy_usage_data: np.ndarray = None,
134
- pump_efficiency_data: np.ndarray = None,
134
+ pump_energy_usage_data = None,
135
+ pump_efficiency_data = None,
136
+ pumps_energy_usage_data_raw: np.ndarray = None,
137
+ pumps_efficiency_data_raw: np.ndarray = None,
135
138
  sensor_faults: list[SensorFault] = [],
136
139
  sensor_reading_attacks: list[SensorReadingAttack] = [],
137
140
  sensor_reading_events: list[SensorReadingEvent] = [],
@@ -195,14 +198,14 @@ class ScadaData(Serializable):
195
198
  raise TypeError("'bulk_species_link_concentration_raw' must be an instance of " +
196
199
  "'numpy.ndarray' but not of " +
197
200
  f"'{type(bulk_species_link_concentration_raw)}'")
198
- if pump_energy_usage_data is not None:
199
- if not isinstance(pump_energy_usage_data, np.ndarray):
200
- raise TypeError("'pump_energy_usage_data' must be an instance of 'numpy.ndarray' " +
201
- f"but not of '{type(pump_energy_usage_data)}'")
202
- if pump_efficiency_data is not None:
203
- if not isinstance(pump_efficiency_data, np.ndarray):
204
- raise TypeError("'pump_efficiency_data' must be an instance of 'numpy.ndarray' " +
205
- f"but not of '{type(pump_efficiency_data)}'")
201
+ if pumps_energy_usage_data_raw is not None:
202
+ if not isinstance(pumps_energy_usage_data_raw, np.ndarray):
203
+ raise TypeError("'pumps_energy_usage_data_raw' must be an instance of 'numpy.ndarray' " +
204
+ f"but not of '{type(pumps_energy_usage_data_raw)}'")
205
+ if pumps_efficiency_data_raw is not None:
206
+ if not isinstance(pumps_efficiency_data_raw, np.ndarray):
207
+ raise TypeError("'pumps_efficiency_data_raw' must be an instance of 'numpy.ndarray' " +
208
+ f"but not of '{type(pumps_efficiency_data_raw)}'")
206
209
  if len(sensor_faults) != 0:
207
210
  if any(not isinstance(f, SensorFault) for f in sensor_faults):
208
211
  raise TypeError("'sensor_faults' must be a list of " +
@@ -223,6 +226,11 @@ class ScadaData(Serializable):
223
226
  raise TypeError("'frozen_sensor_config' must be an instance of 'bool' " +
224
227
  f"but not of '{type(frozen_sensor_config)}'")
225
228
 
229
+ if pump_efficiency_data is not None or pump_energy_usage_data is not None:
230
+ warnings.warn("Loading a file that was created with an outdated version of EPyT-Flow" +
231
+ " -- support of such old files will be removed in the next release!",
232
+ DeprecationWarning)
233
+
226
234
  def __raise_shape_mismatch(var_name: str) -> None:
227
235
  raise ValueError(f"Shape mismatch in '{var_name}' -- " +
228
236
  "i.e number of time steps in 'sensor_readings_time' " +
@@ -271,12 +279,12 @@ class ScadaData(Serializable):
271
279
  if surface_species_concentration_raw is not None:
272
280
  if surface_species_concentration_raw.shape[0] != n_time_steps:
273
281
  __raise_shape_mismatch("surface_species_concentration_raw")
274
- if pump_energy_usage_data is not None:
275
- if pump_energy_usage_data.shape[0] != n_time_steps:
276
- __raise_shape_mismatch("pump_energy_usage_data")
277
- if pump_efficiency_data is not None:
278
- if pump_efficiency_data.shape[0] != n_time_steps:
279
- __raise_shape_mismatch("pump_efficiency_data")
282
+ if pumps_energy_usage_data_raw is not None:
283
+ if pumps_energy_usage_data_raw.shape[0] != n_time_steps:
284
+ __raise_shape_mismatch("pumps_energy_usage_data_raw")
285
+ if pumps_efficiency_data_raw is not None:
286
+ if pumps_efficiency_data_raw.shape[0] != n_time_steps:
287
+ __raise_shape_mismatch("pumps_efficiency_data_raw")
280
288
 
281
289
  self.__sensor_config = sensor_config
282
290
  self.__sensor_noise = sensor_noise
@@ -286,8 +294,6 @@ class ScadaData(Serializable):
286
294
  self.__sensor_readings = None
287
295
  self.__frozen_sensor_config = frozen_sensor_config
288
296
  self.__sensor_readings_time = sensor_readings_time
289
- self.__pump_energy_usage_data = pump_energy_usage_data
290
- self.__pump_efficiency_data = pump_efficiency_data
291
297
 
292
298
  if self.__frozen_sensor_config is False:
293
299
  self.__pressure_data_raw = pressure_data_raw
@@ -301,6 +307,8 @@ class ScadaData(Serializable):
301
307
  self.__surface_species_concentration_raw = surface_species_concentration_raw
302
308
  self.__bulk_species_node_concentration_raw = bulk_species_node_concentration_raw
303
309
  self.__bulk_species_link_concentration_raw = bulk_species_link_concentration_raw
310
+ self.__pumps_energy_usage_data_raw = pumps_energy_usage_data_raw
311
+ self.__pumps_efficiency_data_raw = pumps_efficiency_data_raw
304
312
  else:
305
313
  sensor_config = self.__sensor_config
306
314
 
@@ -338,6 +346,14 @@ class ScadaData(Serializable):
338
346
  self.__pumps_state_data_raw = __reduce_data(data=pumps_state_data_raw,
339
347
  item_to_idx=pump_to_idx,
340
348
  sensors=sensor_config.pump_state_sensors)
349
+ self.__pumps_energy_usage_data_raw = \
350
+ __reduce_data(data=pumps_energy_usage_data_raw,
351
+ item_to_idx=pump_to_idx,
352
+ sensors=sensor_config.pump_enegeryconsumption_sensors)
353
+ self.__pumps_efficiency_data_raw = \
354
+ __reduce_data(data=pumps_efficiency_data_raw,
355
+ item_to_idx=pump_to_idx,
356
+ sensors=sensor_config.pump_efficiency_sensors)
341
357
  self.__valves_state_data_raw = __reduce_data(data=valves_state_data_raw,
342
358
  item_to_idx=valve_to_idx,
343
359
  sensors=sensor_config.valve_state_sensors)
@@ -881,6 +897,10 @@ class ScadaData(Serializable):
881
897
  quality_link_sensors=self.__sensor_config.quality_link_sensors,
882
898
  valve_state_sensors=self.__sensor_config.valve_state_sensors,
883
899
  pump_state_sensors=self.__sensor_config.pump_state_sensors,
900
+ pump_efficiency_sensors=
901
+ self.__sensor_config.pump_efficiency_sensors,
902
+ pump_energyconsumption_sensors=
903
+ self.__sensor_config.pump_energyconsumption_sensors,
884
904
  tank_volume_sensors=self.__sensor_config.tank_volume_sensors,
885
905
  bulk_species_node_sensors=
886
906
  self.__sensor_config.bulk_species_node_sensors,
@@ -906,8 +926,8 @@ class ScadaData(Serializable):
906
926
  pumps_state_data_raw=self.pumps_state_data_raw,
907
927
  valves_state_data_raw=self.valves_state_data_raw,
908
928
  tanks_volume_data_raw=tanks_volume_data,
909
- pump_energy_usage_data=self.pump_energy_usage_data,
910
- pump_efficiency_data=self.pump_efficiency_data,
929
+ pumps_energy_usage_data_raw=self.pumps_energyconsumption_data_raw,
930
+ pumps_efficiency_data_raw=self.pumps_efficiency_data_raw,
911
931
  bulk_species_node_concentration_raw=bulk_species_node_concentrations,
912
932
  bulk_species_link_concentration_raw=bulk_species_link_concentrations,
913
933
  surface_species_concentration_raw=surface_species_concentrations)
@@ -1154,44 +1174,28 @@ class ScadaData(Serializable):
1154
1174
  return deepcopy(self.__bulk_species_link_concentration_raw)
1155
1175
 
1156
1176
  @property
1157
- def pump_energy_usage_data(self) -> np.ndarray:
1177
+ def pumps_energyconsumption_data_raw(self) -> np.ndarray:
1158
1178
  """
1159
- Gets the energy usage of each pump.
1160
-
1161
- .. note::
1162
- This attribute is NOT included in
1163
- :func:`~epyt_flow.simulation.scada.scada_data.ScadaData.get_data` --
1164
- calling this function is the only way of accessing the energy usage of each pump.
1165
-
1166
- The odering in the returned NumPy array corresponds to the ordering
1167
- of the pumps in EPANET.
1179
+ Gets the raw energy consumption of each pump.
1168
1180
 
1169
1181
  Returns
1170
1182
  -------
1171
1183
  `numpy.ndarray`
1172
- Energy usage of each pump.
1184
+ Energy consumption of each pump.
1173
1185
  """
1174
- return deepcopy(self.__pump_energy_usage_data)
1186
+ return deepcopy(self.__pumps_energy_usage_data_raw)
1175
1187
 
1176
1188
  @property
1177
- def pump_efficiency_data(self) -> np.ndarray:
1189
+ def pumps_efficiency_data_raw(self) -> np.ndarray:
1178
1190
  """
1179
- Gets the pumps' efficiency.
1180
-
1181
- .. note::
1182
- This attribute is NOT included in
1183
- :func:`~epyt_flow.simulation.scada.scada_data.ScadaData.get_data` --
1184
- calling this function is the only way of accessing the pumps' efficiency.
1185
-
1186
- The odering in the returned NumPy array corresponds to the ordering
1187
- of the pumps in EPANET.
1191
+ Gets the raw efficiency of each pump.
1188
1192
 
1189
1193
  Returns
1190
1194
  -------
1191
1195
  `numpy.ndarray`
1192
1196
  Pumps' efficiency.
1193
1197
  """
1194
- return deepcopy(self.__pump_efficiency_data)
1198
+ return deepcopy(self.__pumps_efficiency_data_raw)
1195
1199
 
1196
1200
  def __init(self):
1197
1201
  self.__apply_sensor_noise = lambda x: x
@@ -1222,6 +1226,12 @@ class ScadaData(Serializable):
1222
1226
  elif sensor_event.sensor_type == SENSOR_TYPE_PUMP_STATE:
1223
1227
  idx = self.__sensor_config.get_index_of_reading(
1224
1228
  pump_state_sensor=sensor_event.sensor_id)
1229
+ elif sensor_event.sensor_type == SENSOR_TYPE_PUMP_EFFICIENCY:
1230
+ idx = self.__sensor_config.get_index_of_reading(
1231
+ pump_efficiency_sensor=sensor_event.sensor_id)
1232
+ elif sensor_event.sensor_type == SENSOR_TYPE_PUMP_ENERGYCONSUMPTION:
1233
+ idx = self.__sensor_config.get_index_of_reading(
1234
+ pump_energyconsumption_sensor=sensor_event.sensor_id)
1225
1235
  elif sensor_event.sensor_type == SENSOR_TYPE_TANK_VOLUME:
1226
1236
  idx = self.__sensor_config.get_index_of_reading(
1227
1237
  tank_volume_sensor=sensor_event.sensor_id)
@@ -1256,8 +1266,8 @@ class ScadaData(Serializable):
1256
1266
  "surface_species_concentration_raw": self.__surface_species_concentration_raw,
1257
1267
  "bulk_species_node_concentration_raw": self.__bulk_species_node_concentration_raw,
1258
1268
  "bulk_species_link_concentration_raw": self.__bulk_species_link_concentration_raw,
1259
- "pump_energy_usage_data": self.__pump_energy_usage_data,
1260
- "pump_efficiency_data": self.__pump_efficiency_data}
1269
+ "pumps_energy_usage_data_raw": self.__pumps_energy_usage_data_raw,
1270
+ "pumps_efficiency_data_raw": self.__pumps_efficiency_data_raw}
1261
1271
 
1262
1272
  return super().get_attributes() | attr
1263
1273
 
@@ -1286,8 +1296,9 @@ class ScadaData(Serializable):
1286
1296
  other.bulk_species_node_concentration_raw) \
1287
1297
  and np.all(self.__bulk_species_link_concentration_raw ==
1288
1298
  other.bulk_species_link_concentration_raw) \
1289
- and np.all(self.__pump_energy_usage_data == other.pump_energy_usage_data) \
1290
- and np.all(self.__pump_efficiency_data == other.pump_efficiency_data)
1299
+ and np.all(self.__pumps_energy_usage_data_raw ==
1300
+ other.pumps_energyconsumption_data_raw) \
1301
+ and np.all(self.__pumps_efficiency_data_raw == other.pumps_efficiency_data_raw)
1291
1302
  except Exception as ex:
1292
1303
  warnings.warn(ex.__str__())
1293
1304
  return False
@@ -1308,8 +1319,8 @@ class ScadaData(Serializable):
1308
1319
  f"surface_species_concentration_raw: {self.__surface_species_concentration_raw} " + \
1309
1320
  f"bulk_species_node_concentration_raw: {self.__bulk_species_node_concentration_raw}" +\
1310
1321
  f" bulk_species_link_concentration_raw: {self.__bulk_species_link_concentration_raw}" +\
1311
- f" pump_efficiency_data: {self.__pump_efficiency_data} " + \
1312
- f"pump_energy_usage_data: {self.__pump_energy_usage_data}"
1322
+ f" pumps_efficiency_data_raw: {self.__pumps_efficiency_data_raw} " + \
1323
+ f"pumps_energy_usage_data_raw: {self.__pumps_energy_usage_data_raw}"
1313
1324
 
1314
1325
  def change_sensor_config(self, sensor_config: SensorConfig) -> None:
1315
1326
  """
@@ -1491,11 +1502,13 @@ class ScadaData(Serializable):
1491
1502
  self.__sensor_config.surface_species_sensors = \
1492
1503
  other.sensor_config.surface_species_sensors
1493
1504
 
1494
- if self.__pump_energy_usage_data is None and other.pump_energy_usage_data is not None:
1495
- self.__pump_energy_usage_data = other.pump_energy_usage_data
1505
+ if self.__pumps_energy_usage_data_raw is None and \
1506
+ other.pumps_energy_usage_data_raw is not None:
1507
+ self.__pumps_energy_usage_data_raw = other.pumps_energy_usage_data_raw
1496
1508
 
1497
- if self.__pump_efficiency_data is None and other.pump_efficiency_data is not None:
1498
- self.__pump_efficiency_data = other.pump_efficiency_data
1509
+ if self.__pumps_efficiency_data_raw is None and \
1510
+ other.pumps_efficiency_data_raw is not None:
1511
+ self.__pumps_efficiency_data_raw = other.pumps_efficiency_data_raw
1499
1512
 
1500
1513
  self.__init()
1501
1514
 
@@ -1581,14 +1594,14 @@ class ScadaData(Serializable):
1581
1594
  other.bulk_species_link_concentration_raw),
1582
1595
  axis=0)
1583
1596
 
1584
- if self.__pump_energy_usage_data is not None:
1585
- self.__pump_energy_usage_data = np.concatenate(
1586
- (self.__pump_energy_usage_data, other.pump_energy_usage_data),
1597
+ if self.__pumps_energy_usage_data_raw is not None:
1598
+ self.__pumps_energy_usage_data_raw = np.concatenate(
1599
+ (self.__pumps_energy_usage_data_raw, other.pumps_energy_usage_data_raw),
1587
1600
  axis=0)
1588
1601
 
1589
- if self.__pump_efficiency_data is not None:
1590
- self.__pump_efficiency_data = np.concatenate(
1591
- (self.__pump_efficiency_data, other.pump_efficiency_data),
1602
+ if self.__pumps_efficiency_data_raw is not None:
1603
+ self.__pumps_efficiency_data_raw = np.concatenate(
1604
+ (self.__pumps_efficiency_data_raw, other.pumps_efficiency_data_raw),
1592
1605
  axis=0)
1593
1606
 
1594
1607
  def get_data(self) -> np.ndarray:
@@ -1609,6 +1622,8 @@ class ScadaData(Serializable):
1609
1622
  "nodes_quality": self.__node_quality_data_raw,
1610
1623
  "links_quality": self.__link_quality_data_raw,
1611
1624
  "pumps_state": self.__pumps_state_data_raw,
1625
+ "pumps_efficiency": self.__pumps_efficiency_data_raw,
1626
+ "pumps_energyconsumption": self.__pumps_energy_usage_data_raw,
1612
1627
  "valves_state": self.__valves_state_data_raw,
1613
1628
  "tanks_volume": self.__tanks_volume_data_raw,
1614
1629
  "bulk_species_node_concentrations": self.__bulk_species_node_concentration_raw,
@@ -1632,6 +1647,10 @@ class ScadaData(Serializable):
1632
1647
  data.append(self.__valves_state_data_raw)
1633
1648
  if self.__pumps_state_data_raw is not None:
1634
1649
  data.append(self.__pumps_state_data_raw)
1650
+ if self.__pumps_efficiency_data_raw is not None:
1651
+ data.append(self.__pumps_efficiency_data_raw)
1652
+ if self.__pumps_energy_usage_data_raw is not None:
1653
+ data.append(self.__pumps_energy_usage_data_raw)
1635
1654
  if self.__tanks_volume_data_raw is not None:
1636
1655
  data.append(self.__tanks_volume_data_raw)
1637
1656
  if self.__surface_species_concentration_raw is not None:
@@ -1896,6 +1915,85 @@ class ScadaData(Serializable):
1896
1915
  for s_id in sensor_locations]
1897
1916
  return self.__sensor_readings[:, idx]
1898
1917
 
1918
+ def get_data_pumps_efficiency(self, sensor_locations: list[str] = None) -> np.ndarray:
1919
+ """
1920
+ Gets the final pump efficiency sensor readings -- note that those might be subject to
1921
+ given sensor faults and sensor noise/uncertainty.
1922
+
1923
+ Parameters
1924
+ ----------
1925
+ sensor_locations : `list[str]`, optional
1926
+ Existing pump efficiency sensor locations for which the sensor readings are requested.
1927
+ If None, the readings from all pump efficiency sensors are returned.
1928
+
1929
+ The default is None.
1930
+
1931
+ Returns
1932
+ -------
1933
+ `numpy.ndarray`
1934
+ Pump efficiency sensor readings.
1935
+ """
1936
+ if self.__sensor_config.pump_efficiency_sensors == []:
1937
+ raise ValueError("No pump efficiency sensors set")
1938
+ if sensor_locations is not None:
1939
+ if not isinstance(sensor_locations, list):
1940
+ raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
1941
+ f"but not of '{type(sensor_locations)}'")
1942
+ if any(s_id not in self.__sensor_config.pump_efficiency_sensors
1943
+ for s_id in sensor_locations):
1944
+ raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
1945
+ "sensors in 'sensor_locations' must be set in the current " +
1946
+ "pump efficiency sensor configuration")
1947
+ else:
1948
+ sensor_locations = self.__sensor_config.pump_efficiency_sensors
1949
+
1950
+ if self.__sensor_readings is None:
1951
+ self.get_data()
1952
+
1953
+ idx = [self.__sensor_config.get_index_of_reading(pump_efficiency_sensor=s_id)
1954
+ for s_id in sensor_locations]
1955
+ return self.__sensor_readings[:, idx]
1956
+
1957
+ def get_data_pumps_energyconsumption(self, sensor_locations: list[str] = None) -> np.ndarray:
1958
+ """
1959
+ Gets the final pump energy consumption sensor readings -- note that those might be subject
1960
+ to given sensor faults and sensor noise/uncertainty.
1961
+
1962
+ Parameters
1963
+ ----------
1964
+ sensor_locations : `list[str]`, optional
1965
+ Existing pump energy consumption sensor locations for which
1966
+ the sensor readings are requested.
1967
+ If None, the readings from all pump energy consumption sensors are returned.
1968
+
1969
+ The default is None.
1970
+
1971
+ Returns
1972
+ -------
1973
+ `numpy.ndarray`
1974
+ Pump energy consumption sensor readings.
1975
+ """
1976
+ if self.__sensor_config.pump_energyconsumption_sensors == []:
1977
+ raise ValueError("No pump energy consumption sensors set")
1978
+ if sensor_locations is not None:
1979
+ if not isinstance(sensor_locations, list):
1980
+ raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
1981
+ f"but not of '{type(sensor_locations)}'")
1982
+ if any(s_id not in self.__sensor_config.pump_energyconsumption_sensors
1983
+ for s_id in sensor_locations):
1984
+ raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
1985
+ "sensors in 'sensor_locations' must be set in the current " +
1986
+ "pump efficiency sensor configuration")
1987
+ else:
1988
+ sensor_locations = self.__sensor_config.pump_energyconsumption_sensors
1989
+
1990
+ if self.__sensor_readings is None:
1991
+ self.get_data()
1992
+
1993
+ idx = [self.__sensor_config.get_index_of_reading(pump_energyconsumption_sensor=s_id)
1994
+ for s_id in sensor_locations]
1995
+ return self.__sensor_readings[:, idx]
1996
+
1899
1997
  def get_data_valves_state(self, sensor_locations: list[str] = None) -> np.ndarray:
1900
1998
  """
1901
1999
  Gets the final valve state sensor readings -- note that those might be subject to