1"""
2Module provides a class for storing and processing SCADA data.
3"""
4import warnings
5from typing import Callable, Any, Union
6from copy import deepcopy
7import numpy as np
8from scipy.sparse import bsr_array
9import matplotlib
10import pandas as pd
11from epanet_plus import EpanetConstants
12
13from ..sensor_config import SensorConfig, valid_sensor_types, \
14 SENSOR_TYPE_LINK_FLOW, SENSOR_TYPE_LINK_QUALITY, SENSOR_TYPE_NODE_DEMAND, \
15 SENSOR_TYPE_NODE_PRESSURE, SENSOR_TYPE_NODE_QUALITY, SENSOR_TYPE_PUMP_STATE, \
16 SENSOR_TYPE_PUMP_EFFICIENCY, SENSOR_TYPE_PUMP_ENERGYCONSUMPTION, \
17 SENSOR_TYPE_TANK_VOLUME, SENSOR_TYPE_VALVE_STATE, SENSOR_TYPE_NODE_BULK_SPECIES, \
18 SENSOR_TYPE_LINK_BULK_SPECIES, SENSOR_TYPE_SURFACE_SPECIES
19from ..events import SensorFault, SensorReadingAttack, SensorReadingEvent
20from ...uncertainty import SensorNoise
21from ...serialization import serializable, Serializable, SCADA_DATA_ID
22from ...topology import NetworkTopology, UNITS_USCUSTOM, UNITS_SIMETRIC
23from ...utils import plot_timeseries_data, _get_flow_convert_factor, \
24 _get_pressure_convert_factor, is_flowunit_simetric, massunit_to_str, flowunit_to_str,\
25 qualityunit_to_str, areaunit_to_str, pressureunit_to_str, \
26 MASS_UNIT_MG, MASS_UNIT_UG, TIME_UNIT_HRS, MASS_UNIT_MOL, MASS_UNIT_MMOL, \
27 AREA_UNIT_CM2, AREA_UNIT_FT2, AREA_UNIT_M2
28
29
[docs]
30@serializable(SCADA_DATA_ID, ".epytflow_scada_data")
31class ScadaData(Serializable):
32 """
33 Class for storing and processing SCADA data.
34
35 Parameters
36 ----------
37 sensor_config : :class:`~epyt_flow.simulation.sensor_config.SensorConfig`
38 Specifications of all sensors.
39 sensor_readings_time : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
40 Time (seconds since simulation start) for each sensor reading row
41 in `sensor_readings_data_raw`.
42
43 This parameter is expected to be a 1d array with the same size as
44 the number of rows in `sensor_readings_data_raw`.
45 network_topo : :class:`~epyt_flow.topology.NetworkTopology`
46 Topology of the water distribution network.
47 warnings_code : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
48 Codes/IDs of EPANET errors/warnings (if any) for each time step.
49 pressure_data_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
50 Raw pressure values of all nodes as a two-dimensional array --
51 first dimension encodes time, second dimension pressure at nodes.
52
53 The default is None,
54 flow_data_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
55 Raw flow values of all links/pipes --
56 first dimension encodes time, second dimension pressure at links/pipes.
57
58 The default is None.
59 demand_data_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
60 Raw demand values of all nodes --
61 first dimension encodes time, second dimension demand at nodes.
62
63 The default is None.
64 node_quality_data_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
65 Raw quality values of all nodes --
66 first dimension encodes time, second dimension quality at nodes.
67
68 The default is None.
69 link_quality_data_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
70 Raw quality values of all links/pipes --
71 first dimension encodes time, second dimension quality at links/pipes.
72
73 The default is None.
74 pumps_state_data_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
75 States of all pumps --
76 first dimension encodes time, second dimension states of pumps.
77
78 The default is None.
79 valves_state_data_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
80 States of all valves --
81 first dimension encodes time, second dimension states of valves.
82
83 The default is None.
84 tanks_volume_data_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
85 Water volumes in all tanks --
86 first dimension encodes time, second dimension water volume in tanks.
87
88 The default is None.
89 surface_species_concentration_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
90 Raw concentrations of surface species as a tree dimensional array --
91 first dimension encodes time, second dimension denotes the different surface species,
92 third dimension denotes species concentrations at links/pipes.
93
94 The default is None.
95 bulk_species_node_concentration_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
96 Raw concentrations of bulk species at nodes as a tree dimensional array --
97 first dimension encodes time, second dimension denotes the different bulk species,
98 third dimension denotes species concentrations at nodes.
99
100 The default is None.
101 bulk_species_link_concentration_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
102 Raw concentrations of bulk species at links as a tree dimensional array --
103 first dimension encodes time, second dimension denotes the different bulk species,
104 third dimension denotes species concentrations at nodes.
105
106 The default is None.
107 pumps_energy_usage_data_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
108 Energy usage data of each pump.
109
110 The default is None.
111 pumps_efficiency_data_raw : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, optional
112 Pump efficiency data of each pump.
113
114 The default is None.
115 sensor_faults : list[:class:`~epyt_flow.simulation.events.sensor_faults.SensorFault`], optional
116 List of sensor faults to be applied to the sensor readings.
117
118 The default is an empty list.
119 sensor_reading_attacks : list[:class:`~epyt_flow.simulation.events.sensor_reading_attack.SensorReadingAttack`], optional
120 List of sensor reading attacks to be applied to the sensor readings.
121
122 The default is an empty list.
123 sensor_reading_events : list[`:class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`], optional
124 List of additional sensor reading events that are to be applied to the sensor readings.
125
126 The default is an empty list.
127 sensor_noise : :class:`~epyt_flow.uncertainty.sensor_noise.SensorNoise`, optional
128 Specification of the sensor noise/uncertainty to be added to the sensor readings.
129
130 The default is None.
131 frozen_sensor_config : `bool`, optional
132 If True, the sensor config can not be changed and only the required sensor nodes/links
133 will be stored -- this usually leads to a significant reduction in memory consumption.
134
135 The default is False.
136 """
137 def __init__(self, sensor_config: SensorConfig, sensor_readings_time: np.ndarray,
138 network_topo: NetworkTopology, warnings_code: np.ndarray = None,
139 pressure_data_raw: Union[np.ndarray, bsr_array] = None,
140 flow_data_raw: Union[np.ndarray, bsr_array] = None,
141 demand_data_raw: Union[np.ndarray, bsr_array] = None,
142 node_quality_data_raw: Union[np.ndarray, bsr_array] = None,
143 link_quality_data_raw: Union[np.ndarray, bsr_array] = None,
144 pumps_state_data_raw: Union[np.ndarray, bsr_array] = None,
145 valves_state_data_raw: Union[np.ndarray, bsr_array] = None,
146 tanks_volume_data_raw: Union[np.ndarray, bsr_array] = None,
147 surface_species_concentration_raw: Union[np.ndarray, dict[int, bsr_array]] = None,
148 bulk_species_node_concentration_raw: Union[np.ndarray,
149 dict[int, bsr_array]] = None,
150 bulk_species_link_concentration_raw: Union[np.ndarray,
151 dict[int, bsr_array]] = None,
152 pumps_energy_usage_data_raw: np.ndarray = None,
153 pumps_efficiency_data_raw: np.ndarray = None,
154 sensor_faults: list[SensorFault] = [],
155 sensor_reading_attacks: list[SensorReadingAttack] = [],
156 sensor_reading_events: list[SensorReadingEvent] = [],
157 sensor_noise: SensorNoise = None, frozen_sensor_config: bool = False,
158 **kwds):
159 if not isinstance(network_topo, NetworkTopology):
160 raise TypeError("'network_topo' must be an instance of " +
161 "'epyt_flow.topology.NetworkTopology' but not " +
162 f"of '{type(network_topo)}'")
163 if not isinstance(sensor_config, SensorConfig):
164 raise TypeError("'sensor_config' must be an instance of " +
165 "'epyt_flow.simulation.SensorConfig' but not of " +
166 f"'{type(sensor_config)}'")
167 if not isinstance(sensor_readings_time, np.ndarray):
168 raise TypeError("'sensor_readings_time' must be an instance of 'numpy.ndarray' " +
169 f"but not of '{type(sensor_readings_time)}'")
170 if warnings_code is None:
171 warnings_code = np.array([0] * len(sensor_readings_time))
172 else:
173 if not isinstance(warnings_code, np.ndarray):
174 raise TypeError("'warnings_code' must be an instance of 'numpy.ndarray' " +
175 f"but not of '{type(warnings_code)}'")
176 if pressure_data_raw is not None:
177 if not isinstance(pressure_data_raw, np.ndarray) and \
178 not isinstance(pressure_data_raw, bsr_array):
179 raise TypeError("'pressure_data_raw' must be an instance of 'numpy.ndarray'" +
180 f" but not of '{type(pressure_data_raw)}'")
181 if isinstance(pressure_data_raw, bsr_array) and not frozen_sensor_config:
182 raise ValueError("'pressure_data_raw' can only be an instance of " +
183 "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
184 if flow_data_raw is not None:
185 if not isinstance(flow_data_raw, np.ndarray) and \
186 not isinstance(flow_data_raw, bsr_array):
187 raise TypeError("'flow_data_raw' must be an instance of 'numpy.ndarray' " +
188 f"but not of '{type(flow_data_raw)}'")
189 if isinstance(flow_data_raw, bsr_array) and not frozen_sensor_config:
190 raise ValueError("'flow_data_raw' can only be an instance of " +
191 "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
192 if demand_data_raw is not None:
193 if not isinstance(demand_data_raw, np.ndarray) and \
194 not isinstance(demand_data_raw, bsr_array):
195 raise TypeError("'demand_data_raw' must be an instance of 'numpy.ndarray' " +
196 f"but not of '{type(demand_data_raw)}'")
197 if isinstance(demand_data_raw, bsr_array) and not frozen_sensor_config:
198 raise ValueError("'demand_data_raw' can only be an instance of " +
199 "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
200 if node_quality_data_raw is not None:
201 if not isinstance(node_quality_data_raw, np.ndarray) and \
202 not isinstance(node_quality_data_raw, bsr_array):
203 raise TypeError("'node_quality_data_raw' must be an instance of 'numpy.ndarray'" +
204 f" but not of '{type(node_quality_data_raw)}'")
205 if isinstance(node_quality_data_raw, bsr_array) and not frozen_sensor_config:
206 raise ValueError("'node_quality_data_raw' can only be an instance of " +
207 "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
208 if link_quality_data_raw is not None:
209 if not isinstance(link_quality_data_raw, np.ndarray) and \
210 not isinstance(link_quality_data_raw, bsr_array):
211 raise TypeError("'link_quality_data_raw' must be an instance of 'numpy.ndarray'" +
212 f" but not of '{type(link_quality_data_raw)}'")
213 if isinstance(link_quality_data_raw, bsr_array) and not frozen_sensor_config:
214 raise ValueError("'link_quality_data_raw' can only be an instance of " +
215 "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
216 if pumps_state_data_raw is not None:
217 if not isinstance(pumps_state_data_raw, np.ndarray) and \
218 not isinstance(pumps_state_data_raw, bsr_array):
219 raise TypeError("'pumps_state_data_raw' must be an instance of 'numpy.ndarray' " +
220 f"but no of '{type(pumps_state_data_raw)}'")
221 if isinstance(pumps_state_data_raw, bsr_array) and not frozen_sensor_config:
222 raise ValueError("'pumps_state_data_raw' can only be an instance of " +
223 "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
224 if valves_state_data_raw is not None:
225 if not isinstance(valves_state_data_raw, np.ndarray) and \
226 not isinstance(valves_state_data_raw, bsr_array):
227 raise TypeError("'valves_state_data_raw' must be an instance of 'numpy.ndarray' " +
228 f"but no of '{type(valves_state_data_raw)}'")
229 if isinstance(valves_state_data_raw, bsr_array) and not frozen_sensor_config:
230 raise ValueError("'valves_state_data_raw' can only be an instance of " +
231 "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
232 if tanks_volume_data_raw is not None:
233 if not isinstance(tanks_volume_data_raw, np.ndarray) and \
234 not isinstance(tanks_volume_data_raw, bsr_array):
235 raise TypeError("'tanks_volume_data_raw' must be an instance of 'numpy.ndarray'" +
236 f" but not of '{type(tanks_volume_data_raw)}'")
237 if isinstance(tanks_volume_data_raw, bsr_array) and not frozen_sensor_config:
238 raise ValueError("'tanks_volume_data_raw' can only be an instance of " +
239 "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
240 if sensor_faults is None or not isinstance(sensor_faults, list):
241 raise TypeError("'sensor_faults' must be a list of " +
242 "'epyt_flow.simulation.events.SensorFault' instances but " +
243 f"'{type(sensor_faults)}'")
244 if surface_species_concentration_raw is not None:
245 if not isinstance(surface_species_concentration_raw, np.ndarray) and \
246 not isinstance(surface_species_concentration_raw, dict):
247 raise TypeError("'surface_species_concentration_raw' must be an instance of " +
248 "'numpy.ndarray' but not of " +
249 f"'{type(surface_species_concentration_raw)}'")
250 if isinstance(surface_species_concentration_raw, dict) and not frozen_sensor_config:
251 raise TypeError("'surface_species_concentration_raw' can only be an instance of " +
252 "'dict' if 'frozen_sensor_config=True'")
253 if bulk_species_node_concentration_raw is not None:
254 if not isinstance(bulk_species_node_concentration_raw, np.ndarray) and \
255 not isinstance(bulk_species_node_concentration_raw, dict):
256 raise TypeError("'bulk_species_node_concentration_raw' must be an instance of " +
257 "'numpy.ndarray' but not of " +
258 f"'{type(bulk_species_node_concentration_raw)}'")
259 if isinstance(bulk_species_node_concentration_raw, dict) and not frozen_sensor_config:
260 raise TypeError("'bulk_species_node_concentration_raw' can only be an instance of " +
261 "'dict' if 'frozen_sensor_config=True'")
262 if bulk_species_link_concentration_raw is not None:
263 if not isinstance(bulk_species_link_concentration_raw, np.ndarray) and \
264 not isinstance(bulk_species_link_concentration_raw, dict):
265 raise TypeError("'bulk_species_link_concentration_raw' must be an instance of " +
266 "'numpy.ndarray' but not of " +
267 f"'{type(bulk_species_link_concentration_raw)}'")
268 if isinstance(bulk_species_link_concentration_raw, dict) and not frozen_sensor_config:
269 raise TypeError("'bulk_species_link_concentration_raw' can only be an instance of " +
270 "'dict' if 'frozen_sensor_config=True'")
271 if pumps_energy_usage_data_raw is not None:
272 if not isinstance(pumps_energy_usage_data_raw, np.ndarray) and \
273 not isinstance(pumps_energy_usage_data_raw, bsr_array):
274 raise TypeError("'pumps_energy_usage_data_raw' must be an instance of 'numpy.ndarray' " +
275 f"but not of '{type(pumps_energy_usage_data_raw)}'")
276 if isinstance(pumps_energy_usage_data_raw, bsr_array) and not frozen_sensor_config:
277 raise ValueError("'pumps_energy_usage_data_raw' can only be an instance of " +
278 "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
279 if pumps_efficiency_data_raw is not None:
280 if not isinstance(pumps_efficiency_data_raw, np.ndarray) and \
281 not isinstance(pumps_efficiency_data_raw, bsr_array):
282 raise TypeError("'pumps_efficiency_data_raw' must be an instance of 'numpy.ndarray' " +
283 f"but not of '{type(pumps_efficiency_data_raw)}'")
284 if isinstance(pumps_efficiency_data_raw, bsr_array) and not frozen_sensor_config:
285 raise ValueError("'pumps_efficiency_data_raw' can only be an instance of " +
286 "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
287 if len(sensor_faults) != 0:
288 if any(not isinstance(f, SensorFault) for f in sensor_faults):
289 raise TypeError("'sensor_faults' must be a list of " +
290 "'epyt_flow.simulation.event.SensorFault' instances")
291 if len(sensor_reading_attacks) != 0:
292 if any(not isinstance(f, SensorReadingAttack) for f in sensor_reading_attacks):
293 raise TypeError("'sensor_reading_attacks' must be a list of " +
294 "'epyt_flow.simulation.event.SensorReadingAttack' instances")
295 if len(sensor_reading_events) != 0:
296 if any(not isinstance(f, SensorReadingEvent) for f in sensor_reading_events):
297 raise TypeError("'sensor_reading_events' must be a list of " +
298 "'epyt_flow.simulation.event.SensorReadingEvent' instances")
299 if sensor_noise is not None and not isinstance(sensor_noise, SensorNoise):
300 raise TypeError("'sensor_noise' must be an instance of " +
301 "'epyt_flow.uncertainty.SensorNoise' but not of " +
302 f"'{type(sensor_noise)}'")
303 if not isinstance(frozen_sensor_config, bool):
304 raise TypeError("'frozen_sensor_config' must be an instance of 'bool' " +
305 f"but not of '{type(frozen_sensor_config)}'")
306
307 def __raise_shape_mismatch(var_name: str) -> None:
308 raise ValueError(f"Shape mismatch in '{var_name}' -- " +
309 "i.e number of time steps in 'sensor_readings_time' " +
310 "must match number of raw measurements.")
311
312 n_time_steps = sensor_readings_time.shape[0]
313 if warnings_code is not None:
314 if warnings_code.shape[0] != n_time_steps:
315 __raise_shape_mismatch("warnings_code")
316 if pressure_data_raw is not None:
317 if pressure_data_raw.shape[0] != n_time_steps:
318 __raise_shape_mismatch("pressure_data_raw")
319 if flow_data_raw is not None:
320 if flow_data_raw.shape[0] != n_time_steps:
321 __raise_shape_mismatch("flow_data_raw")
322 if demand_data_raw is not None:
323 if demand_data_raw.shape[0] != n_time_steps:
324 __raise_shape_mismatch("demand_data_raw")
325 if node_quality_data_raw is not None:
326 if node_quality_data_raw.shape[0] != n_time_steps:
327 __raise_shape_mismatch("node_quality_data_raw")
328 if link_quality_data_raw is not None:
329 if link_quality_data_raw.shape[0] != n_time_steps:
330 __raise_shape_mismatch("link_quality_data_raw")
331 if valves_state_data_raw is not None:
332 if valves_state_data_raw.shape[0] != n_time_steps:
333 __raise_shape_mismatch("valves_state_data_raw")
334 if pumps_state_data_raw is not None:
335 if pumps_state_data_raw.shape[0] != n_time_steps:
336 __raise_shape_mismatch("pumps_state_data_raw")
337 if tanks_volume_data_raw is not None:
338 if tanks_volume_data_raw.shape[0] != n_time_steps:
339 __raise_shape_mismatch("tanks_volume_data_raw")
340 if valves_state_data_raw is not None:
341 if not valves_state_data_raw.shape[0] == n_time_steps:
342 __raise_shape_mismatch("valves_state_data_raw")
343 if pumps_state_data_raw is not None:
344 if not pumps_state_data_raw.shape[0] == n_time_steps:
345 __raise_shape_mismatch("pumps_state_data_raw")
346 if tanks_volume_data_raw is not None:
347 if not tanks_volume_data_raw.shape[0] == n_time_steps:
348 __raise_shape_mismatch("tanks_volume_data_raw")
349 if bulk_species_node_concentration_raw is not None:
350 if isinstance(bulk_species_node_concentration_raw, np.ndarray):
351 if bulk_species_node_concentration_raw.shape[0] != n_time_steps:
352 __raise_shape_mismatch("bulk_species_node_concentration_raw")
353 if bulk_species_link_concentration_raw is not None:
354 if isinstance(bulk_species_link_concentration_raw, np.ndarray):
355 if bulk_species_link_concentration_raw.shape[0] != n_time_steps:
356 __raise_shape_mismatch("bulk_species_link_concentration_raw")
357 if surface_species_concentration_raw is not None:
358 if isinstance(surface_species_concentration_raw, np.ndarray):
359 if surface_species_concentration_raw.shape[0] != n_time_steps:
360 __raise_shape_mismatch("surface_species_concentration_raw")
361 if pumps_energy_usage_data_raw is not None:
362 if pumps_energy_usage_data_raw.shape[0] != n_time_steps:
363 __raise_shape_mismatch("pumps_energy_usage_data_raw")
364 if pumps_efficiency_data_raw is not None:
365 if pumps_efficiency_data_raw.shape[0] != n_time_steps:
366 __raise_shape_mismatch("pumps_efficiency_data_raw")
367
368 self.__network_topo = network_topo
369 self.__sensor_config = sensor_config
370 self.__warnings_code = warnings_code
371 self.__sensor_noise = sensor_noise
372 self.__sensor_reading_events = sensor_faults + sensor_reading_attacks + \
373 sensor_reading_events
374
375 self.__sensor_readings = None
376 self.__frozen_sensor_config = frozen_sensor_config
377 self.__sensor_readings_time = sensor_readings_time
378 self.__sensor_readings_time_to_idx = {time: idx for idx, time in
379 enumerate(self.__sensor_readings_time)}
380
381 if self.__frozen_sensor_config is False:
382 self.__pressure_data_raw = pressure_data_raw
383 self.__flow_data_raw = flow_data_raw
384 self.__demand_data_raw = demand_data_raw
385 self.__node_quality_data_raw = node_quality_data_raw
386 self.__link_quality_data_raw = link_quality_data_raw
387 self.__pumps_state_data_raw = pumps_state_data_raw
388 self.__valves_state_data_raw = valves_state_data_raw
389 self.__tanks_volume_data_raw = tanks_volume_data_raw
390 self.__surface_species_concentration_raw = surface_species_concentration_raw
391 self.__bulk_species_node_concentration_raw = bulk_species_node_concentration_raw
392 self.__bulk_species_link_concentration_raw = bulk_species_link_concentration_raw
393 self.__pumps_energy_usage_data_raw = pumps_energy_usage_data_raw
394 self.__pumps_efficiency_data_raw = pumps_efficiency_data_raw
395 else:
396 sensor_config = self.__sensor_config
397
398 node_to_idx = sensor_config.map_node_id_to_idx
399 link_to_idx = sensor_config.map_link_id_to_idx
400 pump_to_idx = sensor_config.map_pump_id_to_idx
401 valve_to_idx = sensor_config.map_valve_id_to_idx
402 tank_to_idx = sensor_config.map_tank_id_to_idx
403
404 # EPANET quantities
405 def __reduce_data(data: np.ndarray, sensors: list[str],
406 item_to_idx: Callable[[str], int]) -> np.ndarray:
407 idx = [item_to_idx(item_id) for item_id in sensors]
408
409 if data is None or len(idx) == 0:
410 return None
411 else:
412 return data[:, idx]
413
414 if isinstance(pressure_data_raw, bsr_array):
415 pressure_data_raw = pressure_data_raw.todense()
416 self.__pressure_data_raw = __reduce_data(data=pressure_data_raw,
417 item_to_idx=node_to_idx,
418 sensors=sensor_config.pressure_sensors)
419
420 if isinstance(flow_data_raw, bsr_array):
421 flow_data_raw = flow_data_raw.todense()
422 self.__flow_data_raw = __reduce_data(data=flow_data_raw,
423 item_to_idx=link_to_idx,
424 sensors=sensor_config.flow_sensors)
425
426 if isinstance(demand_data_raw, bsr_array):
427 demand_data_raw = demand_data_raw.todense()
428 self.__demand_data_raw = __reduce_data(data=demand_data_raw,
429 item_to_idx=node_to_idx,
430 sensors=sensor_config.demand_sensors)
431
432 if isinstance(node_quality_data_raw, bsr_array):
433 node_quality_data_raw = node_quality_data_raw.todense()
434 self.__node_quality_data_raw = __reduce_data(data=node_quality_data_raw,
435 item_to_idx=node_to_idx,
436 sensors=sensor_config.quality_node_sensors)
437
438 if isinstance(link_quality_data_raw, bsr_array):
439 link_quality_data_raw = link_quality_data_raw.todense()
440 self.__link_quality_data_raw = __reduce_data(data=link_quality_data_raw,
441 item_to_idx=link_to_idx,
442 sensors=sensor_config.quality_link_sensors)
443
444 if isinstance(pumps_state_data_raw, bsr_array):
445 pumps_state_data_raw = pumps_state_data_raw.todense()
446 self.__pumps_state_data_raw = __reduce_data(data=pumps_state_data_raw,
447 item_to_idx=pump_to_idx,
448 sensors=sensor_config.pump_state_sensors)
449
450 if isinstance(pumps_energy_usage_data_raw, bsr_array):
451 pumps_energy_usage_data_raw = pumps_energy_usage_data_raw.todense()
452 self.__pumps_energy_usage_data_raw = \
453 __reduce_data(data=pumps_energy_usage_data_raw,
454 item_to_idx=pump_to_idx,
455 sensors=sensor_config.pump_energyconsumption_sensors)
456
457 if isinstance(pumps_efficiency_data_raw, bsr_array):
458 pumps_efficiency_data_raw = pumps_efficiency_data_raw.todense()
459 self.__pumps_efficiency_data_raw = \
460 __reduce_data(data=pumps_efficiency_data_raw,
461 item_to_idx=pump_to_idx,
462 sensors=sensor_config.pump_efficiency_sensors)
463
464 if isinstance(valves_state_data_raw, bsr_array):
465 valves_state_data_raw = valves_state_data_raw.todense()
466 self.__valves_state_data_raw = __reduce_data(data=valves_state_data_raw,
467 item_to_idx=valve_to_idx,
468 sensors=sensor_config.valve_state_sensors)
469
470 if isinstance(tanks_volume_data_raw, bsr_array):
471 tanks_volume_data_raw = tanks_volume_data_raw.todense()
472 self.__tanks_volume_data_raw = __reduce_data(data=tanks_volume_data_raw,
473 item_to_idx=tank_to_idx,
474 sensors=sensor_config.tank_volume_sensors)
475
476 # EPANET-MSX quantities
477 def __reduce_msx_data(data: np.ndarray, sensors: list[tuple[list[int], list[int]]]
478 ) -> np.ndarray:
479 if data is None or len(sensors) == 0:
480 return None
481 else:
482 r = []
483 for species_idx, item_idx in sensors:
484 r.append(data[:, species_idx, item_idx].reshape(-1, len(item_idx)))
485
486 return np.concatenate(r, axis=1)
487
488 def __reduce_msx_dict_data(data: dict, species_senors: dict[str, list[str]],
489 map_sensor_id_to_idx: Callable[str, int]) -> np.ndarray:
490 r = []
491
492 for species_id in data:
493 data_ = data[species_id].todense()
494 for sensor_id in species_senors[species_id]:
495 data_idx = map_sensor_id_to_idx(sensor_id)
496 r.append(data_[:, data_idx].reshape(-1, 1))
497
498 return np.concatenate(r, axis=1)
499
500 if isinstance(bulk_species_node_concentration_raw, dict):
501 self.__bulk_species_node_concentration_raw = __reduce_msx_dict_data(
502 bulk_species_node_concentration_raw, sensor_config.bulk_species_node_sensors,
503 sensor_config.map_node_id_to_idx)
504 else:
505 node_bulk_species_idx = [(sensor_config.map_bulkspecies_id_to_idx(s),
506 [sensor_config.map_node_id_to_idx(node_id)
507 for node_id in sensor_config.bulk_species_node_sensors[s]])
508 for s in sensor_config.bulk_species_node_sensors.keys()]
509 self.__bulk_species_node_concentration_raw = \
510 __reduce_msx_data(data=bulk_species_node_concentration_raw,
511 sensors=node_bulk_species_idx)
512
513 if isinstance(bulk_species_link_concentration_raw, dict):
514 self.__bulk_species_link_concentration_raw = __reduce_msx_dict_data(
515 bulk_species_link_concentration_raw, sensor_config.bulk_species_link_sensors,
516 sensor_config.map_link_id_to_idx)
517 else:
518 bulk_species_link_idx = [(sensor_config.map_bulkspecies_id_to_idx(s),
519 [sensor_config.map_link_id_to_idx(link_id)
520 for link_id in sensor_config.bulk_species_link_sensors[s]])
521 for s in sensor_config.bulk_species_link_sensors.keys()]
522 self.__bulk_species_link_concentration_raw = \
523 __reduce_msx_data(data=bulk_species_link_concentration_raw,
524 sensors=bulk_species_link_idx)
525
526 if isinstance(surface_species_concentration_raw, dict):
527 self.__surface_species_concentration_raw = __reduce_msx_dict_data(
528 surface_species_concentration_raw, sensor_config.surface_species_sensors,
529 sensor_config.map_link_id_to_idx)
530 else:
531 surface_species_idx = [(sensor_config.map_surfacespecies_id_to_idx(s),
532 [sensor_config.map_link_id_to_idx(link_id)
533 for link_id in sensor_config.surface_species_sensors[s]])
534 for s in sensor_config.surface_species_sensors.keys()]
535 self.__surface_species_concentration_raw = \
536 __reduce_msx_data(data=surface_species_concentration_raw,
537 sensors=surface_species_idx)
538
539 self.__init()
540
541 super().__init__(**kwds)
542
[docs]
543 def convert_units(self, flow_unit: int = None, pressure_unit: int = None,
544 quality_unit: int = None,
545 bulk_species_mass_unit: list[int] = None,
546 surface_species_mass_unit: list[int] = None,
547 surface_species_area_unit: int = None) -> Any:
548 """
549 Changes the units of some measurement units.
550
551 .. note::
552
553 Beaware of potential rounding errors.
554
555 Parameters
556 ----------
557 flow_unit : `int`, optional
558 New (flow) units of hydraulic measurements -- note that the flow unit
559 specifies all other hydraulic measurement units, except pressure.
560
561 Must be one of the following EPANET constants:
562
563 - EN_CFS = 0 (cubic foot/sec)
564 - EN_GPM = 1 (gal/min)
565 - EN_MGD = 2 (Million gal/day)
566 - EN_IMGD = 3 (Imperial MGD)
567 - EN_AFD = 4 (ac-foot/day)
568 - EN_LPS = 5 (liter/sec)
569 - EN_LPM = 6 (liter/min)
570 - EN_MLD = 7 (Megaliter/day)
571 - EN_CMH = 8 (cubic meter/hr)
572 - EN_CMD = 9 (cubic meter/day)
573 - EN_CMS = 10 (cubic meter/sec)
574
575 If None, units of dependent hydraulic measurement are not changed.
576
577 The default is None.
578 pressure_unit : `int`, optional
579 New pressure units of hydraulic measurementsO
580
581 Must be one of the following EPANET constants:
582
583 - EN_PSI = 0 (Pounds per square inch)
584 - EN_KPA = 1 (Kilopascals)
585 - EN_METERS = 2 (Meters)
586 - EN_BAR = 3 (Bar)
587 - EN_FEET = 4 (Feet)
588
589 The default is None.
590 quality_unit : `int`, optional
591 New unit of quality measurements -- i.e. chemical concentration.
592 Only relevant if basic quality analysis was performed.
593
594 Must be one of the following constants:
595
596 - MASS_UNIT_MG = 4 (mg/L)
597 - MASS_UNIT_UG = 5 (ug/L)
598
599 If None, units of quality measurements are not changed.
600
601 The default is None.
602 bulk_species_mass_unit : `list[int]`, optional
603 New units of all bulk species measurements -- i.e. for each
604 bulk species the measurement unit is specified.
605 Note that the assumed ordering is the same as given in 'bulk_species'
606 in the sensor configuration -- only relevant if EPANET-MSX is used.
607
608 Must be one of the following constants:
609
610 - MASS_UNIT_MG = 4 (milligram)
611 - MASS_UNIT_UG = 5 (microgram)
612 - MASS_UNIT_MOL = 6 (mole)
613 - MASS_UNIT_MMOL = 7 (millimole)
614
615 If None, measurement units of bulk species are not changed.
616
617 The default is None.
618 surface_species_mass_unit : `list[int]`, optional
619 New units of all surface species measurements -- i.e. for each
620 surface species the measurement unit is specified.
621 Note that the assumed ordering is the same as given in 'surface_species'
622 in the sensor configuration -- only relevant if EPANET-MSX is used.
623
624 Must be one of the following constants:
625
626 - MASS_UNIT_MG = 4 (milligram)
627 - MASS_UNIT_UG = 5 (microgram)
628 - MASS_UNIT_MOL = 6 (mole)
629 - MASS_UNIT_MMOL = 7 (millimole)
630
631 If None, measurement units of surface species are not changed.
632
633 The default is None.
634 surface_species_area_unit : `int`, optional
635 New area unit of all surface species -- only relevant if EPANET-MSX is used.
636
637 Must be one of the following constants:
638
639 - AREA_UNIT_FT2 = 1 (square feet)
640 - AREA_UNIT_M2 = 2 (square meters)
641 - AREA_UNIT_CM2 = 3 (square centimeters)
642
643 If None, are units of surface species are not changed.
644
645 The default is None.
646
647 Returns
648 -------
649 :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
650 SCADA data instance with the new units.
651 """
652 if flow_unit is not None:
653 if not isinstance(flow_unit, int):
654 raise TypeError("'flow_unit' must be a an instance of 'int' " +
655 f"but not of '{type(flow_unit)}'")
656 if flow_unit not in range(11):
657 raise ValueError("Invalid value of 'flow_unit'")
658
659 if pressure_unit is not None:
660 if not isinstance(pressure_unit, int):
661 raise TypeError("'pressure_unit' must be a an instance of 'int' " +
662 f"but not of '{type(pressure_unit)}'")
663 if pressure_unit not in range(5):
664 raise ValueError("Invalid value of 'pressure_unit'")
665
666 if quality_unit is not None:
667 if not isinstance(quality_unit, int):
668 raise TypeError("'quality_mass_unit' must be an instance of 'int' " +
669 f"but not of '{type(quality_unit)}'")
670 if quality_unit not in [MASS_UNIT_MG, MASS_UNIT_UG, TIME_UNIT_HRS]:
671 raise ValueError("Invalid value of 'quality_unit'")
672
673 if bulk_species_mass_unit is not None:
674 if not isinstance(bulk_species_mass_unit, list):
675 raise TypeError("'bulk_species_mass_unit' must be an instance of 'list[int]' " +
676 f"but not of '{type(bulk_species_mass_unit)}'")
677 if len(bulk_species_mass_unit) != len(self.__sensor_config.bulk_species):
678 raise ValueError("Inconsistency between 'bulk_species_mass_unit' and " +
679 "'bulk_species'")
680 if any(not isinstance(mass_unit, int) for mass_unit in bulk_species_mass_unit):
681 raise TypeError("All items in 'bulk_species_mass_unit' must be an instance " +
682 "of 'int'")
683 if any(mass_unit not in [MASS_UNIT_MG, MASS_UNIT_UG, MASS_UNIT_MOL, MASS_UNIT_MMOL]
684 for mass_unit in bulk_species_mass_unit):
685 raise ValueError("Invalid mass unit in 'bulk_species_mass_unit'")
686
687 if surface_species_mass_unit is not None:
688 if not isinstance(surface_species_mass_unit, list):
689 raise TypeError("'surface_species_mass_unit' must be an instance of 'list[int]' " +
690 f"but not of '{type(surface_species_mass_unit)}'")
691 if len(surface_species_mass_unit) != len(self.__sensor_config.surface_species):
692 raise ValueError("Inconsistency between 'surface_species_mass_unit' and " +
693 "'surface_species'")
694 if any(not isinstance(mass_unit, int) for mass_unit in surface_species_mass_unit):
695 raise TypeError("All items in 'surface_species_mass_unit' must be an instance " +
696 "of 'int'")
697 if any(mass_unit not in [MASS_UNIT_MG, MASS_UNIT_UG, MASS_UNIT_MOL, MASS_UNIT_MMOL]
698 for mass_unit in surface_species_mass_unit):
699 raise ValueError("Invalid mass unit in 'surface_species_mass_unit'")
700
701 if surface_species_area_unit is not None:
702 if surface_species_area_unit is not None:
703 if not isinstance(surface_species_area_unit, int):
704 raise TypeError("'surface_species_area_unit' must be a an instance of 'int' " +
705 f"but not of '{type(surface_species_area_unit)}'")
706 if surface_species_area_unit not in [AREA_UNIT_FT2, AREA_UNIT_M2, AREA_UNIT_CM2]:
707 raise ValueError("Invalid area unit 'surface_species_area_unit'")
708
709 def __get_mass_convert_factor(new_unit_id: int, old_unit_id: int) -> float:
710 if new_unit_id == MASS_UNIT_MG and old_unit_id == MASS_UNIT_UG:
711 return .001
712 elif new_unit_id == MASS_UNIT_UG and old_unit_id == MASS_UNIT_MG:
713 return 1000.
714 elif new_unit_id == MASS_UNIT_MOL and old_unit_id == MASS_UNIT_MMOL:
715 return .001
716 elif new_unit_id == MASS_UNIT_MMOL and old_unit_id == MASS_UNIT_MOL:
717 return 1000.
718 else:
719 raise NotImplementedError(f"Can not convert '{massunit_to_str(old_unit_id)}' to " +
720 f"'{massunit_to_str(new_unit_id)}'")
721
722 # Convert units
723 attributes = self.get_attributes()
724
725 pressure_data = attributes["pressure_data_raw"]
726 flow_data = attributes["flow_data_raw"]
727 demand_data = attributes["demand_data_raw"]
728 quality_node_data = attributes["node_quality_data_raw"]
729 quality_link_data = attributes["link_quality_data_raw"]
730 tanks_volume_data = attributes["tanks_volume_data_raw"]
731 surface_species_concentrations = attributes["surface_species_concentration_raw"]
732 bulk_species_node_concentrations = attributes["bulk_species_node_concentration_raw"]
733 bulk_species_link_concentrations = attributes["bulk_species_link_concentration_raw"]
734
735 if pressure_unit is not None:
736 old_pressure_unit = self.__sensor_config.pressure_unit
737 if pressure_unit == old_pressure_unit:
738 warnings.warn("'pressure_unit' is identical to the current pressure units " +
739 "-- nothing to do!", UserWarning)
740 else:
741 convert_factor = _get_pressure_convert_factor(pressure_unit, old_pressure_unit)
742 pressure_data *= convert_factor
743
744 if flow_unit is not None:
745 old_flow_unit = self.__sensor_config.flow_unit
746 if flow_unit == old_flow_unit:
747 warnings.warn("'flow_unit' is identical to the current flow units " +
748 "-- nothing to do!", UserWarning)
749 else:
750 # Convert flows and demands
751 convert_factor = _get_flow_convert_factor(flow_unit, old_flow_unit)
752
753 flow_data *= convert_factor
754 demand_data *= convert_factor
755
756 if is_flowunit_simetric(flow_unit) != is_flowunit_simetric(old_flow_unit):
757 # Convert tank volume
758 convert_factor = None
759 if is_flowunit_simetric(flow_unit) is True and \
760 is_flowunit_simetric(old_flow_unit) is False:
761 convert_factor_volume = .0283168
762 else:
763 convert_factor_volume = 35.3147
764
765 if tanks_volume_data is not None:
766 tanks_volume_data *= convert_factor_volume
767
768 if quality_unit is not None:
769 old_quality_unit = self.__sensor_config.quality_unit()
770 if quality_unit == old_quality_unit:
771 warnings.warn("'quality_unit' are identical to the current quality units " +
772 "-- nothing to do!", UserWarning)
773 else:
774 # Convert chemical concentration and time (basic quality analysis)
775 if quality_unit != TIME_UNIT_HRS:
776 convert_factor = __get_mass_convert_factor(quality_unit, old_quality_unit)
777
778 quality_node_data *= convert_factor
779 quality_link_data *= convert_factor
780
781 if bulk_species_mass_unit is not None:
782 # Convert bulk species concentrations
783 if self.__frozen_sensor_config is True:
784 for i, species_id in enumerate(self.__sensor_config.bulk_species_node_sensors):
785 species_idx = self.__sensor_config.bulk_species.index(species_id)
786 new_mass_unit = bulk_species_mass_unit[species_idx]
787 old_mass_unit = self.__sensor_config.bulk_species_mass_unit[species_idx]
788
789 if new_mass_unit != old_mass_unit:
790 convert_factor = __get_mass_convert_factor(new_mass_unit, old_mass_unit)
791 bulk_species_node_concentrations[species_id] *= convert_factor
792
793 for i, species_id, in enumerate(self.__sensor_config.bulk_species_link_sensors):
794 species_idx = self.__sensor_config.bulk_species.index(species_id)
795 new_mass_unit = bulk_species_mass_unit[species_idx]
796 old_mass_unit = self.__sensor_config.bulk_species_mass_unit[species_idx]
797
798 if new_mass_unit != old_mass_unit:
799 convert_factor = __get_mass_convert_factor(new_mass_unit, old_mass_unit)
800 bulk_species_link_concentrations[species_id] *= convert_factor
801 else:
802 for i in range(bulk_species_node_concentrations.shape[1]):
803 if bulk_species_mass_unit[i] != self.__sensor_config.bulk_species_mass_unit[i]:
804 old_mass_unit = self.__sensor_config.bulk_species_mass_unit[i]
805 convert_factor = __get_mass_convert_factor(bulk_species_mass_unit[i],
806 old_mass_unit)
807
808 bulk_species_node_concentrations[:, i, :] *= convert_factor
809 bulk_species_link_concentrations[:, i, :] *= convert_factor
810
811 if surface_species_mass_unit is not None:
812 # Convert surface species concentrations
813 if self.__frozen_sensor_config is True:
814 for i, species_id, _ in enumerate(self.__sensor_config.surface_species_sensors):
815 species_idx = self.__sensor_config.surface_species.index(species_id)
816 new_mass_unit = surface_species_mass_unit[species_idx]
817 old_mass_unit = self.__sensor_config.surface_species_mass_unit[species_idx]
818
819 if new_mass_unit != old_mass_unit:
820 convert_factor = __get_mass_convert_factor(new_mass_unit, old_mass_unit)
821 surface_species_concentrations[species_id] *= convert_factor
822 else:
823 for i in range(surface_species_concentrations.shape[1]):
824 old_mass_unit = self.__sensor_config.surface_species_mass_unit[i]
825 if surface_species_mass_unit[i] != old_mass_unit:
826 convert_factor = __get_mass_convert_factor(surface_species_mass_unit[i],
827 old_mass_unit)
828
829 surface_species_concentrations[:, i, :] *= convert_factor
830
831 # Create new SCADA data instance
832 new_flow_unit = self.__sensor_config.flow_unit
833 if flow_unit is not None:
834 new_flow_unit = flow_unit
835
836 new_pressure_unit = self.__sensor_config.pressure_unit
837 if pressure_unit is not None:
838 new_pressure_unit = pressure_unit
839
840 new_quality_unit = self.__sensor_config.quality_unit
841 if quality_unit is not None:
842 new_quality_unit = quality_unit
843
844 new_bulk_species_mass_unit = self.__sensor_config.bulk_species_mass_unit
845 if bulk_species_mass_unit is not None:
846 new_bulk_species_mass_unit = bulk_species_mass_unit
847
848 new_surface_species_mass_unit = self.__sensor_config.surface_species_mass_unit
849 if surface_species_mass_unit is not None:
850 new_surface_species_mass_unit = surface_species_mass_unit
851
852 new_surface_species_area_unit = self.__sensor_config.surface_species_area_unit
853 if surface_species_area_unit is not None:
854 new_surface_species_mass_unit = surface_species_area_unit
855
856 sensor_config = SensorConfig(nodes=self.__sensor_config.nodes,
857 links=self.__sensor_config.links,
858 valves=self.__sensor_config.valves,
859 pumps=self.__sensor_config.pumps,
860 tanks=self.__sensor_config.tanks,
861 bulk_species=self.__sensor_config.bulk_species,
862 surface_species=self.__sensor_config.surface_species,
863 node_id_to_idx=self.__sensor_config.node_id_to_idx,
864 link_id_to_idx=self.__sensor_config.link_id_to_idx,
865 valve_id_to_idx=self.__sensor_config.valve_id_to_idx,
866 pump_id_to_idx=self.__sensor_config.pump_id_to_idx,
867 tank_id_to_idx=self.__sensor_config.tank_id_to_idx,
868 bulkspecies_id_to_idx=self.__sensor_config.
869 bulkspecies_id_to_idx,
870 surfacespecies_id_to_idx=self.__sensor_config.
871 surfacespecies_id_to_idx,
872 flow_unit=new_flow_unit,
873 pressure_unit=new_pressure_unit,
874 pressure_sensors=self.__sensor_config.pressure_sensors,
875 flow_sensors=self.__sensor_config.flow_sensors,
876 demand_sensors=self.__sensor_config.demand_sensors,
877 quality_node_sensors=self.__sensor_config.quality_node_sensors,
878 quality_link_sensors=self.__sensor_config.quality_link_sensors,
879 valve_state_sensors=self.__sensor_config.valve_state_sensors,
880 pump_state_sensors=self.__sensor_config.pump_state_sensors,
881 pump_efficiency_sensors=
882 self.__sensor_config.pump_efficiency_sensors,
883 pump_energyconsumption_sensors=
884 self.__sensor_config.pump_energyconsumption_sensors,
885 tank_volume_sensors=self.__sensor_config.tank_volume_sensors,
886 bulk_species_node_sensors=
887 self.__sensor_config.bulk_species_node_sensors,
888 bulk_species_link_sensors=
889 self.__sensor_config.bulk_species_link_sensors,
890 surface_species_sensors=
891 self.__sensor_config.surface_species_sensors,
892 quality_unit=new_quality_unit,
893 bulk_species_mass_unit=new_bulk_species_mass_unit,
894 surface_species_mass_unit=new_surface_species_mass_unit,
895 surface_species_area_unit=new_surface_species_area_unit)
896
897 if flow_unit is not None or pressure_unit is not None:
898 network_topo = self.network_topo.convert_units(new_flow_unit, new_pressure_unit)
899 else:
900 network_topo = self.network_topo
901
902 return ScadaData(network_topo=network_topo,
903 warnings_code=self.warnings_code,
904 sensor_config=sensor_config,
905 sensor_readings_time=self.sensor_readings_time,
906 sensor_reading_events=self.sensor_reading_events,
907 sensor_noise=self.sensor_noise,
908 frozen_sensor_config=self.frozen_sensor_config,
909 pressure_data_raw=pressure_data,
910 flow_data_raw=flow_data,
911 demand_data_raw=demand_data,
912 node_quality_data_raw=quality_node_data,
913 link_quality_data_raw=quality_link_data,
914 pumps_state_data_raw=self.pumps_state_data_raw,
915 valves_state_data_raw=self.valves_state_data_raw,
916 tanks_volume_data_raw=tanks_volume_data,
917 pumps_energy_usage_data_raw=self.pumps_energyconsumption_data_raw,
918 pumps_efficiency_data_raw=self.pumps_efficiency_data_raw,
919 bulk_species_node_concentration_raw=bulk_species_node_concentrations,
920 bulk_species_link_concentration_raw=bulk_species_link_concentrations,
921 surface_species_concentration_raw=surface_species_concentrations)
922
923 @property
924 def network_topo(self) -> NetworkTopology:
925 """
926 Returns the topology of the water distribution network.
927
928 Returns
929 -------
930 :class:`epyt_flow.topology.NetworkTopology`
931 Topology of the network.
932 """
933 return deepcopy(self.__network_topo)
934
935 @property
936 def warnings_code(self) -> np.ndarray:
937 """
938 Returns the codes/IDs of EPANET errors/warnings (if any) for each time step.
939 Note that zero denotes the absence of any error/warning.
940
941 Returns:
942 --------
943 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
944 Codes/IDs of EPANET errors/warnings (if any) for each time step.
945 """
946 return deepcopy(self.__warnings_code)
947
948 @property
949 def frozen_sensor_config(self) -> bool:
950 """
951 Checks if the sensor configuration is frozen or not.
952
953 Returns
954 -------
955 `bool`
956 True if the sensor configuration is frozen, False otherwise.
957 """
958 return self.__frozen_sensor_config
959
960 @property
961 def sensor_config(self) -> SensorConfig:
962 """
963 Gets the sensor configuration.
964
965 Returns
966 -------
967 :class:`~epyt_flow.simulation.sensor_config.SensorConfig`
968 Sensor configuration.
969 """
970 return deepcopy(self.__sensor_config)
971
972 @sensor_config.setter
973 def sensor_config(self, sensor_config: SensorConfig) -> None:
974 if self.__frozen_sensor_config is True:
975 raise RuntimeError("Sensor config can not be changed because it is frozen")
976
977 self.change_sensor_config(sensor_config)
978
979 @property
980 def sensor_noise(self) -> SensorNoise:
981 """
982 Gets the sensor noise.
983
984 Returns
985 -------
986 :class:`~epyt_flow.uncertainty.sensor_noise.SensorNoise`
987 Sensor noise.
988 """
989 return deepcopy(self.__sensor_noise)
990
991 @sensor_noise.setter
992 def sensor_noise(self, sensor_noise: SensorNoise) -> None:
993 self.change_sensor_noise(sensor_noise)
994
995 @property
996 def sensor_faults(self) -> list[SensorFault]:
997 """
998 Gets all sensor faults.
999
1000 Returns
1001 -------
1002 list[:class:`~epyt_flow.simulation.events.sensor_faults.SensorFault`]
1003 All sensor faults.
1004 """
1005 return deepcopy(list(filter(lambda e: isinstance(e, SensorFault),
1006 self.__sensor_reading_events)))
1007
1008 @sensor_faults.setter
1009 def sensor_faults(self, sensor_faults: list[SensorFault]) -> None:
1010 self.change_sensor_faults(sensor_faults)
1011
1012 @property
1013 def sensor_reading_attacks(self) -> list[SensorReadingAttack]:
1014 """
1015 Gets all sensor reading attacks.
1016
1017 Returns
1018 -------
1019 list[:class:`~epyt_flow.simulation.events.sensor_reading_attack.SensorReadingAttack`]
1020 All sensor reading attacks.
1021 """
1022 return deepcopy(list(filter(lambda e: isinstance(e, SensorReadingAttack),
1023 self.__sensor_reading_events)))
1024
1025 @sensor_reading_attacks.setter
1026 def sensor_reading_attacks(self, sensor_reading_attacks: list[SensorReadingAttack]) -> None:
1027 self.change_sensor_reading_attacks(sensor_reading_attacks)
1028
1029 @property
1030 def sensor_reading_events(self) -> list[SensorReadingEvent]:
1031 """
1032 Gets all sensor reading events.
1033
1034 Returns
1035 -------
1036 list[:class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`]
1037 All sensor faults.
1038 """
1039 return deepcopy(self.__sensor_reading_events)
1040
1041 @sensor_reading_events.setter
1042 def sensor_reading_events(self, sensor_reading_events: list[SensorReadingEvent]) -> None:
1043 self.change_sensor_reading_events(sensor_reading_events)
1044
1045 @property
1046 def pressure_data_raw(self) -> np.ndarray:
1047 """
1048 Gets the raw pressure readings.
1049
1050 Returns
1051 -------
1052 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1053 Raw pressure readings.
1054 """
1055 return deepcopy(self.__pressure_data_raw)
1056
1057 @property
1058 def flow_data_raw(self) -> np.ndarray:
1059 """
1060 Gets the raw flow readings.
1061
1062 Returns
1063 -------
1064 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1065 Raw flow readings.
1066 """
1067 return deepcopy(self.__flow_data_raw)
1068
1069 @property
1070 def demand_data_raw(self) -> np.ndarray:
1071 """
1072 Gets the raw demand readings.
1073
1074 Returns
1075 -------
1076 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1077 Raw demand readings.
1078 """
1079 return deepcopy(self.__demand_data_raw)
1080
1081 @property
1082 def node_quality_data_raw(self) -> np.ndarray:
1083 """
1084 Gets the raw node quality readings.
1085
1086 Returns
1087 -------
1088 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1089 Raw node quality readings.
1090 """
1091 return deepcopy(self.__node_quality_data_raw)
1092
1093 @property
1094 def link_quality_data_raw(self) -> np.ndarray:
1095 """
1096 Gets the raw link quality readings.
1097
1098 Returns
1099 -------
1100 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1101 Raw link quality readings.
1102 """
1103 return deepcopy(self.__link_quality_data_raw)
1104
1105 @property
1106 def sensor_readings_time(self) -> np.ndarray:
1107 """
1108 Gets the sensor readings time stamps.
1109
1110 Returns
1111 -------
1112 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1113 Sensor readings time stamps.
1114 """
1115 return deepcopy(self.__sensor_readings_time)
1116
1117 @property
1118 def pumps_state_data_raw(self) -> np.ndarray:
1119 """
1120 Gets the raw pump state readings.
1121
1122 Returns
1123 -------
1124 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1125 Raw pump state readings.
1126 """
1127 return deepcopy(self.__pumps_state_data_raw)
1128
1129 @property
1130 def valves_state_data_raw(self) -> np.ndarray:
1131 """
1132 Gets the raw valve state readings.
1133
1134 Returns
1135 -------
1136 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1137 Raw valve state readings.
1138 """
1139 return deepcopy(self.__valves_state_data_raw)
1140
1141 @property
1142 def tanks_volume_data_raw(self) -> np.ndarray:
1143 """
1144 Gets the raw tank volume readings.
1145
1146 Returns
1147 -------
1148 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1149 Raw tank volume readings.
1150 """
1151 return deepcopy(self.__tanks_volume_data_raw)
1152
1153 @property
1154 def surface_species_concentration_raw(self) -> np.ndarray:
1155 """
1156 Gets the raw surface species concentrations at links/pipes.
1157
1158 Returns
1159 -------
1160 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1161 Raw species concentrations.
1162 """
1163 return deepcopy(self.__surface_species_concentration_raw)
1164
1165 @property
1166 def bulk_species_node_concentration_raw(self) -> np.ndarray:
1167 """
1168 Gets the raw bulk species concentrations at nodes.
1169
1170 Returns
1171 -------
1172 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1173 Raw species concentrations.
1174 """
1175 return deepcopy(self.__bulk_species_node_concentration_raw)
1176
1177 @property
1178 def bulk_species_link_concentration_raw(self) -> np.ndarray:
1179 """
1180 Gets the raw bulk species concentrations at links/pipes.
1181
1182 Returns
1183 -------
1184 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1185 Raw species concentrations.
1186 """
1187 return deepcopy(self.__bulk_species_link_concentration_raw)
1188
1189 @property
1190 def pumps_energyconsumption_data_raw(self) -> np.ndarray:
1191 """
1192 Gets the raw energy consumption of each pump.
1193
1194 Returns
1195 -------
1196 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1197 Energy consumption of each pump.
1198 """
1199 return deepcopy(self.__pumps_energy_usage_data_raw)
1200
1201 @property
1202 def pumps_efficiency_data_raw(self) -> np.ndarray:
1203 """
1204 Gets the raw efficiency of each pump.
1205
1206 Returns
1207 -------
1208 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1209 Pumps' efficiency.
1210 """
1211 return deepcopy(self.__pumps_efficiency_data_raw)
1212
1213 def __map_sensor_to_idx(self, sensor_type: int, sensor_id: Union[str, tuple[str, str]]) -> int:
1214 if sensor_type == SENSOR_TYPE_NODE_PRESSURE:
1215 return self.__sensor_config.get_index_of_reading(pressure_sensor=sensor_id)
1216 elif sensor_type == SENSOR_TYPE_NODE_QUALITY:
1217 return self.__sensor_config.get_index_of_reading(node_quality_sensor=sensor_id)
1218 elif sensor_type == SENSOR_TYPE_NODE_DEMAND:
1219 return self.__sensor_config.get_index_of_reading(demand_sensor=sensor_id)
1220 elif sensor_type == SENSOR_TYPE_LINK_FLOW:
1221 return self.__sensor_config.get_index_of_reading(flow_sensor=sensor_id)
1222 elif sensor_type == SENSOR_TYPE_LINK_QUALITY:
1223 return self.__sensor_config.get_index_of_reading(link_quality_sensor=sensor_id)
1224 elif sensor_type == SENSOR_TYPE_VALVE_STATE:
1225 return self.__sensor_config.get_index_of_reading(valve_state_sensor=sensor_id)
1226 elif sensor_type == SENSOR_TYPE_PUMP_STATE:
1227 return self.__sensor_config.get_index_of_reading(pump_state_sensor=sensor_id)
1228 elif sensor_type == SENSOR_TYPE_PUMP_EFFICIENCY:
1229 return self.__sensor_config.get_index_of_reading(pump_efficiency_sensor=sensor_id)
1230 elif sensor_type == SENSOR_TYPE_PUMP_ENERGYCONSUMPTION:
1231 return self.__sensor_config.get_index_of_reading(pump_energyconsumption_sensor=sensor_id)
1232 elif sensor_type == SENSOR_TYPE_TANK_VOLUME:
1233 return self.__sensor_config.get_index_of_reading(tank_volume_sensor=sensor_id)
1234 elif sensor_type == SENSOR_TYPE_NODE_BULK_SPECIES:
1235 return self.__sensor_config.get_index_of_reading(bulk_species_node_sensor=sensor_id)
1236 elif sensor_type == SENSOR_TYPE_LINK_BULK_SPECIES:
1237 return self.__sensor_config.get_index_of_reading(bulk_species_link_sensor=sensor_id)
1238 elif sensor_type == SENSOR_TYPE_SURFACE_SPECIES:
1239 return self.__sensor_config.get_index_of_reading(surface_species_sensor=sensor_id)
1240 else:
1241 raise ValueError(f"Unknown sensor type '{sensor_type}'")
1242
1243 def __init(self):
1244 self.__apply_global_sensor_noise = lambda x: x
1245 if self.__sensor_noise is not None:
1246 self.__apply_global_sensor_noise = self.__sensor_noise.apply_global_uncertainty
1247
1248 self.__apply_sensor_reading_events = []
1249 for sensor_event in self.__sensor_reading_events:
1250 sensor_id = sensor_event.sensor_id
1251 if sensor_event.sensor_type == SENSOR_TYPE_NODE_BULK_SPECIES:
1252 for species_id, node_sensors_id in self.__sensor_config.bulk_species_node_sensors.items():
1253 if sensor_id in node_sensors_id:
1254 idx = self.__map_sensor_to_idx(sensor_event.sensor_type, (species_id, sensor_id))
1255 self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
1256 elif sensor_event.sensor_type == SENSOR_TYPE_LINK_BULK_SPECIES:
1257 for species_id, link_sensors_id in self.__sensor_config.bulk_species_link_sensors.items():
1258 if sensor_id in link_sensors_id:
1259 idx = self.__map_sensor_to_idx(sensor_event.sensor_type, (species_id, sensor_id))
1260 self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
1261 elif sensor_event.sensor_type == SENSOR_TYPE_SURFACE_SPECIES:
1262 for species_id, link_sensors_id in self.__sensor_config.surface_species_sensors.items():
1263 if sensor_id in link_sensors_id:
1264 idx = self.__map_sensor_to_idx(sensor_event.sensor_type, (species_id, sensor_id))
1265 self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
1266 else:
1267 idx = self.__map_sensor_to_idx(sensor_event.sensor_type, sensor_event.sensor_id)
1268 self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
1269
1270 self.__sensor_readings = None
1271
[docs]
1272 def get_attributes(self) -> dict:
1273 attr = {"network_topo": deepcopy(self.__network_topo),
1274 "warnings_code": deepcopy(self.__warnings_code),
1275 "sensor_config": deepcopy(self.__sensor_config),
1276 "frozen_sensor_config": deepcopy(self.__frozen_sensor_config),
1277 "sensor_noise": deepcopy(self.__sensor_noise),
1278 "sensor_reading_events": deepcopy(self.__sensor_reading_events),
1279 "pressure_data_raw": deepcopy(self.__pressure_data_raw),
1280 "flow_data_raw": deepcopy(self.__flow_data_raw),
1281 "demand_data_raw": deepcopy(self.__demand_data_raw),
1282 "node_quality_data_raw": deepcopy(self.__node_quality_data_raw),
1283 "link_quality_data_raw": deepcopy(self.__link_quality_data_raw),
1284 "sensor_readings_time": deepcopy(self.__sensor_readings_time),
1285 "pumps_state_data_raw": deepcopy(self.__pumps_state_data_raw),
1286 "valves_state_data_raw": deepcopy(self.__valves_state_data_raw),
1287 "tanks_volume_data_raw": deepcopy(self.__tanks_volume_data_raw),
1288 "surface_species_concentration_raw":
1289 deepcopy(self.__surface_species_concentration_raw),
1290 "bulk_species_node_concentration_raw":
1291 deepcopy(self.__bulk_species_node_concentration_raw),
1292 "bulk_species_link_concentration_raw":
1293 deepcopy(self.__bulk_species_link_concentration_raw),
1294 "pumps_energy_usage_data_raw": deepcopy(self.__pumps_energy_usage_data_raw),
1295 "pumps_efficiency_data_raw": deepcopy(self.__pumps_efficiency_data_raw)
1296 }
1297
1298 if self.__frozen_sensor_config is True:
1299 def __create_sparse_array(sensors: list[str], map_sensor_to_idx: Callable[str, int],
1300 n_all_items: int,
1301 get_data: Callable[list[str], np.ndarray]) -> bsr_array:
1302 row = []
1303 col = []
1304 data = []
1305
1306 for sensor_id in sensors:
1307 idx = map_sensor_to_idx(sensor_id)
1308 data_ = get_data([sensor_id])
1309
1310 data += data_.flatten().tolist()
1311 row += list(range(len(self.__sensor_readings_time)))
1312 col += [idx] * len(self.__sensor_readings_time)
1313
1314 return bsr_array((data, (row, col)),
1315 shape=(len(self.__sensor_readings_time), n_all_items))
1316
1317 def __msx_create_sparse_array(sensors: list[str], map_sensor_to_idx: Callable[str, int],
1318 species_id: str, n_all_items: int,
1319 get_data: Callable[dict[str, list[str]], np.ndarray]
1320 ) -> bsr_array:
1321 row = []
1322 col = []
1323 data = []
1324
1325 for sensor_id in sensors:
1326 item_idx = map_sensor_to_idx(sensor_id)
1327
1328 row += list(range(len(self.__sensor_readings_time)))
1329 col += [item_idx] * len(self.__sensor_readings_time)
1330 data += get_data({species_id: [sensor_id]}).flatten().tolist()
1331
1332 return bsr_array((data, (row, col)),
1333 shape=(len(self.__sensor_readings_time), n_all_items))
1334
1335 if self.__pressure_data_raw is not None:
1336 attr["pressure_data_raw"] = __create_sparse_array(
1337 self.__sensor_config.pressure_sensors,
1338 self.__sensor_config.map_node_id_to_idx,
1339 len(self.__sensor_config.nodes),
1340 self.get_data_pressures)
1341 if self.__flow_data_raw is not None:
1342 attr["flow_data_raw"] = __create_sparse_array(
1343 self.__sensor_config.flow_sensors,
1344 self.__sensor_config.map_link_id_to_idx,
1345 len(self.__sensor_config.links),
1346 self.get_data_flows)
1347 if self.__demand_data_raw is not None:
1348 attr["demand_data_raw"] = __create_sparse_array(
1349 self.__sensor_config.demand_sensors,
1350 self.__sensor_config.map_node_id_to_idx,
1351 len(self.__sensor_config.nodes),
1352 self.get_data_demands)
1353 if self.__node_quality_data_raw is not None:
1354 attr["node_quality_data_raw"] = __create_sparse_array(
1355 self.__sensor_config.quality_node_sensors,
1356 self.__sensor_config.map_node_id_to_idx,
1357 len(self.__sensor_config.nodes),
1358 self.get_data_nodes_quality)
1359 if self.__link_quality_data_raw is not None:
1360 attr["link_quality_data_raw"] = __create_sparse_array(
1361 self.__sensor_config.quality_link_sensors,
1362 self.__sensor_config.map_link_id_to_idx,
1363 len(self.__sensor_config.links),
1364 self.get_data_links_quality)
1365 if self.__pumps_state_data_raw is not None:
1366 attr["pumps_state_data_raw"] = __create_sparse_array(
1367 self.__sensor_config.pump_state_sensors,
1368 self.__sensor_config.map_pump_id_to_idx,
1369 len(self.__sensor_config.pumps),
1370 self.get_data_pumps_state)
1371 if self.__valves_state_data_raw is not None:
1372 attr["valves_state_data_raw"] = __create_sparse_array(
1373 self.__sensor_config.valve_state_sensors,
1374 self.__sensor_config.map_valve_id_to_idx,
1375 len(self.__sensor_config.valves),
1376 self.get_data_valves_state)
1377 if self.__tanks_volume_data_raw is not None:
1378 attr["tanks_volume_data_raw"] = __create_sparse_array(
1379 self.__sensor_config.tank_volume_sensors,
1380 self.__sensor_config.map_tank_id_to_idx,
1381 len(self.__sensor_config.tanks),
1382 self.get_data_tanks_water_volume)
1383 if self.__pumps_energy_usage_data_raw is not None:
1384 attr["pumps_energy_usage_data_raw"] = __create_sparse_array(
1385 self.__sensor_config.pump_energyconsumption_sensors,
1386 self.__sensor_config.map_pump_id_to_idx,
1387 len(self.__sensor_config.pumps),
1388 self.get_data_pumps_energyconsumption)
1389 if self.__pumps_efficiency_data_raw is not None:
1390 attr["pumps_efficiency_data_raw"] = __create_sparse_array(
1391 self.__sensor_config.pump_efficiency_sensors,
1392 self.__sensor_config.map_pump_id_to_idx,
1393 len(self.__sensor_config.pumps),
1394 self.get_data_pumps_efficiency)
1395 if self.__surface_species_concentration_raw is not None:
1396 data = {}
1397 for s in self.__sensor_config.surface_species_sensors.keys():
1398 data[s] = __msx_create_sparse_array(self.__sensor_config.surface_species_sensors[s],
1399 self.__sensor_config.map_link_id_to_idx,
1400 s, len(self.__sensor_config.links),
1401 self.get_data_surface_species_concentration)
1402
1403 attr["surface_species_concentration_raw"] = data
1404 if self.__bulk_species_node_concentration_raw is not None:
1405 data = {}
1406 for s in self.__sensor_config.bulk_species_node_sensors.keys():
1407 data[s] = __msx_create_sparse_array(self.__sensor_config.bulk_species_node_sensors[s],
1408 self.__sensor_config.map_node_id_to_idx,
1409 s, len(self.__sensor_config.nodes),
1410 self.get_data_bulk_species_node_concentration)
1411
1412 attr["bulk_species_node_concentration_raw"] = data
1413 if self.__bulk_species_link_concentration_raw is not None:
1414 data = {}
1415 for s in self.__sensor_config.bulk_species_link_sensors.keys():
1416 data[s] = __msx_create_sparse_array(self.__sensor_config.bulk_species_link_sensors[s],
1417 self.__sensor_config.map_link_id_to_idx,
1418 s, len(self.__sensor_config.links),
1419 self.get_data_bulk_species_link_concentration)
1420
1421 attr["bulk_species_link_concentration_raw"] = data
1422
1423 return super().get_attributes() | attr
1424
1425 def __eq__(self, other) -> bool:
1426 if not isinstance(other, ScadaData):
1427 raise TypeError(f"Can not compare 'ScadaData' instance to '{type(other)}' instance")
1428
1429 try:
1430 return self.__network_topo == other.network_topo \
1431 and np.all(self.__warnings_code == other.warnings_code) \
1432 and self.__sensor_config == other.sensor_config \
1433 and self.__frozen_sensor_config == other.frozen_sensor_config \
1434 and self.__sensor_noise == other.sensor_noise \
1435 and all(a == b for a, b in
1436 zip(self.__sensor_reading_events, other.sensor_reading_events)) \
1437 and np.all(self.__pressure_data_raw == other.pressure_data_raw) \
1438 and np.all(self.__flow_data_raw == other.flow_data_raw) \
1439 and np.all(self.__demand_data_raw == self.demand_data_raw) \
1440 and np.all(self.__node_quality_data_raw == other.node_quality_data_raw) \
1441 and np.all(self.__link_quality_data_raw == other.link_quality_data_raw) \
1442 and np.all(self.__sensor_readings_time == other.sensor_readings_time) \
1443 and np.all(self.__pumps_state_data_raw == other.pumps_state_data_raw) \
1444 and np.all(self.__valves_state_data_raw == other.valves_state_data_raw) \
1445 and np.all(self.__tanks_volume_data_raw == other.tanks_volume_data_raw) \
1446 and np.all(self.__surface_species_concentration_raw ==
1447 other.surface_species_concentration_raw) \
1448 and np.all(self.__bulk_species_node_concentration_raw ==
1449 other.bulk_species_node_concentration_raw) \
1450 and np.all(self.__bulk_species_link_concentration_raw ==
1451 other.bulk_species_link_concentration_raw) \
1452 and np.all(self.__pumps_energy_usage_data_raw ==
1453 other.pumps_energyconsumption_data_raw) \
1454 and np.all(self.__pumps_efficiency_data_raw == other.pumps_efficiency_data_raw)
1455 except Exception as ex:
1456 warnings.warn(ex.__str__())
1457 return False
1458
1459 def __str__(self) -> str:
1460 return f"network_topo: {self.__network_topo} sensor_config: {self.__sensor_config} " + \
1461 f"warnings_code: {self.__warnings_code} " + \
1462 f"frozen_sensor_config: {self.__frozen_sensor_config} " + \
1463 f"sensor_noise: {self.__sensor_noise} " + \
1464 f"sensor_reading_events: {self.__sensor_reading_events} " + \
1465 f"pressure_data_raw: {self.__pressure_data_raw} " + \
1466 f"flow_data_raw: {self.__flow_data_raw} demand_data_raw: {self.__demand_data_raw} " + \
1467 f"node_quality_data_raw: {self.__node_quality_data_raw} " + \
1468 f"link_quality_data_raw: {self.__link_quality_data_raw} " + \
1469 f"sensor_readings_time: {self.__sensor_readings_time} " + \
1470 f"pumps_state_data_raw: {self.__pumps_state_data_raw} " + \
1471 f"valves_state_data_raw: {self.__valves_state_data_raw} " + \
1472 f"tanks_volume_data_raw: {self.__tanks_volume_data_raw} " + \
1473 f"surface_species_concentration_raw: {self.__surface_species_concentration_raw} " + \
1474 f"bulk_species_node_concentration_raw: {self.__bulk_species_node_concentration_raw}" +\
1475 f" bulk_species_link_concentration_raw: {self.__bulk_species_link_concentration_raw}" +\
1476 f" pumps_efficiency_data_raw: {self.__pumps_efficiency_data_raw} " + \
1477 f"pumps_energy_usage_data_raw: {self.__pumps_energy_usage_data_raw}"
1478
[docs]
1479 def change_sensor_config(self, sensor_config: SensorConfig) -> None:
1480 """
1481 Changes the sensor configuration.
1482
1483 Parameters
1484 ----------
1485 sensor_config : :class:`~epyt_flow.simulation.sensor_config.SensorConfig`
1486 New sensor configuration.
1487 """
1488 if self.__frozen_sensor_config is True:
1489 raise RuntimeError("Sensor configuration can not be changed because it is frozen")
1490 if not isinstance(sensor_config, SensorConfig):
1491 raise TypeError("'sensor_config' must be an instance of " +
1492 "'epyt_flow.simulation.SensorConfig' but not of " +
1493 f"'{type(sensor_config)}'")
1494
1495 self.__sensor_config = sensor_config
1496 self.__init()
1497
[docs]
1498 def change_sensor_noise(self, sensor_noise: SensorNoise) -> None:
1499 """
1500 Changes the sensor noise/uncertainty.
1501
1502 Parameters
1503 ----------
1504 sensor_noise : :class:`~epyt_flow.uncertainty.sensor_noise.SensorNoise`
1505 New sensor noise/uncertainty specification.
1506 """
1507 if not isinstance(sensor_noise, SensorNoise):
1508 raise TypeError("'sensor_noise' must be an instance of " +
1509 "'epyt_flow.uncertainty.SensorNoise' but not of " +
1510 f"'{type(sensor_noise)}'")
1511
1512 self.__sensor_noise = sensor_noise
1513 self.__init()
1514
[docs]
1515 def change_sensor_faults(self, sensor_faults: list[SensorFault]) -> None:
1516 """
1517 Changes the sensor faults -- overrides all previous sensor faults!
1518
1519 sensor_faults : list[:class:`~epyt_flow.simulation.events.sensor_faults.SensorFault`]
1520 List of new sensor faults.
1521 """
1522 if len(sensor_faults) != 0:
1523 if any(not isinstance(e, SensorFault) for e in sensor_faults):
1524 raise TypeError("'sensor_faults' must be a list of " +
1525 "'epyt_flow.simulation.events.SensorFault' instances")
1526
1527 self.__sensor_reading_events = list(filter(lambda e: not isinstance(e, SensorFault),
1528 self.__sensor_reading_events))
1529 self.__sensor_reading_events += sensor_faults
1530 self.__init()
1531
[docs]
1532 def change_sensor_reading_attacks(self,
1533 sensor_reading_attacks: list[SensorReadingAttack]) -> None:
1534 """
1535 Changes the sensor reading attacks -- overrides all previous sensor reading attacks!
1536
1537 sensor_reading_attacks : list[:class:`~epyt_flow.simulation.events.sensor_reading_attack.SensorReadingAttack`]
1538 List of new sensor reading attacks.
1539 """
1540 if len(sensor_reading_attacks) != 0:
1541 if any(not isinstance(e, SensorReadingAttack) for e in sensor_reading_attacks):
1542 raise TypeError("'sensor_reading_attacks' must be a list of " +
1543 "'epyt_flow.simulation.events.SensorReadingAttack' instances")
1544
1545 self.__sensor_reading_events = list(filter(lambda e: not isinstance(e, SensorReadingAttack),
1546 self.__sensor_reading_events))
1547 self.__sensor_reading_events += sensor_reading_attacks
1548 self.__init()
1549
[docs]
1550 def change_sensor_reading_events(self, sensor_reading_events: list[SensorReadingEvent]) -> None:
1551 """
1552 Changes the sensor reading events -- overrides all previous sensor reading events
1553 (incl. sensor faults)!
1554
1555 sensor_reading_events : list[:class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`]
1556 List of new sensor reading events.
1557 """
1558 if len(sensor_reading_events) != 0:
1559 if any(not isinstance(e, SensorReadingEvent) for e in sensor_reading_events):
1560 raise TypeError("'sensor_reading_events' must be a list of " +
1561 "'epyt_flow.simulation.events.SensorReadingEvent' instances")
1562
1563 self.__sensor_reading_events = sensor_reading_events
1564 self.__init()
1565
1680
[docs]
1681 def join(self, other) -> None:
1682 """
1683 Joins two :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` instances based
1684 on the sensor reading times. Consequently, **both instances must be equal in their
1685 sensor reading times**.
1686 Attributes (i.e. types of sensor readings) that are NOT present in THIS instance
1687 but in `others` will be added to this instance -- all other attributes are ignored.
1688 The sensor configuration is updated according to the sensor readings in `other`.
1689
1690 Parameters
1691 ----------
1692 other : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
1693 Other scada data to be concatenated to this data.
1694 """
1695 if not isinstance(other, ScadaData):
1696 raise TypeError("'other' must be an instance of 'ScadaData' " +
1697 f"but not of '{type(other)}'")
1698 if self.__network_topo != other.network_topo:
1699 raise ValueError("Network topology must be the same in both instances")
1700 if self.__frozen_sensor_config != other.frozen_sensor_config:
1701 raise ValueError("Sensor configurations of both instances must be " +
1702 "either frozen or not frozen")
1703 if not np.all(self.__sensor_readings_time == other.sensor_readings_time):
1704 raise ValueError("Both 'ScadaData' instances must be equal in their " +
1705 "sensor readings times")
1706 if any(e1 != e2 for e1, e2 in zip(self.__sensor_reading_events,
1707 other.sensor_reading_events)):
1708 raise ValueError("'other' must have the same sensor reading events as this instance!")
1709 if self.__sensor_config.nodes != other.sensor_config.nodes:
1710 raise ValueError("Inconsistency in nodes found")
1711 if self.__sensor_config.links != other.sensor_config.links:
1712 raise ValueError("Inconsistency in links/pipes found")
1713 if self.__sensor_config.valves != other.sensor_config.valves:
1714 raise ValueError("Inconsistency in valves found")
1715 if self.__sensor_config.pumps != other.sensor_config.pumps:
1716 raise ValueError("Inconsistency in pumps found")
1717 if self.__sensor_config.tanks != other.sensor_config.tanks:
1718 raise ValueError("Inconsistency in tanks found")
1719 if self.__sensor_config.bulk_species != other.sensor_config.bulk_species:
1720 raise ValueError("Inconsistency in bulk species found")
1721 if self.__sensor_config.surface_species != other.sensor_config.surface_species:
1722 raise ValueError("Inconsistency in surface species found")
1723
1724 self.__sensor_readings = None
1725
1726 if self.__pressure_data_raw is None and other.pressure_data_raw is not None:
1727 self.__pressure_data_raw = other.pressure_data_raw
1728 self.__sensor_config.pressure_sensors = other.sensor_config.pressure_sensors
1729
1730 if self.__flow_data_raw is None and other.flow_data_raw is not None:
1731 self.__flow_data_raw = other.flow_data_raw
1732 self.__sensor_config.flow_sensors = other.sensor_config.flow_sensors
1733
1734 if self.__demand_data_raw is None and other.demand_data_raw is not None:
1735 self.__demand_data_raw = other.demand_data_raw
1736 self.__sensor_config.demand_sensors = other.sensor_config.demand_sensors
1737
1738 if self.__node_quality_data_raw is None and other.node_quality_data_raw is not None:
1739 self.__node_quality_data_raw = other.node_quality_data_raw
1740 self.__sensor_config.quality_node_sensors = other.sensor_config.quality_node_sensors
1741
1742 if self.__link_quality_data_raw is None and other.link_quality_data_raw is not None:
1743 self.__link_quality_data_raw = other.link_quality_data_raw
1744 self.__sensor_config.quality_node_sensors = other.sensor_config.quality_node_sensors
1745
1746 if self.__valves_state_data_raw is None and other.valves_state_data_raw is not None:
1747 self.__valves_state_data_raw = other.valves_state_data_raw
1748 self.__sensor_config.valve_state_sensors = other.sensor_config.valve_state_sensors
1749
1750 if self.__pumps_state_data_raw is None and other.pumps_state_data_raw is not None:
1751 self.__pumps_state_data_raw = other.pumps_state_data_raw
1752 self.__sensor_config.pump_state_sensors = other.sensor_config.pump_state_sensors
1753
1754 if self.__tanks_volume_data_raw is None and other.tanks_volume_data_raw is not None:
1755 self.__tanks_volume_data_raw = other.tanks_volume_data_raw
1756 self.__sensor_config.tank_volume_sensors = other.sensor_config.tank_volume_sensors
1757
1758 if self.__bulk_species_node_concentration_raw is None and \
1759 other.bulk_species_node_concentration_raw is not None:
1760 self.__bulk_species_node_concentration_raw = other.bulk_species_node_concentration_raw
1761 self.__sensor_config.bulk_species_node_sensors = \
1762 other.sensor_config.bulk_species_node_sensors
1763
1764 if self.__bulk_species_link_concentration_raw is None and \
1765 other.bulk_species_link_concentration_raw is not None:
1766 self.__bulk_species_link_concentration_raw = other.bulk_species_link_concentration_raw
1767 self.__sensor_config.bulk_species_link_sensors = \
1768 other.sensor_config.bulk_species_link_sensors
1769
1770 if self.__surface_species_concentration_raw is None and \
1771 other.surface_species_concentration_raw is not None:
1772 self.__surface_species_concentration_raw = other.surface_species_concentration_raw
1773 self.__sensor_config.surface_species_sensors = \
1774 other.sensor_config.surface_species_sensors
1775
1776 if self.__pumps_energy_usage_data_raw is None and \
1777 other.pumps_energyconsumption_data_raw is not None:
1778 self.__pumps_energy_usage_data_raw = other.pumps_energyconsumption_data_raw
1779
1780 if self.__pumps_efficiency_data_raw is None and \
1781 other.pumps_efficiency_data_raw is not None:
1782 self.__pumps_efficiency_data_raw = other.pumps_efficiency_data_raw
1783
1784 self.__init()
1785
[docs]
1786 def concatenate(self, other) -> None:
1787 """
1788 Concatenates two :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` instances
1789 -- i.e. add SCADA data from another given
1790 :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` instance to this one.
1791
1792 Note that the two :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` instances
1793 must be the same in all other attributs (e.g. sensor configuration, etc.).
1794
1795 Parameters
1796 ----------
1797 other : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
1798 Other scada data to be concatenated to this data.
1799 """
1800 if not isinstance(other, ScadaData):
1801 raise TypeError(f"'other' must be an instance of 'ScadaData' but not of {type(other)}")
1802 if self.__network_topo != other.network_topo:
1803 raise ValueError("Network topology must be the same")
1804 if self.__sensor_config != other.sensor_config:
1805 raise ValueError("Sensor configurations must be the same!")
1806 if self.__frozen_sensor_config != other.frozen_sensor_config:
1807 raise ValueError("Sensor configurations of both instances must be " +
1808 "either frozen or not frozen")
1809 if len(self.__sensor_reading_events) != len(other.sensor_reading_events):
1810 raise ValueError("'other' must have the same sensor reading events as this instance!")
1811 if any(e1 != e2 for e1, e2 in zip(self.__sensor_reading_events,
1812 other.sensor_reading_events)):
1813 raise ValueError("'other' must have the same sensor reading events as this instance!")
1814
1815 self.__sensor_readings = None
1816
1817 self.__sensor_readings_time = np.concatenate(
1818 (self.__sensor_readings_time, other.sensor_readings_time), axis=0)
1819
1820 self.__warnings_code = np.concatenate((self.__warnings_code, other.warnings_code), axis=0)
1821
1822 if self.__pressure_data_raw is not None:
1823 self.__pressure_data_raw = np.concatenate(
1824 (self.__pressure_data_raw, other.pressure_data_raw), axis=0)
1825
1826 if self.__flow_data_raw is not None:
1827 self.__flow_data_raw = np.concatenate(
1828 (self.__flow_data_raw, other.flow_data_raw), axis=0)
1829
1830 if self.__demand_data_raw is not None:
1831 self.__demand_data_raw = np.concatenate(
1832 (self.__demand_data_raw, other.demand_data_raw), axis=0)
1833
1834 if self.__node_quality_data_raw is not None:
1835 self.__node_quality_data_raw = np.concatenate(
1836 (self.__node_quality_data_raw, other.node_quality_data_raw), axis=0)
1837
1838 if self.__link_quality_data_raw is not None:
1839 self.__link_quality_data_raw = np.concatenate(
1840 (self.__link_quality_data_raw, other.link_quality_data_raw), axis=0)
1841
1842 if self.__pumps_state_data_raw is not None:
1843 self.__pumps_state_data_raw = np.concatenate(
1844 (self.__pumps_state_data_raw, other.pumps_state_data_raw), axis=0)
1845
1846 if self.__valves_state_data_raw is not None:
1847 self.__valves_state_data_raw = np.concatenate(
1848 (self.__valves_state_data_raw, other.valves_state_data_raw), axis=0)
1849
1850 if self.__tanks_volume_data_raw is not None:
1851 self.__tanks_volume_data_raw = np.concatenate(
1852 (self.__tanks_volume_data_raw, other.tanks_volume_data_raw), axis=0)
1853
1854 if self.__surface_species_concentration_raw is not None:
1855 self.__surface_species_concentration_raw = np.concatenate(
1856 (self.__surface_species_concentration_raw,
1857 other.surface_species_concentration_raw),
1858 axis=0)
1859
1860 if self.__bulk_species_node_concentration_raw is not None:
1861 self.__bulk_species_node_concentration_raw = np.concatenate(
1862 (self.__bulk_species_node_concentration_raw,
1863 other.bulk_species_node_concentration_raw),
1864 axis=0)
1865
1866 if self.__bulk_species_link_concentration_raw is not None:
1867 self.__bulk_species_link_concentration_raw = np.concatenate(
1868 (self.__bulk_species_link_concentration_raw,
1869 other.bulk_species_link_concentration_raw),
1870 axis=0)
1871
1872 if self.__pumps_energy_usage_data_raw is not None:
1873 self.__pumps_energy_usage_data_raw = np.concatenate(
1874 (self.__pumps_energy_usage_data_raw, other.pumps_energyconsumption_data_raw),
1875 axis=0)
1876
1877 if self.__pumps_efficiency_data_raw is not None:
1878 self.__pumps_efficiency_data_raw = np.concatenate(
1879 (self.__pumps_efficiency_data_raw, other.pumps_efficiency_data_raw),
1880 axis=0)
1881
[docs]
1882 def topo_adj_matrix(self) -> bsr_array:
1883 """
1884 Returns the adjacency matrix of the network.
1885
1886 Nodes are ordered according to EPANET.
1887
1888 Returns
1889 -------
1890 `scipy.bsr_array <https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.bsr_array.html>`_
1891 Adjacency matrix as a sparse array of shape [num_nodes, num_nodes].
1892 """
1893 return self.__network_topo.get_adj_matrix()
1894
[docs]
1895 def map_link_id_to_edge_idx(self, link_id: str) -> tuple[int, int]:
1896 """
1897 Maps a given link to the corresponding two indices in the edge indices as computed by
1898 :func:`epyt_flow.simulation.scada.scada_data.ScadaData.topo_edge_indices`.
1899
1900 Returns
1901 -------
1902 `tuple[int, int]`
1903 Indices.
1904 """
1905 if not isinstance(link_id, str):
1906 raise TypeError(f"'link_id' must be an instance of 'str' but not of '{type(link_id)}'")
1907 if link_id not in self.__sensor_config.links:
1908 raise ValueError(f"Unknown link '{link_id}'")
1909
1910 idx = 0
1911 for l_id, [node_a_id, node_b_id] in self.__network_topo.get_all_links():
1912 if l_id == link_id:
1913 return (idx, idx+1)
1914
1915 idx += 2
1916
[docs]
1917 def get_topo_edge_indices(self) -> np.ndarray:
1918 """
1919 Returns the edge indices -- i.e. a 2 dimensional array where the first dimension denotes
1920 the source node indices and the second dimension denotes the target node indices
1921 for all links in the network.
1922 Nodes are ordered according to EPANET.
1923
1924 Note that the network is consideres as a directed graph -- i.e. one link corresponds to
1925 two edges in opposite directions!
1926
1927 Returns
1928 -------
1929 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1930 Edge indices of shape [2, num_links * 2].
1931 """
1932 edge_indices = [[], []]
1933
1934 nodes_id = self.__network_topo.get_all_nodes()
1935 links = self.__network_topo.get_all_links()
1936
1937 for _, [node_a_id, node_b_id] in self.__network_topo.get_all_links():
1938 node_a_idx = nodes_id.index(node_a_id)
1939 node_b_idx = nodes_id.index(node_b_id)
1940
1941 edge_indices[0] += [node_a_idx, node_b_idx]
1942 edge_indices[1] += [node_b_idx, node_a_idx]
1943
1944 return np.array(edge_indices)
1945
[docs]
1946 def get_data(self) -> np.ndarray:
1947 """
1948 Computes the final sensor readings -- note that those might be subject to
1949 given sensor faults and sensor noise/uncertainty.
1950
1951 If the ordering has not been changed in the sensor config,
1952 the columns (i.e. sensor readings) are ordered as follows:
1953
1954 1. Pressures
1955 2. Flows
1956 3. Demands
1957 4. Nodes quality
1958 5. Links quality
1959 6. Valve state
1960 7. Pumps state
1961 8. Pumps efficiency
1962 9. Pumps energy consumption
1963 10. Tanks volume
1964 11. Surface species concentrations
1965 12. Bulk species nodes concentrations
1966 13. Bulk species links concentrations
1967
1968 Otherwise, the ordering follows the one specified in the sensor config
1969
1970 Returns
1971 -------
1972 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
1973 Final sensor readings.
1974 """
1975 # Comute clean sensor readings
1976 if self.__frozen_sensor_config is False:
1977 args = {"pressures": self.__pressure_data_raw,
1978 "flows": self.__flow_data_raw,
1979 "demands": self.__demand_data_raw,
1980 "nodes_quality": self.__node_quality_data_raw,
1981 "links_quality": self.__link_quality_data_raw,
1982 "pumps_state": self.__pumps_state_data_raw,
1983 "pumps_efficiency": self.__pumps_efficiency_data_raw,
1984 "pumps_energyconsumption": self.__pumps_energy_usage_data_raw,
1985 "valves_state": self.__valves_state_data_raw,
1986 "tanks_volume": self.__tanks_volume_data_raw,
1987 "bulk_species_node_concentrations": self.__bulk_species_node_concentration_raw,
1988 "bulk_species_link_concentrations": self.__bulk_species_link_concentration_raw,
1989 "surface_species_concentrations": self.__surface_species_concentration_raw}
1990 sensor_readings = self.__sensor_config.compute_readings(**args)
1991 else:
1992 data = []
1993
1994 for sensor_type in self.__sensor_config.sensor_ordering:
1995 if sensor_type==SENSOR_TYPE_NODE_PRESSURE \
1996 and self.__pressure_data_raw is not None:
1997 data.append(self.__pressure_data_raw)
1998 elif sensor_type==SENSOR_TYPE_NODE_QUALITY \
1999 and self.__node_quality_data_raw is not None:
2000 data.append(self.__node_quality_data_raw)
2001 elif sensor_type==SENSOR_TYPE_NODE_DEMAND \
2002 and self.__demand_data_raw is not None:
2003 data.append(self.__demand_data_raw)
2004 elif sensor_type==SENSOR_TYPE_LINK_FLOW \
2005 and self.__flow_data_raw is not None:
2006 data.append(self.__flow_data_raw)
2007 elif sensor_type==SENSOR_TYPE_LINK_QUALITY \
2008 and self.__link_quality_data_raw is not None:
2009 data.append(self.__link_quality_data_raw)
2010 elif sensor_type==SENSOR_TYPE_VALVE_STATE \
2011 and self.__valves_state_data_raw is not None:
2012 data.append(self.__valves_state_data_raw)
2013 elif sensor_type==SENSOR_TYPE_PUMP_STATE \
2014 and self.__pumps_state_data_raw is not None:
2015 data.append(self.__pumps_state_data_raw)
2016 elif sensor_type==SENSOR_TYPE_TANK_VOLUME \
2017 and self.__tanks_volume_data_raw is not None:
2018 data.append(self.__tanks_volume_data_raw)
2019 elif sensor_type==SENSOR_TYPE_NODE_BULK_SPECIES \
2020 and self.__bulk_species_node_concentration_raw is not None:
2021 data.append(self.__bulk_species_node_concentration_raw)
2022 elif sensor_type==SENSOR_TYPE_LINK_BULK_SPECIES \
2023 and self.__bulk_species_link_concentration_raw is not None:
2024 data.append(self.__bulk_species_link_concentration_raw)
2025 elif sensor_type==SENSOR_TYPE_SURFACE_SPECIES \
2026 and self.__surface_species_concentration_raw is not None:
2027 data.append(self.__surface_species_concentration_raw)
2028 elif sensor_type==SENSOR_TYPE_PUMP_EFFICIENCY \
2029 and self.__pumps_efficiency_data_raw is not None:
2030 data.append(self.__pumps_efficiency_data_raw)
2031 elif sensor_type==SENSOR_TYPE_PUMP_ENERGYCONSUMPTION \
2032 and self.__pumps_energy_usage_data_raw is not None:
2033 data.append(self.__pumps_energy_usage_data_raw)
2034 elif sensor_type not in range(1,14):
2035 raise ValueError(
2036 f"Unknown sensor type '{sensor_type}' found in "
2037 f"'sensor_ordering'. Valid sensor types are:\n"
2038 f"{valid_sensor_types()}"
2039 )
2040 sensor_readings = np.concatenate(data, axis=1)
2041
2042 # Apply sensor uncertainties
2043 if self.__sensor_noise is not None:
2044 state_sensors_idx = [] # Pump states and valve states are NOT affected!
2045 for link_id in self.sensor_config.pump_state_sensors:
2046 state_sensors_idx.append(
2047 self.__sensor_config.get_index_of_reading(pump_state_sensor=link_id))
2048 for link_id in self.sensor_config.valve_state_sensors:
2049 state_sensors_idx.append(
2050 self.__sensor_config.get_index_of_reading(valve_state_sensor=link_id))
2051
2052 mask = np.ones(sensor_readings.shape[1], dtype=bool)
2053 mask[state_sensors_idx] = False
2054 sensor_readings[:, mask] = self.__apply_global_sensor_noise(sensor_readings[:, mask])
2055
2056 sensor_readings = self.__sensor_noise.apply_local_uncertainty(self.__map_sensor_to_idx,
2057 sensor_readings)
2058
2059 # Apply sensor faults
2060 for idx, f in self.__apply_sensor_reading_events:
2061 sensor_readings[:, idx] = f(sensor_readings[:, idx], self.__sensor_readings_time)
2062
2063 self.__sensor_readings = deepcopy(sensor_readings)
2064
2065 return sensor_readings
2066
[docs]
2067 def get_data_node_features(self, default_missing_value: float = 0.
2068 ) -> tuple[np.ndarray, np.ndarray]:
2069 """
2070 Returns the sensor readings as node features together with a boolean mask indicating the
2071 presence of a sensor -- i.e. pressure, demand, quality, bulk species concentration
2072 at each node.
2073
2074 Note that only quantities with at least one sensor are considered.
2075
2076 Parameters
2077 ----------
2078 default_missing_value : `float`, optional
2079 Default value (i.e. missing value) for nodes where no sensor is installed.
2080
2081 The default is 0.
2082
2083 Returns
2084 -------
2085 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>`_]
2086 Node features of shape [num_nodes, num_time_steps, num_node_features], and
2087 mask of shape [num_nodes, num_node_features].
2088 """
2089 node_features = []
2090 node_features_mask = []
2091
2092 if len(self.__sensor_config.pressure_sensors) != 0:
2093 features, node_mask = self.get_data_pressures_as_node_features(default_missing_value)
2094 features = features.T
2095 features = features.reshape(features.shape[0], features.shape[1], 1)
2096 node_features.append(features)
2097 node_features_mask.append(node_mask.reshape(-1, 1))
2098
2099 if len(self.__sensor_config.demand_sensors) != 0:
2100 features, node_mask = self.get_data_demands_as_node_features(default_missing_value)
2101 features = features.T
2102 features = features.reshape(features.shape[0], features.shape[1], 1)
2103 node_features.append(features)
2104 node_features_mask.append(node_mask.reshape(-1, 1))
2105
2106 if len(self.__sensor_config.quality_node_sensors) != 0:
2107 features, node_mask = self.get_data_nodes_quality_as_node_features(default_missing_value)
2108 features = features.T
2109 features = features.reshape(features.shape[0], features.shape[1], 1)
2110 node_features.append(features)
2111 node_features_mask.append(node_mask.reshape(-1, 1))
2112
2113 if len(self.__sensor_config.bulk_species_node_sensors) != 0:
2114 features, node_mask = self.\
2115 get_data_bulk_species_concentrations_as_node_features(default_missing_value)
2116 features = np.swapaxes(features, 0, 1)
2117 node_features.append(features)
2118 node_features_mask.append(node_mask)
2119
2120 return np.concatenate(node_features, axis=2), np.concatenate(node_features_mask, axis=1)
2121
[docs]
2122 def get_data_edge_features(self, default_missing_value: float = 0.
2123 ) -> tuple[np.ndarray, np.ndarray]:
2124 """
2125 Returns the sensor readings as edge features together with a boolean mask indicating the
2126 presence of a sensor -- i.e. flow, quality, surface species concentration,
2127 bulk species concentration at each link.
2128
2129 Note that only quantities with at least one sensor are considered.
2130
2131 Parameters
2132 ----------
2133 default_missing_value : `float`, optional
2134 Default value (i.e. missing value) for links where no sensor is installed.
2135
2136 The default is 0.
2137
2138 Returns
2139 -------
2140 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>`_]
2141 Edge features of shape [num_links, num_time_steps, num_edge_features] and
2142 mask of shape [num_links, num_edge_features].
2143 """
2144 edge_features = []
2145 edge_features_mask = []
2146
2147 if len(self.__sensor_config.flow_sensors) != 0:
2148 features, link_mask = self.get_data_flows_as_edge_features(default_missing_value)
2149 features = features.T
2150 features = features.reshape(features.shape[0], features.shape[1], 1)
2151 edge_features.append(features)
2152 edge_features_mask.append(link_mask.reshape(-1, 1))
2153
2154 if len(self.__sensor_config.quality_link_sensors) != 0:
2155 features, link_mask = self.get_data_links_quality_as_edge_features(default_missing_value)
2156 features = features.T
2157 features = features.reshape(features.shape[0], features.shape[1], 1)
2158 edge_features.append(features)
2159 edge_features_mask.append(link_mask.reshape(-1, 1))
2160
2161 if len(self.__sensor_config.surface_species_sensors) != 0:
2162 features, link_mask = self.\
2163 get_data_surface_species_concentrations_as_edge_features(default_missing_value)
2164 features = np.swapaxes(features, 0, 1)
2165 edge_features.append(features)
2166 edge_features_mask.append(link_mask)
2167
2168 if len(self.__sensor_config.bulk_species_link_sensors) != 0:
2169 features, link_mask = self.\
2170 get_data_bulk_species_concentrations_as_edge_features(default_missing_value)
2171 features = np.swapaxes(features, 0, 1)
2172 edge_features.append(features)
2173 edge_features_mask.append(link_mask)
2174
2175 return np.concatenate(edge_features, axis=2), np.concatenate(edge_features_mask, axis=1)
2176
2177 def __get_x_axis_label(self) -> str:
2178 if len(self.__sensor_readings_time) > 1:
2179 time_step = self.__sensor_readings_time[1] - self.__sensor_readings_time[0]
2180 if time_step > 60:
2181 time_steps_desc = f"{int(time_step / 60)}min"
2182 if time_step > 60*60:
2183 time_steps_desc = f"{int(time_step / 60)}hr"
2184 else:
2185 time_steps_desc = f"{time_step}s"
2186 return f"Time ({time_steps_desc} steps)"
2187 else:
2188 return "Time"
2189
[docs]
2190 def get_data_pressures(self, sensor_locations: list[str] = None) -> np.ndarray:
2191 """
2192 Gets the final pressure sensor readings -- note that those might be subject to
2193 given sensor faults and sensor noise/uncertainty.
2194
2195 Parameters
2196 ----------
2197 sensor_locations : `list[str]`, optional
2198 Existing pressure sensor locations for which the sensor readings are requested.
2199 If None, the readings from all pressure sensors are returned.
2200
2201 The default is None.
2202
2203 Returns
2204 -------
2205 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
2206 Pressure sensor readings.
2207 """
2208 if self.__sensor_config.pressure_sensors == []:
2209 raise ValueError("No pressure sensors set")
2210 if sensor_locations is not None:
2211 if not isinstance(sensor_locations, list):
2212 raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
2213 f"but not of '{type(sensor_locations)}'")
2214 if any(s_id not in self.__sensor_config.pressure_sensors for s_id in sensor_locations):
2215 raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
2216 "sensors in 'sensor_locations' must be set in the current " +
2217 "pressure sensor configuration")
2218 else:
2219 sensor_locations = self.__sensor_config.pressure_sensors
2220
2221 if self.__sensor_readings is None:
2222 self.get_data()
2223
2224 idx = [self.__sensor_config.get_index_of_reading(pressure_sensor=s_id)
2225 for s_id in sensor_locations]
2226 return self.__sensor_readings[:, idx]
2227
[docs]
2228 def get_data_pressures_as_node_features(self,
2229 default_missing_value: float = 0.
2230 ) -> tuple[np.ndarray, np.ndarray]:
2231 """
2232 Returns the pressures as node features together with a boolean mask indicating the
2233 presence of a sensor.
2234
2235 Parameters
2236 ----------
2237 default_missing_value : `float`, optional
2238 Default value (i.e. missing value) for nodes where no pressure sensor is installed.
2239
2240 The default is 0.
2241
2242 Returns
2243 -------
2244 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>`_]
2245 Pressures as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
2246 """
2247 mask = np.zeros(len(self.__sensor_config.nodes))
2248 node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
2249 for _ in range(len(self.__sensor_readings_time))])
2250
2251 pressure_readings = self.get_data_pressures()
2252 for pressures_idx, node_id in enumerate(self.__sensor_config.pressure_sensors):
2253 idx = self.__sensor_config.map_node_id_to_idx(node_id)
2254 node_features[:, idx] = pressure_readings[:, pressures_idx]
2255 mask[idx] = 1
2256
2257 return node_features, mask
2258
[docs]
2259 def plot_pressures(self, sensor_locations: list[str] = None, show: bool = True,
2260 save_to_file: str = None, ax: matplotlib.axes.Axes = None
2261 ) -> matplotlib.axes.Axes:
2262 """
2263 Plots the final pressure sensor readings -- note that those might be subject to
2264 given sensor faults and sensor noise/uncertainty.
2265
2266 Parameters
2267 ----------
2268 sensor_locations : `list[str]`, optional
2269 Existing pressure sensor locations for which the sensor readings have to be plotted.
2270 If None, the readings from all pressure sensors are plotted.
2271
2272 The default is None.
2273 show : `bool`, optional
2274 If True, the plot/figure is shown in a window.
2275
2276 Only considered when 'ax' is None.
2277
2278 The default is True.
2279 save_to_file : `str`, optional
2280 File to which the plot is saved.
2281
2282 If specified, 'show' must be set to False --
2283 i.e. a plot can not be shown and saved to a file at the same time!
2284
2285 The default is None.
2286 ax : `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_, optional
2287 If not None, 'ax' is used for plotting.
2288
2289 The default is None.
2290
2291 Returns
2292 -------
2293 `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_
2294 Plot.
2295 """
2296 data = self.get_data_pressures(sensor_locations)
2297 pressure_sensors = sensor_locations if sensor_locations is not None else \
2298 self.__sensor_config.pressure_sensors
2299
2300 y_axis_label = f"Pressure in ${pressureunit_to_str(self.__sensor_config.pressure_unit)}$"
2301
2302 return plot_timeseries_data(data.T, labels=[f"Node {n_id}" for n_id in pressure_sensors],
2303 x_axis_label=self.__get_x_axis_label(),
2304 y_axis_label=y_axis_label,
2305 show=show, save_to_file=save_to_file, ax=ax)
2306
[docs]
2307 def get_data_flows(self, sensor_locations: list[str] = None) -> np.ndarray:
2308 """
2309 Gets the final flow sensor readings -- note that those might be subject to
2310 given sensor faults and sensor noise/uncertainty.
2311
2312 Parameters
2313 ----------
2314 sensor_locations : `list[str]`, optional
2315 Existing flow sensor locations for which the sensor readings are requested.
2316 If None, the readings from all flow sensors are returned.
2317
2318 The default is None.
2319
2320 Returns
2321 -------
2322 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
2323 Flow sensor readings.
2324 """
2325 if self.__sensor_config.flow_sensors == []:
2326 raise ValueError("No flow sensors set")
2327 if sensor_locations is not None:
2328 if not isinstance(sensor_locations, list):
2329 raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
2330 f"but not of '{type(sensor_locations)}'")
2331 if any(s_id not in self.__sensor_config.flow_sensors for s_id in sensor_locations):
2332 raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
2333 "sensors in 'sensor_locations' must be set in the current " +
2334 "flow sensor configuration")
2335 else:
2336 sensor_locations = self.__sensor_config.flow_sensors
2337
2338 if self.__sensor_readings is None:
2339 self.get_data()
2340
2341 idx = [self.__sensor_config.get_index_of_reading(flow_sensor=s_id)
2342 for s_id in sensor_locations]
2343 return self.__sensor_readings[:, idx]
2344
[docs]
2345 def get_data_flows_as_edge_features(self, default_missing_value: float = 0.
2346 ) -> tuple[np.ndarray, np.ndarray]:
2347 """
2348 Returns the flows as edge features together with a boolean mask indicating the
2349 presence of a sensor.
2350
2351 Note that the second link has the opposite flow direction of the flow at the first link --
2352 recall that we have an undirected graph, i.e. two edges per link.
2353
2354 Parameters
2355 ----------
2356 default_missing_value : `float`, optional
2357 Default value (i.e. missing value) for links where no flow sensor is installed.
2358
2359 The default is 0.
2360
2361 Returns
2362 -------
2363 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>`_]
2364 Flows as edge features of shape [num_time_steps, num_links * 2] and mask of shape [num_links * 2].
2365 """
2366 mask = np.zeros(2 * len(self.__sensor_config.links))
2367 edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
2368 for _ in range(len(self.__sensor_readings_time))])
2369
2370 flow_readings = self.get_data_flows()
2371 for flows_idx, link_id in enumerate(self.__sensor_config.flow_sensors):
2372 idx1, idx2 = self.map_link_id_to_edge_idx(link_id)
2373
2374 mask[idx1] = 1
2375 mask[idx2] = 1
2376
2377 edge_features[:, idx1] = flow_readings[:, flows_idx]
2378 edge_features[:, idx2] = -1 * flow_readings[:, flows_idx]
2379
2380 return edge_features, mask
2381
[docs]
2382 def plot_flows(self, sensor_locations: list[str] = None, show: bool = True,
2383 save_to_file: str = None, ax: matplotlib.axes.Axes = None
2384 ) -> matplotlib.axes.Axes:
2385 """
2386 Plots the final flow sensor readings -- note that those might be subject to
2387 given sensor faults and sensor noise/uncertainty.
2388
2389 Parameters
2390 ----------
2391 sensor_locations : `list[str]`, optional
2392 Existing flow sensor locations for which the sensor readings have to be plotted.
2393 If None, the readings from all flow sensors are plotted.
2394
2395 The default is None.
2396 show : `bool`, optional
2397 If True, the plot/figure is shown in a window.
2398
2399 Only considered when 'ax' is None.
2400
2401 The default is True.
2402 save_to_file : `str`, optional
2403 File to which the plot is saved.
2404
2405 If specified, 'show' must be set to False --
2406 i.e. a plot can not be shown and saved to a file at the same time!
2407
2408 The default is None.
2409 ax : `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_, optional
2410 If not None, 'ax' is used for plotting.
2411
2412 The default is None.
2413
2414 Returns
2415 -------
2416 `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_
2417 Plot.
2418 """
2419 data = self.get_data_flows(sensor_locations)
2420 flow_sensors = sensor_locations if sensor_locations is not None else \
2421 self.__sensor_config.flow_sensors
2422
2423 y_axis_label = f"Flow rate in ${flowunit_to_str(self.__sensor_config.flow_unit)}$"
2424
2425 return plot_timeseries_data(data.T, labels=[f"Link {n_id}" for n_id in flow_sensors],
2426 x_axis_label=self.__get_x_axis_label(),
2427 y_axis_label=y_axis_label,
2428 show=show, save_to_file=save_to_file, ax=ax)
2429
[docs]
2430 def get_data_demands(self, sensor_locations: list[str] = None) -> np.ndarray:
2431 """
2432 Gets the final demand sensor readings -- note that those might be subject to
2433 given sensor faults and sensor noise/uncertainty.
2434
2435 Parameters
2436 ----------
2437 sensor_locations : `list[str]`, optional
2438 Existing demand sensor locations for which the sensor readings are requested.
2439 If None, the readings from all demand sensors are returned.
2440
2441 The default is None.
2442
2443 Returns
2444 -------
2445 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
2446 Demand sensor readings.
2447 """
2448 if self.__sensor_config.demand_sensors == []:
2449 raise ValueError("No demand sensors set")
2450 if sensor_locations is not None:
2451 if not isinstance(sensor_locations, list):
2452 raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
2453 f"but not of '{type(sensor_locations)}'")
2454 if any(s_id not in self.__sensor_config.demand_sensors for s_id in sensor_locations):
2455 raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
2456 "sensors in 'sensor_locations' must be set in the current " +
2457 "demand sensor configuration")
2458 else:
2459 sensor_locations = self.__sensor_config.demand_sensors
2460
2461 if self.__sensor_readings is None:
2462 self.get_data()
2463
2464 idx = [self.__sensor_config.get_index_of_reading(demand_sensor=s_id)
2465 for s_id in sensor_locations]
2466 return self.__sensor_readings[:, idx]
2467
[docs]
2468 def get_data_demands_as_node_features(self,
2469 default_missing_value: float = 0.
2470 ) -> tuple[np.ndarray, np.ndarray]:
2471 """
2472 Returns the demands as node features together with a boolean mask indicating the
2473 presence of a sensor.
2474
2475 Parameters
2476 ----------
2477 default_missing_value : `float`, optional
2478 Default value (i.e. missing value) for nodes where no demand sensor is installed.
2479
2480 The default is 0.
2481
2482 Returns
2483 -------
2484 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>`_]
2485 Demands as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
2486 """
2487 mask = np.zeros(len(self.__sensor_config.nodes))
2488 node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
2489 for _ in range(len(self.__sensor_readings_time))])
2490
2491 demand_readings = self.get_data_demands()
2492 for demands_idx, node_id in enumerate(self.__sensor_config.demand_sensors):
2493 idx = self.__sensor_config.map_node_id_to_idx(node_id)
2494 node_features[:, idx] = demand_readings[:, demands_idx]
2495 mask[idx] = 1
2496
2497 return node_features, mask
2498
[docs]
2499 def plot_demands(self, sensor_locations: list[str] = None, show: bool = True,
2500 save_to_file: str = None, ax: matplotlib.axes.Axes = None
2501 ) -> matplotlib.axes.Axes:
2502 """
2503 Plots the final demand sensor readings -- note that those might be subject to
2504 given sensor faults and sensor noise/uncertainty.
2505
2506 Parameters
2507 ----------
2508 sensor_locations : `list[str]`, optional
2509 Existing demand sensor locations for which the sensor readings have to be plotted.
2510 If None, the readings from all demand sensors are plotted.
2511
2512 The default is None.
2513 show : `bool`, optional
2514 If True, the plot/figure is shown in a window.
2515
2516 Only considered when 'ax' is None.
2517
2518 The default is True.
2519 save_to_file : `str`, optional
2520 File to which the plot is saved.
2521
2522 If specified, 'show' must be set to False --
2523 i.e. a plot can not be shown and saved to a file at the same time!
2524
2525 The default is None.
2526 ax : `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_, optional
2527 If not None, 'ax' is used for plotting.
2528
2529 The default is None.
2530
2531 Returns
2532 -------
2533 `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_
2534 Plot.
2535 """
2536 data = self.get_data_demands(sensor_locations)
2537 demand_sensors = sensor_locations if sensor_locations is not None else \
2538 self.__sensor_config.demand_sensors
2539
2540 y_axis_label = f"Demand in ${flowunit_to_str(self.__sensor_config.flow_unit)}$"
2541
2542 return plot_timeseries_data(data.T, labels=[f"Node {n_id}" for n_id in demand_sensors],
2543 x_axis_label=self.__get_x_axis_label(),
2544 y_axis_label=y_axis_label,
2545 show=show, save_to_file=save_to_file, ax=ax)
2546
[docs]
2547 def get_data_nodes_quality(self, sensor_locations: list[str] = None) -> np.ndarray:
2548 """
2549 Gets the final node quality sensor readings -- note that those might be subject to
2550 given sensor faults and sensor noise/uncertainty.
2551
2552 Parameters
2553 ----------
2554 sensor_locations : `list[str]`, optional
2555 Existing node quality sensor locations for which the sensor readings are requested.
2556 If None, the readings from all node quality sensors are returned.
2557
2558 The default is None.
2559
2560 Returns
2561 -------
2562 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
2563 Node quality sensor readings.
2564 """
2565 if self.__sensor_config.quality_node_sensors == []:
2566 raise ValueError("No node quality sensors set")
2567 if sensor_locations is not None:
2568 if not isinstance(sensor_locations, list):
2569 raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
2570 f"but not of '{type(sensor_locations)}'")
2571 if any(s_id not in self.__sensor_config.quality_node_sensors
2572 for s_id in sensor_locations):
2573 raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
2574 "sensors in 'sensor_locations' must be set in the current " +
2575 "node quality sensor configuration")
2576 else:
2577 sensor_locations = self.__sensor_config.quality_node_sensors
2578
2579 if self.__sensor_readings is None:
2580 self.get_data()
2581
2582 idx = [self.__sensor_config.get_index_of_reading(node_quality_sensor=s_id)
2583 for s_id in sensor_locations]
2584 return self.__sensor_readings[:, idx]
2585
[docs]
2586 def get_data_nodes_quality_as_node_features(self,
2587 default_missing_value: float = 0
2588 ) -> tuple[np.ndarray, np.ndarray]:
2589 """
2590 Returns the nodes' quality as node features together with a boolean mask indicating the
2591 presence of a sensor.
2592
2593 Parameters
2594 ----------
2595 default_missing_value : `float`, optional
2596 Default value (i.e. missing value) for nodes where no quality sensor is installed.
2597
2598 The default is 0.
2599
2600 Returns
2601 -------
2602 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>`_]
2603 Nodes' quality as node features of shape [num_time_steps, num_nodes], and mask of
2604 shape [num_nodes].
2605 """
2606 mask = np.zeros(len(self.__sensor_config.nodes))
2607 node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
2608 for _ in range(len(self.__sensor_readings_time))])
2609
2610 node_quality_readings = self.get_data_nodes_quality()
2611 for quality_idx, node_id in enumerate(self.__sensor_config.quality_node_sensors):
2612 idx = self.__sensor_config.map_node_id_to_idx(node_id)
2613 node_features[:, idx] = node_quality_readings[:, quality_idx]
2614 mask[idx] = 1
2615
2616 return node_features, mask
2617
[docs]
2618 def plot_nodes_quality(self, sensor_locations: list[str] = None, show: bool = True,
2619 save_to_file: str = None, ax: matplotlib.axes.Axes = None
2620 ) -> matplotlib.axes.Axes:
2621 """
2622 Plots the final node quality sensor readings -- note that those might be subject to
2623 given sensor faults and sensor noise/uncertainty.
2624
2625 Parameters
2626 ----------
2627 sensor_locations : `list[str]`, optional
2628 Existing node quality sensor locations for which the sensor readings
2629 have to be plotted.
2630 If None, the readings from all node quality sensors are plotted.
2631
2632 The default is None.
2633 show : `bool`, optional
2634 If True, the plot/figure is shown in a window.
2635
2636 Only considered when 'ax' is None.
2637
2638 The default is True.
2639 save_to_file : `str`, optional
2640 File to which the plot is saved.
2641
2642 If specified, 'show' must be set to False --
2643 i.e. a plot can not be shown and saved to a file at the same time!
2644
2645 The default is None.
2646 ax : `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_, optional
2647 If not None, 'ax' is used for plotting.
2648
2649 The default is None.
2650
2651 Returns
2652 -------
2653 `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_
2654 Plot.
2655 """
2656 data = self.get_data_nodes_quality(sensor_locations)
2657 nodes_quality_sensors = sensor_locations if sensor_locations is not None else \
2658 self.__sensor_config.quality_node_sensors
2659
2660 y_axis_label = f"${qualityunit_to_str(self.__sensor_config.quality_unit)}$"
2661
2662 return plot_timeseries_data(data.T, labels=[f"Node {n_id}"
2663 for n_id in nodes_quality_sensors],
2664 x_axis_label=self.__get_x_axis_label(),
2665 y_axis_label=y_axis_label,
2666 show=show, save_to_file=save_to_file, ax=ax)
2667
[docs]
2668 def get_data_links_quality(self, sensor_locations: list[str] = None) -> np.ndarray:
2669 """
2670 Gets the final link quality sensor readings -- note that those might be subject to
2671 given sensor faults and sensor noise/uncertainty.
2672
2673 Parameters
2674 ----------
2675 sensor_locations : `list[str]`, optional
2676 Existing link quality sensor locations for which the sensor readings are requested.
2677 If None, the readings from all link quality sensors are returned.
2678
2679 The default is None.
2680
2681 Returns
2682 -------
2683 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
2684 Link quality sensor readings.
2685 """
2686 if self.__sensor_config.quality_link_sensors == []:
2687 raise ValueError("No link quality sensors set")
2688 if sensor_locations is not None:
2689 if not isinstance(sensor_locations, list):
2690 raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
2691 f"but not of '{type(sensor_locations)}'")
2692 if any(s_id not in self.__sensor_config.quality_link_sensors
2693 for s_id in sensor_locations):
2694 raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
2695 "sensors in 'sensor_locations' must be set in the current " +
2696 "link quality sensor configuration")
2697 else:
2698 sensor_locations = self.__sensor_config.quality_link_sensors
2699
2700 if self.__sensor_readings is None:
2701 self.get_data()
2702
2703 idx = [self.__sensor_config.get_index_of_reading(link_quality_sensor=s_id)
2704 for s_id in sensor_locations]
2705 return self.__sensor_readings[:, idx]
2706
[docs]
2707 def get_data_links_quality_as_edge_features(self,
2708 default_missing_value: float = 0
2709 ) -> tuple[np.ndarray, np.ndarray]:
2710 """
2711 Returns the links' quality as edge features together with a boolean mask indicating the
2712 presence of a sensor.
2713
2714 Parameters
2715 ----------
2716 default_missing_value : `float`, optional
2717 Default value (i.e. missing value) for links where no quality sensor is installed.
2718
2719 The default is 0.
2720
2721 Returns
2722 -------
2723 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>`_]
2724 Links' quality as edge features of shape [num_time_steps, num_links * 2], and mask of
2725 shape [num_links * 2].
2726 """
2727 mask = np.zeros(2 * len(self.__sensor_config.links))
2728 edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
2729 for _ in range(len(self.__sensor_readings_time))])
2730
2731 links_quality_readings = self.get_data_links_quality()
2732 for quality_idx, link_id in enumerate(self.__sensor_config.quality_link_sensors):
2733 for idx in self.map_link_id_to_edge_idx(link_id):
2734 edge_features[:, idx] = links_quality_readings[:, quality_idx]
2735 mask[idx] = 1
2736
2737 return edge_features, mask
2738
[docs]
2739 def plot_links_quality(self, sensor_locations: list[str] = None, show: bool = True,
2740 save_to_file: str = None, ax: matplotlib.axes.Axes = None
2741 ) -> matplotlib.axes.Axes:
2742 """
2743 Plots the final link/pipe quality sensor readings -- note that those might be subject to
2744 given sensor faults and sensor noise/uncertainty.
2745
2746 Parameters
2747 ----------
2748 sensor_locations : `list[str]`, optional
2749 Existing link quality sensor locations for which the sensor readings
2750 have to be plotted.
2751 If None, the readings from all link quality sensors are plotted.
2752
2753 The default is None.
2754 show : `bool`, optional
2755 If True, the plot/figure is shown in a window.
2756
2757 Only considered when 'ax' is None.
2758
2759 The default is True.
2760 save_to_file : `str`, optional
2761 File to which the plot is saved.
2762
2763 If specified, 'show' must be set to False --
2764 i.e. a plot can not be shown and saved to a file at the same time!
2765
2766 The default is None.
2767 ax : `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_, optional
2768 If not None, 'ax' is used for plotting.
2769
2770 The default is None.
2771
2772 Returns
2773 -------
2774 `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_
2775 Plot.
2776 """
2777 data = self.get_data_links_quality(sensor_locations)
2778 links_quality_sensors = sensor_locations if sensor_locations is not None else \
2779 self.__sensor_config.quality_link_sensors
2780
2781 y_axis_label = f"${qualityunit_to_str(self.__sensor_config.quality_unit)}$"
2782
2783 return plot_timeseries_data(data.T, labels=[f"Link {n_id}"
2784 for n_id in links_quality_sensors],
2785 x_axis_label=self.__get_x_axis_label(),
2786 y_axis_label=y_axis_label,
2787 show=show, save_to_file=save_to_file, ax=ax)
2788
[docs]
2789 def get_data_pumps_state(self, sensor_locations: list[str] = None) -> np.ndarray:
2790 """
2791 Gets the final pump state sensor readings -- note that those might be subject to
2792 given sensor faults and sensor noise/uncertainty.
2793
2794 Parameters
2795 ----------
2796 sensor_locations : `list[str]`, optional
2797 Existing pump state sensor locations for which the sensor readings are requested.
2798 If None, the readings from all pump state sensors are returned.
2799
2800 The default is None.
2801
2802 Returns
2803 -------
2804 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
2805 Pump state sensor readings.
2806 """
2807 if self.__sensor_config.pump_state_sensors == []:
2808 raise ValueError("No pump state sensors set")
2809 if sensor_locations is not None:
2810 if not isinstance(sensor_locations, list):
2811 raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
2812 f"but not of '{type(sensor_locations)}'")
2813 if any(s_id not in self.__sensor_config.pump_state_sensors
2814 for s_id in sensor_locations):
2815 raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
2816 "sensors in 'sensor_locations' must be set in the current " +
2817 "pump state sensor configuration")
2818 else:
2819 sensor_locations = self.__sensor_config.pump_state_sensors
2820
2821 if self.__sensor_readings is None:
2822 self.get_data()
2823
2824 idx = [self.__sensor_config.get_index_of_reading(pump_state_sensor=s_id)
2825 for s_id in sensor_locations]
2826 return self.__sensor_readings[:, idx]
2827
[docs]
2828 def get_data_pumps_state_as_node_features(self,
2829 default_missing_value: float = 0.
2830 ) -> tuple[np.ndarray, np.ndarray]:
2831 """
2832 Returns the pump state as node features together with a boolean mask indicating the
2833 presence of a sensor.
2834
2835 Parameters
2836 ----------
2837 default_missing_value : `float`, optional
2838 Default value (i.e. missing value) for nodes where no pump state sensor is installed.
2839
2840 The default is 0.
2841
2842 Returns
2843 -------
2844 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>`_]
2845 Pump state as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
2846 """
2847 mask = np.zeros(len(self.__sensor_config.pumps))
2848 pump_features = np.array([[default_missing_value] * len(self.__sensor_config.pumps)
2849 for _ in range(len(self.__sensor_readings_time))])
2850 pumps_id = self.__network_topo.get_all_pumps()
2851
2852 state_readings = self.get_data_pumps_state()
2853 for pumps_state_idx, pump_id in enumerate(self.__sensor_config.pump_state_sensors):
2854 idx = pumps_id.index(pump_id)
2855 pump_features[:, idx] = state_readings[:, pumps_state_idx]
2856 mask[idx] = 1
2857
2858 return pump_features, mask
2859
[docs]
2860 def plot_pumps_state(self, sensor_locations: list[str] = None, show: bool = True,
2861 save_to_file: str = None, ax: matplotlib.axes.Axes = None
2862 ) -> matplotlib.axes.Axes:
2863 """
2864 Plots the final pump state sensor readings -- note that those might be subject to
2865 given sensor faults and sensor noise/uncertainty.
2866
2867 Parameters
2868 ----------
2869 sensor_locations : `list[str]`, optional
2870 Existing pump state sensor locations for which the sensor readings have to be plotted.
2871 If None, the readings from all pump state sensors are plotted.
2872
2873 The default is None.
2874 show : `bool`, optional
2875 If True, the plot/figure is shown in a window.
2876
2877 Only considered when 'ax' is None.
2878
2879 The default is True.
2880 save_to_file : `str`, optional
2881 File to which the plot is saved.
2882
2883 If specified, 'show' must be set to False --
2884 i.e. a plot can not be shown and saved to a file at the same time!
2885
2886 The default is None.
2887 ax : `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_, optional
2888 If not None, 'ax' is used for plotting.
2889
2890 The default is None.
2891
2892 Returns
2893 -------
2894 `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_
2895 Plot.
2896 """
2897 data = self.get_data_pumps_state(sensor_locations)
2898 pump_state_sensors = sensor_locations if sensor_locations is not None else \
2899 self.__sensor_config.pump_state_sensors
2900
2901 return plot_timeseries_data(data.T, labels=[f"Pump {n_id}"
2902 for n_id in pump_state_sensors],
2903 x_axis_label=self.__get_x_axis_label(),
2904 y_axis_label="Pump state",
2905 y_ticks=([2.0, 3.0], ["Off", "On"]),
2906 show=show, save_to_file=save_to_file, ax=ax)
2907
[docs]
2908 def get_data_pumps_efficiency(self, sensor_locations: list[str] = None) -> np.ndarray:
2909 """
2910 Gets the final pump efficiency sensor readings -- note that those might be subject to
2911 given sensor faults and sensor noise/uncertainty.
2912
2913 Parameters
2914 ----------
2915 sensor_locations : `list[str]`, optional
2916 Existing pump efficiency sensor locations for which the sensor readings are requested.
2917 If None, the readings from all pump efficiency sensors are returned.
2918
2919 The default is None.
2920
2921 Returns
2922 -------
2923 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
2924 Pump efficiency sensor readings.
2925 """
2926 if self.__sensor_config.pump_efficiency_sensors == []:
2927 raise ValueError("No pump efficiency sensors set")
2928 if sensor_locations is not None:
2929 if not isinstance(sensor_locations, list):
2930 raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
2931 f"but not of '{type(sensor_locations)}'")
2932 if any(s_id not in self.__sensor_config.pump_efficiency_sensors
2933 for s_id in sensor_locations):
2934 raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
2935 "sensors in 'sensor_locations' must be set in the current " +
2936 "pump efficiency sensor configuration")
2937 else:
2938 sensor_locations = self.__sensor_config.pump_efficiency_sensors
2939
2940 if self.__sensor_readings is None:
2941 self.get_data()
2942
2943 idx = [self.__sensor_config.get_index_of_reading(pump_efficiency_sensor=s_id)
2944 for s_id in sensor_locations]
2945 return self.__sensor_readings[:, idx]
2946
[docs]
2947 def get_data_pumps_efficiency_as_node_features(self,
2948 default_missing_value: float = 0.
2949 ) -> tuple[np.ndarray, np.ndarray]:
2950 """
2951 Returns the pump efficiency as node features together with a boolean mask indicating the
2952 presence of a sensor.
2953
2954 Parameters
2955 ----------
2956 default_missing_value : `float`, optional
2957 Default value (i.e. missing value) for nodes where no pump efficiency sensor is installed.
2958
2959 The default is 0.
2960
2961 Returns
2962 -------
2963 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>`_]
2964 Pump efficiencies as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
2965 """
2966 mask = np.zeros(len(self.__sensor_config.pumps))
2967 pump_features = np.array([[default_missing_value] * len(self.__sensor_config.pumps)
2968 for _ in range(len(self.__sensor_readings_time))])
2969 pumps_id = self.__network_topo.get_all_pumps()
2970
2971 efficiency_readings = self.get_data_pumps_efficiency()
2972 for pumps_efficiency_idx, pump_id in enumerate(self.__sensor_config.pump_efficiency_sensors):
2973 idx = pumps_id.index(pump_id)
2974 pump_features[:, idx] = efficiency_readings[:, pumps_efficiency_idx]
2975 mask[idx] = 1
2976
2977 return pump_features, mask
2978
[docs]
2979 def plot_pumps_efficiency(self, sensor_locations: list[str] = None, show: bool = True,
2980 save_to_file: str = None, ax: matplotlib.axes.Axes = None
2981 ) -> matplotlib.axes.Axes:
2982 """
2983 Plots the final pump efficiency sensor readings -- note that those might be subject to
2984 given sensor faults and sensor noise/uncertainty.
2985
2986 Parameters
2987 ----------
2988 sensor_locations : `list[str]`, optional
2989 Existing pump efficiency sensor locations for which the sensor readings
2990 have to be plotted.
2991 If None, the readings from all pump efficiency sensors are plotted.
2992
2993 The default is None.
2994 show : `bool`, optional
2995 If True, the plot/figure is shown in a window.
2996
2997 Only considered when 'ax' is None.
2998
2999 The default is True.
3000 save_to_file : `str`, optional
3001 File to which the plot is saved.
3002
3003 If specified, 'show' must be set to False --
3004 i.e. a plot can not be shown and saved to a file at the same time!
3005
3006 The default is None.
3007 ax : `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_, optional
3008 If not None, 'ax' is used for plotting.
3009
3010 The default is None.
3011
3012 Returns
3013 -------
3014 `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_
3015 Plot.
3016 """
3017 data = self.get_data_pumps_efficiency(sensor_locations)
3018 pump_efficiency_sensors = sensor_locations if sensor_locations is not None else \
3019 self.__sensor_config.pump_efficiency_sensors
3020
3021 return plot_timeseries_data(data.T, labels=[f"Pump {n_id}"
3022 for n_id in pump_efficiency_sensors],
3023 x_axis_label=self.__get_x_axis_label(),
3024 y_axis_label="Efficiency in $%$",
3025 show=show, save_to_file=save_to_file, ax=ax)
3026
[docs]
3027 def get_data_pumps_energyconsumption(self, sensor_locations: list[str] = None) -> np.ndarray:
3028 """
3029 Gets the final pump energy consumption sensor readings -- note that those might be subject
3030 to given sensor faults and sensor noise/uncertainty.
3031
3032 Parameters
3033 ----------
3034 sensor_locations : `list[str]`, optional
3035 Existing pump energy consumption sensor locations for which
3036 the sensor readings are requested.
3037 If None, the readings from all pump energy consumption sensors are returned.
3038
3039 The default is None.
3040
3041 Returns
3042 -------
3043 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
3044 Pump energy consumption sensor readings.
3045 """
3046 if self.__sensor_config.pump_energyconsumption_sensors == []:
3047 raise ValueError("No pump energy consumption sensors set")
3048 if sensor_locations is not None:
3049 if not isinstance(sensor_locations, list):
3050 raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
3051 f"but not of '{type(sensor_locations)}'")
3052 if any(s_id not in self.__sensor_config.pump_energyconsumption_sensors
3053 for s_id in sensor_locations):
3054 raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
3055 "sensors in 'sensor_locations' must be set in the current " +
3056 "pump efficiency sensor configuration")
3057 else:
3058 sensor_locations = self.__sensor_config.pump_energyconsumption_sensors
3059
3060 if self.__sensor_readings is None:
3061 self.get_data()
3062
3063 idx = [self.__sensor_config.get_index_of_reading(pump_energyconsumption_sensor=s_id)
3064 for s_id in sensor_locations]
3065 return self.__sensor_readings[:, idx]
3066
[docs]
3067 def get_data_pumps_energyconsumption_as_node_features(self,
3068 default_missing_value: float = 0.
3069 ) -> tuple[np.ndarray, np.ndarray]:
3070 """
3071 Returns the pump energy consumption as node features together with a boolean mask indicating the
3072 presence of a sensor.
3073
3074 Parameters
3075 ----------
3076 default_missing_value : `float`, optional
3077 Default value (i.e. missing value) for nodes where no pump energy consumption sensor is installed.
3078
3079 The default is 0.
3080
3081 Returns
3082 -------
3083 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>`_]
3084 Pump energy consumptions as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
3085 """
3086 mask = np.zeros(len(self.__sensor_config.pumps))
3087 pump_features = np.array([[default_missing_value] * len(self.__sensor_config.pumps)
3088 for _ in range(len(self.__sensor_readings_time))])
3089 pumps_id = self.__network_topo.get_all_pumps()
3090
3091 energyconsumption_readings = self.get_data_pumps_energyconsumption()
3092 for pumps_energyconsumption_idx, pump_id in enumerate(self.__sensor_config.pump_energyconsumption_sensors):
3093 idx = pumps_id.index(pump_id)
3094 pump_features[:, idx] = energyconsumption_readings[:, pumps_energyconsumption_idx]
3095 mask[idx] = 1
3096
3097 return pump_features, mask
3098
[docs]
3099 def plot_pumps_energyconsumption(self, sensor_locations: list[str] = None, show: bool = True,
3100 save_to_file: str = None, ax: matplotlib.axes.Axes = None
3101 ) -> matplotlib.axes.Axes:
3102 """
3103 Plots the final pump energy consumption sensor readings -- note that those might be
3104 subject to given sensor faults and sensor noise/uncertainty.
3105
3106 Parameters
3107 ----------
3108 sensor_locations : `list[str]`, optional
3109 Existing pump energy consumption sensor locations for which the sensor readings
3110 have to be plotted.
3111 If None, the readings from all pump energy consumption sensors are plotted.
3112
3113 The default is None.
3114 show : `bool`, optional
3115 If True, the plot/figure is shown in a window.
3116
3117 Only considered when 'ax' is None.
3118
3119 The default is True.
3120 save_to_file : `str`, optional
3121 File to which the plot is saved.
3122
3123 If specified, 'show' must be set to False --
3124 i.e. a plot can not be shown and saved to a file at the same time!
3125
3126 The default is None.
3127 ax : `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_, optional
3128 If not None, 'ax' is used for plotting.
3129
3130 The default is None.
3131
3132 Returns
3133 -------
3134 `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_
3135 Plot.
3136 """
3137 data = self.get_data_pumps_energyconsumption(sensor_locations)
3138 pump_energyconsumption_sensors = sensor_locations if sensor_locations is not None else \
3139 self.__sensor_config.pump_energyconsumption_sensors
3140
3141 return plot_timeseries_data(data.T, labels=[f"Pump {n_id}"
3142 for n_id in pump_energyconsumption_sensors],
3143 x_axis_label=self.__get_x_axis_label(),
3144 y_axis_label="Energy consumption in $kilowatt - hour$",
3145 show=show, save_to_file=save_to_file, ax=ax)
3146
[docs]
3147 def get_data_valves_state(self, sensor_locations: list[str] = None) -> np.ndarray:
3148 """
3149 Gets the final valve state sensor readings -- note that those might be subject to
3150 given sensor faults and sensor noise/uncertainty.
3151
3152 Parameters
3153 ----------
3154 sensor_locations : `list[str]`, optional
3155 Existing valve state sensor locations for which the sensor readings are requested.
3156 If None, the readings from all valve state sensors are returned.
3157
3158 The default is None.
3159
3160 Returns
3161 -------
3162 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
3163 Valve state sensor readings.
3164 """
3165 if self.__sensor_config.valve_state_sensors == []:
3166 raise ValueError("No valve state sensors set")
3167 if sensor_locations is not None:
3168 if not isinstance(sensor_locations, list):
3169 raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
3170 f"but not of '{type(sensor_locations)}'")
3171 if any(s_id not in self.__sensor_config.valve_state_sensors
3172 for s_id in sensor_locations):
3173 raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
3174 "sensors in 'sensor_locations' must be set in the current " +
3175 "valve state sensor configuration")
3176 else:
3177 sensor_locations = self.__sensor_config.valve_state_sensors
3178
3179 if self.__sensor_readings is None:
3180 self.get_data()
3181
3182 idx = [self.__sensor_config.get_index_of_reading(valve_state_sensor=s_id)
3183 for s_id in sensor_locations]
3184 return self.__sensor_readings[:, idx]
3185
[docs]
3186 def get_data_valves_state_as_node_features(self,
3187 default_missing_value: float = 0.
3188 ) -> tuple[np.ndarray, np.ndarray]:
3189 """
3190 Returns the valves state as node features together with a boolean mask indicating the
3191 presence of a sensor.
3192
3193 Parameters
3194 ----------
3195 default_missing_value : `float`, optional
3196 Default value (i.e. missing value) for nodes where no valves state sensor is installed.
3197
3198 The default is 0.
3199
3200 Returns
3201 -------
3202 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>`_]
3203 Valves state as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
3204 """
3205 mask = np.zeros(len(self.__sensor_config.valves))
3206 valve_features = np.array([[default_missing_value] * len(self.__sensor_config.valves)
3207 for _ in range(len(self.__sensor_readings_time))])
3208 valves_id = self.__network_topo.get_all_valves()
3209
3210 state_readings = self.get_data_valves_state()
3211 for valves_state_idx, valve_id in enumerate(self.__sensor_config.valve_state_sensors):
3212 idx = valves_id.index(valve_id)
3213 valve_features[:, idx] = state_readings[:, valves_state_idx]
3214 mask[idx] = 1
3215
3216 return valve_features, mask
3217
[docs]
3218 def plot_valves_state(self, sensor_locations: list[str] = None, show: bool = True,
3219 save_to_file: str = None, ax: matplotlib.axes.Axes = None
3220 ) -> matplotlib.axes.Axes:
3221 """
3222 Plots the final valve state sensor readings -- note that those might be subject to
3223 given sensor faults and sensor noise/uncertainty.
3224
3225 Parameters
3226 ----------
3227 sensor_locations : `list[str]`, optional
3228 Existing valve state sensor locations for which the sensor readings have to be plotted.
3229 If None, the readings from all valve state sensors are plotted.
3230
3231 The default is None.
3232 show : `bool`, optional
3233 If True, the plot/figure is shown in a window.
3234
3235 Only considered when 'ax' is None.
3236
3237 The default is True.
3238 save_to_file : `str`, optional
3239 File to which the plot is saved.
3240
3241 If specified, 'show' must be set to False --
3242 i.e. a plot can not be shown and saved to a file at the same time!
3243
3244 The default is None.
3245 ax : `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_, optional
3246 If not None, 'ax' is used for plotting.
3247
3248 The default is None.
3249
3250 Returns
3251 -------
3252 `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_
3253 Plot.
3254 """
3255 data = self.get_data_valves_state(sensor_locations)
3256 valve_state_sensors = sensor_locations if sensor_locations is not None else \
3257 self.__sensor_config.valve_state_sensors
3258
3259 return plot_timeseries_data(data.T, labels=[f"Valve {n_id}"
3260 for n_id in valve_state_sensors],
3261 x_axis_label=self.__get_x_axis_label(),
3262 y_axis_label="Valve state",
3263 y_ticks=([2.0, 3.0], ["Closed", "Open"]),
3264 show=show, save_to_file=save_to_file, ax=ax)
3265
[docs]
3266 def get_data_tanks_water_volume(self, sensor_locations: list[str] = None) -> np.ndarray:
3267 """
3268 Gets the final water tanks volume sensor readings -- note that those might be subject to
3269 given sensor faults and sensor noise/uncertainty.
3270
3271 Parameters
3272 ----------
3273 sensor_locations : `list[str]`, optional
3274 Existing flow sensor locations for which the sensor readings are requested.
3275 If None, the readings from all water tanks volume sensors are returned.
3276
3277 The default is None.
3278
3279 Returns
3280 -------
3281 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
3282 Water tanks volume sensor readings.
3283 """
3284 if self.__sensor_config.tank_volume_sensors == []:
3285 raise ValueError("No tank volume sensors set")
3286 if sensor_locations is not None:
3287 if not isinstance(sensor_locations, list):
3288 raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
3289 f"but not of '{type(sensor_locations)}'")
3290 if any(s_id not in self.__sensor_config.tank_volume_sensors
3291 for s_id in sensor_locations):
3292 raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
3293 "sensors in 'sensor_locations' must be set in the current " +
3294 "water tanks volume sensor configuration")
3295 else:
3296 sensor_locations = self.__sensor_config.tank_volume_sensors
3297
3298 if self.__sensor_readings is None:
3299 self.get_data()
3300
3301 idx = [self.__sensor_config.get_index_of_reading(tank_volume_sensor=s_id)
3302 for s_id in sensor_locations]
3303 return self.__sensor_readings[:, idx]
3304
[docs]
3305 def get_data_tanks_water_volume_as_node_features(self,
3306 default_missing_value: float = 0.
3307 ) -> tuple[np.ndarray, np.ndarray]:
3308 """
3309 Returns the tank water volume as node features together with a boolean mask indicating the
3310 presence of a sensor.
3311
3312 Parameters
3313 ----------
3314 default_missing_value : `float`, optional
3315 Default value (i.e. missing value) for nodes where no tank water volume sensor is installed.
3316
3317 The default is 0.
3318
3319 Returns
3320 -------
3321 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>`_]
3322 Tank water volumes as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
3323 """
3324 mask = np.zeros(len(self.__sensor_config.tanks))
3325 tank_features = np.array([[default_missing_value] * len(self.__sensor_config.tanks)
3326 for _ in range(len(self.__sensor_readings_time))])
3327 tanks_id = self.__network_topo.get_all_tanks()
3328
3329 water_volume_readings = self.get_data_tanks_water_volume()
3330 for tanks_water_volume_idx, tank_id in enumerate(self.__sensor_config.tank_volume_sensors):
3331 idx = tanks_id.index(tank_id)
3332 tank_features[:, idx] = water_volume_readings[:, tanks_water_volume_idx]
3333 mask[idx] = 1
3334
3335 return tank_features, mask
3336
[docs]
3337 def plot_tanks_water_volume(self, sensor_locations: list[str] = None, show: bool = True,
3338 save_to_file: str = None, ax: matplotlib.axes.Axes = None
3339 ) -> matplotlib.axes.Axes:
3340 """
3341 Plots the final water tanks volume sensor readings -- note that those might be subject to
3342 given sensor faults and sensor noise/uncertainty.
3343
3344 Parameters
3345 ----------
3346 sensor_locations : `list[str]`, optional
3347 Existing flow sensor locations for which the sensor readings have to be plotted.
3348 If None, the readings from all water tanks volume sensors are plotted.
3349
3350 The default is None.
3351 show : `bool`, optional
3352 If True, the plot/figure is shown in a window.
3353
3354 Only considered when 'ax' is None.
3355
3356 The default is True.
3357 save_to_file : `str`, optional
3358 File to which the plot is saved.
3359
3360 If specified, 'show' must be set to False --
3361 i.e. a plot can not be shown and saved to a file at the same time!
3362
3363 The default is None.
3364 ax : `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_, optional
3365 If not None, 'ax' is used for plotting.
3366
3367 The default is None.
3368
3369 Returns
3370 -------
3371 `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_
3372 Plot.
3373 """
3374 data = self.get_data_tanks_water_volume(sensor_locations)
3375 tank_volume_sensors = sensor_locations if sensor_locations is not None else \
3376 self.__sensor_config.tank_volume_sensors
3377
3378 volume_unit = "m^3" if is_flowunit_simetric(self.__sensor_config.flow_unit) else "feet^3"
3379 y_axis_label = f"Water volume in ${volume_unit}$"
3380
3381 return plot_timeseries_data(data.T, labels=[f"Tank {n_id}"
3382 for n_id in tank_volume_sensors],
3383 x_axis_label=self.__get_x_axis_label(),
3384 y_axis_label=y_axis_label,
3385 show=show, save_to_file=save_to_file, ax=ax)
3386
[docs]
3387 def get_data_surface_species_concentration(self,
3388 surface_species_sensor_locations: dict = None
3389 ) -> np.ndarray:
3390 """
3391 Gets the final surface species concentration sensor readings --
3392 note that those might be subject to given sensor faults and sensor noise/uncertainty.
3393
3394 Parameters
3395 ----------
3396 surface_species_sensor_locations : `dict`, optional
3397 Existing surface species concentration sensors (species ID and link/pipe IDs) for which
3398 the sensor readings are requested.
3399 If None, the readings from all surface species concentration sensors are returned.
3400
3401 The default is None.
3402
3403 Returns
3404 -------
3405 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
3406 Surface species concentration sensor readings.
3407 """
3408 if self.__sensor_config.surface_species_sensors == {}:
3409 raise ValueError("No surface species sensors set")
3410 if surface_species_sensor_locations is not None:
3411 if not isinstance(surface_species_sensor_locations, dict):
3412 raise TypeError("'surface_species_sensor_locations' must be an instance of 'dict'" +
3413 f" but not of '{type(surface_species_sensor_locations)}'")
3414 for species_id in surface_species_sensor_locations:
3415 if species_id not in self.__sensor_config.surface_species_sensors:
3416 raise ValueError(f"Species '{species_id}' is not included in the " +
3417 "sensor configuration")
3418
3419 my_surface_species_sensor_locations = \
3420 self.__sensor_config.surface_species_sensors[species_id]
3421 for sensor_id in surface_species_sensor_locations[species_id]:
3422 if sensor_id not in my_surface_species_sensor_locations:
3423 raise ValueError(f"Link '{sensor_id}' is not included in the " +
3424 f"sensor configuration for species '{species_id}'")
3425 else:
3426 surface_species_sensor_locations = self.__sensor_config.surface_species_sensors
3427
3428 if self.__sensor_readings is None:
3429 self.get_data()
3430
3431 idx = [self.__sensor_config.get_index_of_reading(
3432 surface_species_sensor=(species_id, link_id))
3433 for species_id in surface_species_sensor_locations
3434 for link_id in surface_species_sensor_locations[species_id]]
3435 return self.__sensor_readings[:, idx]
3436
[docs]
3437 def get_data_surface_species_concentrations_as_edge_features(self,
3438 default_missing_value: float = 0.
3439 ) -> tuple[np.ndarray, np.ndarray]:
3440 """
3441 Returns the concentrations of surface species as edge features together with a
3442 boolean mask indicating the presence of a sensor.
3443
3444 Note that only surface species with at least one sensor are considered.
3445
3446 Parameters
3447 ----------
3448 default_missing_value : `float`, optional
3449 Default value (i.e. missing value) for links where no surface species
3450 sensor is installed.
3451
3452 The default is 0.
3453
3454 Returns
3455 -------
3456 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>`_]
3457 Concentrations of surface species as edge features of shape
3458 [num_time_steps, num_links * 2, num_species], and mask of
3459 shape [num_links * 2, num_species].
3460 """
3461 masks = []
3462 results = []
3463
3464 surface_species_sensor_locations = self.__sensor_config.surface_species_sensors
3465 for species_id, links_id in surface_species_sensor_locations.items():
3466 mask = np.zeros(2 * len(self.__sensor_config.links))
3467 edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
3468 for _ in range(len(self.__sensor_readings_time))])
3469
3470 sensor_readings = self.get_data_surface_species_concentration({species_id: links_id})
3471 for sensor_readings_idx, link_id in enumerate(links_id):
3472 for idx in self.map_link_id_to_edge_idx(link_id):
3473 edge_features[:, idx] = sensor_readings[:, sensor_readings_idx]
3474 mask[idx] = 1
3475
3476 results.append(edge_features.reshape(edge_features.shape[0], edge_features.shape[1], 1))
3477 masks.append(mask.reshape(-1, 1))
3478
3479 return np.concatenate(results, axis=2), np.concatenate(masks, axis=1)
3480
[docs]
3481 def plot_surface_species_concentration(self, surface_species_sensor_locations: dict = None,
3482 show: bool = True, save_to_file: str = None,
3483 ax: matplotlib.axes.Axes = None
3484 ) -> matplotlib.axes.Axes:
3485 """
3486 Plots the final surface species concentration sensor readings -- note that those might be
3487 subject to given sensor faults and sensor noise/uncertainty.
3488
3489 Parameters
3490 ----------
3491 surface_species_sensor_locations : `dict`, optional
3492 Existing surface species concentration sensors (species ID and link/pipe IDs) for which
3493 the sensor readings have to be plotted.
3494 If None, the readings from all surface species concentration sensors are plotted.
3495
3496 The default is None.
3497 show : `bool`, optional
3498 If True, the plot/figure is shown in a window.
3499
3500 Only considered when 'ax' is None.
3501
3502 The default is True.
3503 save_to_file : `str`, optional
3504 File to which the plot is saved.
3505
3506 If specified, 'show' must be set to False --
3507 i.e. a plot can not be shown and saved to a file at the same time!
3508
3509 The default is None.
3510 ax : `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_, optional
3511 If not None, 'ax' is used for plotting.
3512
3513 The default is None.
3514
3515 Returns
3516 -------
3517 `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_
3518 Plot.
3519 """
3520 data = self.get_data_surface_species_concentration(surface_species_sensor_locations)
3521 if surface_species_sensor_locations is None:
3522 surface_species_sensor_locations = self.__sensor_config.surface_species_sensors
3523
3524 area_unit = self.__sensor_config.surface_species_area_unit
3525 concentration_unit = None
3526 labels = []
3527 for species_id in surface_species_sensor_locations:
3528 mass_unit = self.__sensor_config.get_surface_species_mass_unit_id(species_id)
3529 if concentration_unit is not None:
3530 if concentration_unit != mass_unit:
3531 raise ValueError("Can not plot species with different mass units")
3532 concentration_unit = mass_unit
3533 else:
3534 concentration_unit = mass_unit
3535
3536 for link_id in surface_species_sensor_locations[species_id]:
3537 labels.append(f"{species_id} @ link {link_id}")
3538
3539 y_axis_label = f"Concentration in ${massunit_to_str(concentration_unit)}/" +\
3540 f"{areaunit_to_str(area_unit)}$"
3541
3542 return plot_timeseries_data(data.T, labels=labels,
3543 x_axis_label=self.__get_x_axis_label(),
3544 y_axis_label=y_axis_label,
3545 show=show, save_to_file=save_to_file, ax=ax)
3546
[docs]
3547 def get_data_bulk_species_node_concentration(self,
3548 bulk_species_sensor_locations: dict = None
3549 ) -> np.ndarray:
3550 """
3551 Gets the final bulk species node concentration sensor readings --
3552 note that those might be subject to given sensor faults and sensor noise/uncertainty.
3553
3554 Parameters
3555 ----------
3556 bulk_species_sensor_locations : `dict`, optional
3557 Existing bulk species concentration sensors (species ID and node IDs) for which
3558 the sensor readings are requested.
3559 If None, the readings from all bulk species node concentration sensors are returned.
3560
3561 The default is None.
3562
3563 Returns
3564 -------
3565 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
3566 Bulk species concentration sensor readings.
3567 """
3568 if self.__sensor_config.bulk_species_node_sensors == {}:
3569 raise ValueError("No bulk species node sensors set")
3570 if bulk_species_sensor_locations is not None:
3571 if not isinstance(bulk_species_sensor_locations, dict):
3572 raise TypeError("'bulk_species_sensor_locations' must be an instance of 'dict'" +
3573 f" but not of '{type(bulk_species_sensor_locations)}'")
3574 for species_id in bulk_species_sensor_locations:
3575 if species_id not in self.__sensor_config.bulk_species_node_sensors:
3576 raise ValueError(f"Species '{species_id}' is not included in the " +
3577 "sensor configuration")
3578
3579 my_bulk_species_sensor_locations = \
3580 self.__sensor_config.bulk_species_node_sensors[species_id]
3581 for sensor_id in bulk_species_sensor_locations[species_id]:
3582 if sensor_id not in my_bulk_species_sensor_locations:
3583 raise ValueError(f"Link '{sensor_id}' is not included in the " +
3584 f"sensor configuration for species '{species_id}'")
3585 else:
3586 bulk_species_sensor_locations = self.__sensor_config.bulk_species_node_sensors
3587
3588 if self.__sensor_readings is None:
3589 self.get_data()
3590
3591 idx = [self.__sensor_config.get_index_of_reading(
3592 bulk_species_node_sensor=(species_id, node_id))
3593 for species_id in bulk_species_sensor_locations
3594 for node_id in bulk_species_sensor_locations[species_id]]
3595 return self.__sensor_readings[:, idx]
3596
[docs]
3597 def get_data_bulk_species_concentrations_as_node_features(self,
3598 default_missing_value: float = 0.
3599 ) -> tuple[np.ndarray, np.ndarray]:
3600 """
3601 Returns the concentrations of bulk species as node features together with a boolean mask
3602 indicating the presence of a sensor.
3603
3604 Note that only bulk species with at least one sensor are considered.
3605
3606 Parameters
3607 ----------
3608 default_missing_value : `float`, optional
3609 Default value (i.e. missing value) for nodes where no bulk species
3610 sensor is installed.
3611
3612 The default is 0.
3613
3614 Returns
3615 -------
3616 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>`_]
3617 Concentrations of bulk species as node features of shape
3618 [num_time_steps, num_nodes, num_species], and mask of shape [num_nodes, num_species].
3619 """
3620 masks = []
3621 results = []
3622
3623 bulk_species_sensor_locations = self.__sensor_config.bulk_species_node_sensors
3624
3625 for species_id, nodes_id in bulk_species_sensor_locations.items():
3626 mask = np.zeros(len(self.__sensor_config.nodes))
3627 node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
3628 for _ in range(len(self.__sensor_readings_time))])
3629
3630 sensor_readings = self.get_data_bulk_species_node_concentration({species_id: nodes_id})
3631 for sensor_readings_idx, node_id in enumerate(nodes_id):
3632 idx = self.__sensor_config.map_node_id_to_idx(node_id)
3633 node_features[:, idx] = sensor_readings[:, sensor_readings_idx]
3634 mask[idx] = 1
3635
3636 results.append(node_features.reshape(node_features.shape[0], node_features.shape[1],1))
3637 masks.append(mask.reshape(-1, 1))
3638
3639 return np.concatenate(results, axis=2), np.concatenate(masks, axis=1)
3640
[docs]
3641 def plot_bulk_species_node_concentration(self, bulk_species_node_sensors: dict = None,
3642 show: bool = True, save_to_file: str = None,
3643 ax: matplotlib.axes.Axes = None
3644 ) -> matplotlib.axes.Axes:
3645 """
3646 Plots the final bulk species node concentration sensor readings --
3647 note that those might be subject to given sensor faults and sensor noise/uncertainty.
3648
3649 Parameters
3650 ----------
3651 bulk_species_node_sensors : `dict`, optional
3652 Existing bulk species concentration sensors (species ID and node IDs) for which
3653 the sensor readings are requested.
3654 If None, the readings from all bulk species node concentration sensors are returned.
3655
3656 The default is None.
3657 show : `bool`, optional
3658 If True, the plot/figure is shown in a window.
3659
3660 Only considered when 'ax' is None.
3661
3662 The default is True.
3663 save_to_file : `str`, optional
3664 File to which the plot is saved.
3665
3666 If specified, 'show' must be set to False --
3667 i.e. a plot can not be shown and saved to a file at the same time!
3668
3669 The default is None.
3670 ax : `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_, optional
3671 If not None, 'ax' is used for plotting.
3672
3673 The default is None.
3674
3675 Returns
3676 -------
3677 `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_
3678 Plot.
3679 """
3680 data = self.get_data_bulk_species_node_concentration(bulk_species_node_sensors)
3681 if bulk_species_node_sensors is None:
3682 bulk_species_node_sensors = self.__sensor_config.bulk_species_node_sensors
3683
3684 concentration_unit = None
3685 labels = []
3686 for species_id in bulk_species_node_sensors:
3687 mass_unit = self.__sensor_config.get_bulk_species_mass_unit_id(species_id)
3688 if concentration_unit is not None:
3689 if concentration_unit != mass_unit:
3690 raise ValueError("Can not plot species with different mass units")
3691 concentration_unit = mass_unit
3692 else:
3693 concentration_unit = mass_unit
3694
3695 for node_id in bulk_species_node_sensors[species_id]:
3696 labels.append(f"{species_id} @ node {node_id}")
3697
3698 y_axis_label = f"Concentration in ${massunit_to_str(concentration_unit)}/L$"
3699
3700 return plot_timeseries_data(data.T, labels=labels,
3701 x_axis_label=self.__get_x_axis_label(),
3702 y_axis_label=y_axis_label,
3703 show=show, save_to_file=save_to_file, ax=ax)
3704
[docs]
3705 def get_data_bulk_species_link_concentration(self,
3706 bulk_species_sensor_locations: dict = None
3707 ) -> np.ndarray:
3708 """
3709 Gets the final bulk species link/pipe concentration sensor readings --
3710 note that those might be subject to given sensor faults and sensor noise/uncertainty.
3711
3712 Parameters
3713 ----------
3714 bulk_species_sensor_locations : `dict`, optional
3715 Existing bulk species concentration sensors (species ID and link/pipe IDs) for which
3716 the sensor readings are requested.
3717 If None, the readings from all bulk species concentration link/pipe sensors
3718 are returned.
3719
3720 The default is None.
3721
3722 Returns
3723 -------
3724 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
3725 Bulk species concentration sensor readings.
3726 """
3727 if self.__sensor_config.bulk_species_link_sensors == {}:
3728 raise ValueError("No bulk species link/pipe sensors set")
3729 if bulk_species_sensor_locations is not None:
3730 if not isinstance(bulk_species_sensor_locations, dict):
3731 raise TypeError("'bulk_species_sensor_locations' must be an instance of 'dict'" +
3732 f" but not of '{type(bulk_species_sensor_locations)}'")
3733 for species_id in bulk_species_sensor_locations:
3734 if species_id not in self.__sensor_config.bulk_species_link_sensors:
3735 raise ValueError(f"Species '{species_id}' is not included in the " +
3736 "sensor configuration")
3737
3738 my_bulk_species_sensor_locations = \
3739 self.__sensor_config.bulk_species_link_sensors[species_id]
3740 for sensor_id in bulk_species_sensor_locations[species_id]:
3741 if sensor_id not in my_bulk_species_sensor_locations:
3742 raise ValueError(f"Link '{sensor_id}' is not included in the " +
3743 f"sensor configuration for species '{species_id}'")
3744 else:
3745 bulk_species_sensor_locations = self.__sensor_config.bulk_species_link_sensors
3746
3747 if self.__sensor_readings is None:
3748 self.get_data()
3749
3750 idx = [self.__sensor_config.get_index_of_reading(
3751 bulk_species_link_sensor=(species_id, node_id))
3752 for species_id in bulk_species_sensor_locations
3753 for node_id in bulk_species_sensor_locations[species_id]]
3754 return self.__sensor_readings[:, idx]
3755
[docs]
3756 def get_data_bulk_species_concentrations_as_edge_features(self,
3757 default_missing_value: float = 0.
3758 ) -> tuple[np.ndarray, np.ndarray]:
3759 """
3760 Returns the concentrations of bulk species as edge features together with a boolean mask
3761 indicating the presence of a sensor.
3762
3763 Note that only bulk species with at least one sensor are considered.
3764
3765 Parameters
3766 ----------
3767 default_missing_value : `float`, optional
3768 Default value (i.e. missing value) for links where no bulk species
3769 sensor is installed.
3770
3771 The default is 0.
3772
3773 Returns
3774 -------
3775 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>`_]
3776 Concentrations of bulk species as edge features of shape
3777 [num_time_steps, num_links * 2, num_species], and mask of
3778 shape [num_links * 2, num_species].
3779 """
3780 masks = []
3781 results = []
3782
3783 bulk_species_sensor_locations = self.__sensor_config.bulk_species_link_sensors
3784 for species_id, links_id in bulk_species_sensor_locations.items():
3785 mask = np.zeros(2 * len(self.__sensor_config.links))
3786 edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
3787 for _ in range(len(self.__sensor_readings_time))])
3788
3789 sensor_readings = self.get_data_bulk_species_link_concentration({species_id: links_id})
3790 for sensor_readings_idx, link_id in enumerate(links_id):
3791 for idx in self.map_link_id_to_edge_idx(link_id):
3792 edge_features[:, idx] = sensor_readings[:, sensor_readings_idx]
3793 mask[idx] = 1
3794
3795 results.append(edge_features.reshape(edge_features.shape[0], edge_features.shape[1], 1))
3796 masks.append(mask.reshape(-1, 1))
3797
3798 return np.concatenate(results, axis=2), np.concatenate(masks, axis=1)
3799
[docs]
3800 def plot_bulk_species_link_concentration(self, bulk_species_link_sensors: dict = None,
3801 show: bool = True, save_to_file: str = None,
3802 ax: matplotlib.axes.Axes = None
3803 ) -> matplotlib.axes.Axes:
3804 """
3805 Plots the final bulk species link concentration sensor readings -- note that those might be
3806 subject to given sensor faults and sensor noise/uncertainty.
3807
3808 Parameters
3809 ----------
3810 bulk_species_link_sensors : `dict`, optional
3811 Existing bulk species link concentration sensors (species ID and link/pipe IDs) for which
3812 the sensor readings have to be plotted.
3813 If None, the readings from all bulk species link concentration sensors are plotted.
3814
3815 The default is None.
3816 show : `bool`, optional
3817 If True, the plot/figure is shown in a window.
3818
3819 Only considered when 'ax' is None.
3820
3821 The default is True.
3822 save_to_file : `str`, optional
3823 File to which the plot is saved.
3824
3825 If specified, 'show' must be set to False --
3826 i.e. a plot can not be shown and saved to a file at the same time!
3827
3828 The default is None.
3829 ax : `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_, optional
3830 If not None, 'ax' is used for plotting.
3831
3832 The default is None.
3833
3834 Returns
3835 -------
3836 `matplotlib.axes.Axes <https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.html>`_
3837 Plot.
3838 """
3839 data = self.get_data_bulk_species_link_concentration(bulk_species_link_sensors)
3840 if bulk_species_link_sensors is None:
3841 bulk_species_link_sensors = self.__sensor_config.bulk_species_link_sensors
3842
3843 area_unit = self.__sensor_config.surface_species_area_unit
3844 concentration_unit = None
3845 labels = []
3846 for species_id in bulk_species_link_sensors:
3847 mass_unit = self.__sensor_config.get_bulk_species_mass_unit_id(species_id)
3848 if concentration_unit is not None:
3849 if concentration_unit != mass_unit:
3850 raise ValueError("Can not plot species with different mass units")
3851 concentration_unit = mass_unit
3852 else:
3853 concentration_unit = mass_unit
3854
3855 for link_id in bulk_species_link_sensors[species_id]:
3856 labels.append(f"{species_id} @ link {link_id}")
3857
3858 y_axis_label = f"Concentration in ${massunit_to_str(concentration_unit)}/" +\
3859 f"{areaunit_to_str(area_unit)}$"
3860
3861 return plot_timeseries_data(data.T, labels=labels,
3862 x_axis_label=self.__get_x_axis_label(),
3863 y_axis_label=y_axis_label,
3864 show=show, save_to_file=save_to_file, ax=ax)
3865
[docs]
3866 def to_pandas_dataframe(self, export_raw_data: bool = False) -> pd.DataFrame:
3867 """
3868 Exports this SCADA data to a Pandas dataframe.
3869
3870 Parameters
3871 ----------
3872 export_raw_data : `bool`, optional
3873 If True, the raw measurements (i.e. sensor reading without any noise or faults)
3874 are exported instead of the final sensor readings.
3875
3876 The default is False.
3877
3878 Returns
3879 -------
3880 `pandas.DataFrame <https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.html>`_
3881 Exported data.
3882 """
3883 from .scada_data_export import ScadaDataExport
3884
3885 old_sensor_config = None
3886 if export_raw_data is True:
3887 # Backup old sensor config and set a new one with sensors everywhere
3888 old_sensor_config = self.sensor_config
3889 self.change_sensor_config(ScadaDataExport.create_global_sensor_config(self))
3890
3891 sensor_readings = self.get_data()
3892 col_desc = ScadaDataExport.create_column_desc(self)
3893 columns = [f"{sensor_type} [{unit_desc}] at {item_id}" for sensor_type, item_id, unit_desc in col_desc]
3894
3895 data = {col_desc: sensor_readings[:, c_id] for c_id, col_desc in enumerate(columns)}
3896
3897 if export_raw_data is True:
3898 # Restore old sensor config
3899 self.change_sensor_config(old_sensor_config)
3900
3901 return pd.DataFrame(data)
3902
[docs]
3903 def to_numpy_file(self, f_out: str, export_raw_data: bool = False) -> None:
3904 """
3905 Exporting this SCADA data to Numpy (.npz file).
3906
3907 Parameters
3908 ----------
3909 f_out : `str`
3910 Path to the .npz file to which the SCADA data will be exported.
3911 export_raw_data : `bool`, optional
3912 If True, the raw measurements (i.e. sensor reading without any noise or faults)
3913 are exported instead of the final sensor readings.
3914
3915 The default is False.
3916 """
3917 from .scada_data_export import ScadaDataNumpyExport
3918 ScadaDataNumpyExport(f_out, export_raw_data).export(self)
3919
[docs]
3920 def to_excel_file(self, f_out: str, export_raw_data: bool = False) -> None:
3921 """
3922 Exporting this SCADA data to MS Excel (.xlsx file).
3923
3924 Parameters
3925 ----------
3926 f_out : `str`
3927 Path to the .xlsx file to which the SCADA data will be exported.
3928 export_raw_data : `bool`, optional
3929 If True, the raw measurements (i.e. sensor reading without any noise or faults)
3930 are exported instead of the final sensor readings.
3931
3932 The default is False.
3933 """
3934 from .scada_data_export import ScadaDataXlsxExport
3935 ScadaDataXlsxExport(f_out, export_raw_data).export(self)
3936
[docs]
3937 def to_matlab_file(self, f_out: str, export_raw_data: bool = False) -> None:
3938 """
3939 Exporting this SCADA data to Matlab (.mat file).
3940
3941 Parameters
3942 ----------
3943 f_out : `str`
3944 Path to the .mat file to which the SCADA data will be exported.
3945 export_raw_data : `bool`, optional
3946 If True, the raw measurements (i.e. sensor reading without any noise or faults)
3947 are exported instead of the final sensor readings.
3948
3949 The default is False.
3950 """
3951 from .scada_data_export import ScadaDataMatlabExport
3952 ScadaDataMatlabExport(f_out, export_raw_data).export(self)