epyt-flow 0.7.3__py3-none-any.whl → 0.8.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,10 +2,12 @@
2
2
  Module provides a class for storing and processing SCADA data.
3
3
  """
4
4
  import warnings
5
- from typing import Callable, Any
5
+ from typing import Callable, Any, Union
6
6
  from copy import deepcopy
7
7
  import numpy as np
8
+ from scipy.sparse import bsr_array
8
9
  import matplotlib
10
+ import pandas as pd
9
11
  from epyt.epanet import ToolkitConstants
10
12
 
11
13
  from ..sensor_config import SensorConfig, is_flowunit_simetric, massunit_to_str, flowunit_to_str,\
@@ -127,13 +129,19 @@ class ScadaData(Serializable):
127
129
  The default is False.
128
130
  """
129
131
  def __init__(self, sensor_config: SensorConfig, sensor_readings_time: np.ndarray,
130
- pressure_data_raw: np.ndarray = None, flow_data_raw: np.ndarray = None,
131
- demand_data_raw: np.ndarray = None, node_quality_data_raw: np.ndarray = None,
132
- link_quality_data_raw: np.ndarray = None, pumps_state_data_raw: np.ndarray = None,
133
- valves_state_data_raw: np.ndarray = None, tanks_volume_data_raw: np.ndarray = None,
134
- surface_species_concentration_raw: np.ndarray = None,
135
- bulk_species_node_concentration_raw: np.ndarray = None,
136
- bulk_species_link_concentration_raw: np.ndarray = None,
132
+ pressure_data_raw: Union[np.ndarray, bsr_array] = None,
133
+ flow_data_raw: Union[np.ndarray, bsr_array] = None,
134
+ demand_data_raw: Union[np.ndarray, bsr_array] = None,
135
+ node_quality_data_raw: Union[np.ndarray, bsr_array] = None,
136
+ link_quality_data_raw: Union[np.ndarray, bsr_array] = None,
137
+ pumps_state_data_raw: Union[np.ndarray, bsr_array] = None,
138
+ valves_state_data_raw: Union[np.ndarray, bsr_array] = None,
139
+ tanks_volume_data_raw: Union[np.ndarray, bsr_array] = None,
140
+ surface_species_concentration_raw: Union[np.ndarray, dict[int, bsr_array]] = None,
141
+ bulk_species_node_concentration_raw: Union[np.ndarray,
142
+ dict[int, bsr_array]] = None,
143
+ bulk_species_link_concentration_raw: Union[np.ndarray,
144
+ dict[int, bsr_array]] = None,
137
145
  pump_energy_usage_data = None,
138
146
  pump_efficiency_data = None,
139
147
  pumps_energy_usage_data_raw: np.ndarray = None,
@@ -151,64 +159,116 @@ class ScadaData(Serializable):
151
159
  raise TypeError("'sensor_readings_time' must be an instance of 'numpy.ndarray' " +
152
160
  f"but not of '{type(sensor_readings_time)}'")
153
161
  if pressure_data_raw is not None:
154
- if not isinstance(pressure_data_raw, np.ndarray):
162
+ if not isinstance(pressure_data_raw, np.ndarray) and \
163
+ not isinstance(pressure_data_raw, bsr_array):
155
164
  raise TypeError("'pressure_data_raw' must be an instance of 'numpy.ndarray'" +
156
165
  f" but not of '{type(pressure_data_raw)}'")
166
+ if isinstance(pressure_data_raw, bsr_array) and not frozen_sensor_config:
167
+ raise ValueError("'pressure_data_raw' can only be an instance of " +
168
+ "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
157
169
  if flow_data_raw is not None:
158
- if not isinstance(flow_data_raw, np.ndarray):
170
+ if not isinstance(flow_data_raw, np.ndarray) and \
171
+ not isinstance(flow_data_raw, bsr_array):
159
172
  raise TypeError("'flow_data_raw' must be an instance of 'numpy.ndarray' " +
160
173
  f"but not of '{type(flow_data_raw)}'")
174
+ if isinstance(flow_data_raw, bsr_array) and not frozen_sensor_config:
175
+ raise ValueError("'flow_data_raw' can only be an instance of " +
176
+ "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
161
177
  if demand_data_raw is not None:
162
- if not isinstance(demand_data_raw, np.ndarray):
178
+ if not isinstance(demand_data_raw, np.ndarray) and \
179
+ not isinstance(demand_data_raw, bsr_array):
163
180
  raise TypeError("'demand_data_raw' must be an instance of 'numpy.ndarray' " +
164
181
  f"but not of '{type(demand_data_raw)}'")
182
+ if isinstance(demand_data_raw, bsr_array) and not frozen_sensor_config:
183
+ raise ValueError("'demand_data_raw' can only be an instance of " +
184
+ "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
165
185
  if node_quality_data_raw is not None:
166
- if not isinstance(node_quality_data_raw, np.ndarray):
186
+ if not isinstance(node_quality_data_raw, np.ndarray) and \
187
+ not isinstance(node_quality_data_raw, bsr_array):
167
188
  raise TypeError("'node_quality_data_raw' must be an instance of 'numpy.ndarray'" +
168
189
  f" but not of '{type(node_quality_data_raw)}'")
190
+ if isinstance(node_quality_data_raw, bsr_array) and not frozen_sensor_config:
191
+ raise ValueError("'node_quality_data_raw' can only be an instance of " +
192
+ "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
169
193
  if link_quality_data_raw is not None:
170
- if not isinstance(link_quality_data_raw, np.ndarray):
194
+ if not isinstance(link_quality_data_raw, np.ndarray) and \
195
+ not isinstance(link_quality_data_raw, bsr_array):
171
196
  raise TypeError("'link_quality_data_raw' must be an instance of 'numpy.ndarray'" +
172
197
  f" but not of '{type(link_quality_data_raw)}'")
198
+ if isinstance(link_quality_data_raw, bsr_array) and not frozen_sensor_config:
199
+ raise ValueError("'link_quality_data_raw' can only be an instance of " +
200
+ "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
173
201
  if pumps_state_data_raw is not None:
174
- if not isinstance(pumps_state_data_raw, np.ndarray):
202
+ if not isinstance(pumps_state_data_raw, np.ndarray) and \
203
+ not isinstance(pumps_state_data_raw, bsr_array):
175
204
  raise TypeError("'pumps_state_data_raw' must be an instance of 'numpy.ndarray' " +
176
205
  f"but no of '{type(pumps_state_data_raw)}'")
206
+ if isinstance(pumps_state_data_raw, bsr_array) and not frozen_sensor_config:
207
+ raise ValueError("'pumps_state_data_raw' can only be an instance of " +
208
+ "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
177
209
  if valves_state_data_raw is not None:
178
- if not isinstance(valves_state_data_raw, np.ndarray):
210
+ if not isinstance(valves_state_data_raw, np.ndarray) and \
211
+ not isinstance(valves_state_data_raw, bsr_array):
179
212
  raise TypeError("'valves_state_data_raw' must be an instance of 'numpy.ndarray' " +
180
213
  f"but no of '{type(valves_state_data_raw)}'")
214
+ if isinstance(valves_state_data_raw, bsr_array) and not frozen_sensor_config:
215
+ raise ValueError("'valves_state_data_raw' can only be an instance of " +
216
+ "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
181
217
  if tanks_volume_data_raw is not None:
182
- if not isinstance(tanks_volume_data_raw, np.ndarray):
218
+ if not isinstance(tanks_volume_data_raw, np.ndarray) and \
219
+ not isinstance(tanks_volume_data_raw, bsr_array):
183
220
  raise TypeError("'tanks_volume_data_raw' must be an instance of 'numpy.ndarray'" +
184
221
  f" but not of '{type(tanks_volume_data_raw)}'")
222
+ if isinstance(tanks_volume_data_raw, bsr_array) and not frozen_sensor_config:
223
+ raise ValueError("'tanks_volume_data_raw' can only be an instance of " +
224
+ "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
185
225
  if sensor_faults is None or not isinstance(sensor_faults, list):
186
226
  raise TypeError("'sensor_faults' must be a list of " +
187
227
  "'epyt_flow.simulation.events.SensorFault' instances but " +
188
228
  f"'{type(sensor_faults)}'")
189
229
  if surface_species_concentration_raw is not None:
190
- if not isinstance(surface_species_concentration_raw, np.ndarray):
230
+ if not isinstance(surface_species_concentration_raw, np.ndarray) and \
231
+ not isinstance(surface_species_concentration_raw, dict):
191
232
  raise TypeError("'surface_species_concentration_raw' must be an instance of " +
192
233
  "'numpy.ndarray' but not of " +
193
234
  f"'{type(surface_species_concentration_raw)}'")
235
+ if isinstance(surface_species_concentration_raw, dict) and not frozen_sensor_config:
236
+ raise TypeError("'surface_species_concentration_raw' can only be an instance of " +
237
+ "'dict' if 'frozen_sensor_config=True'")
194
238
  if bulk_species_node_concentration_raw is not None:
195
- if not isinstance(bulk_species_node_concentration_raw, np.ndarray):
239
+ if not isinstance(bulk_species_node_concentration_raw, np.ndarray) and \
240
+ not isinstance(bulk_species_node_concentration_raw, dict):
196
241
  raise TypeError("'bulk_species_node_concentration_raw' must be an instance of " +
197
242
  "'numpy.ndarray' but not of " +
198
243
  f"'{type(bulk_species_node_concentration_raw)}'")
244
+ if isinstance(bulk_species_node_concentration_raw, dict) and not frozen_sensor_config:
245
+ raise TypeError("'bulk_species_node_concentration_raw' can only be an instance of " +
246
+ "'dict' if 'frozen_sensor_config=True'")
199
247
  if bulk_species_link_concentration_raw is not None:
200
- if not isinstance(bulk_species_link_concentration_raw, np.ndarray):
248
+ if not isinstance(bulk_species_link_concentration_raw, np.ndarray) and \
249
+ not isinstance(bulk_species_link_concentration_raw, dict):
201
250
  raise TypeError("'bulk_species_link_concentration_raw' must be an instance of " +
202
251
  "'numpy.ndarray' but not of " +
203
252
  f"'{type(bulk_species_link_concentration_raw)}'")
253
+ if isinstance(bulk_species_link_concentration_raw, dict) and not frozen_sensor_config:
254
+ raise TypeError("'bulk_species_link_concentration_raw' can only be an instance of " +
255
+ "'dict' if 'frozen_sensor_config=True'")
204
256
  if pumps_energy_usage_data_raw is not None:
205
- if not isinstance(pumps_energy_usage_data_raw, np.ndarray):
257
+ if not isinstance(pumps_energy_usage_data_raw, np.ndarray) and \
258
+ not isinstance(pumps_energy_usage_data_raw, bsr_array):
206
259
  raise TypeError("'pumps_energy_usage_data_raw' must be an instance of 'numpy.ndarray' " +
207
260
  f"but not of '{type(pumps_energy_usage_data_raw)}'")
261
+ if isinstance(pumps_energy_usage_data_raw, bsr_array) and not frozen_sensor_config:
262
+ raise ValueError("'pumps_energy_usage_data_raw' can only be an instance of " +
263
+ "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
208
264
  if pumps_efficiency_data_raw is not None:
209
- if not isinstance(pumps_efficiency_data_raw, np.ndarray):
265
+ if not isinstance(pumps_efficiency_data_raw, np.ndarray) and \
266
+ not isinstance(pumps_efficiency_data_raw, bsr_array):
210
267
  raise TypeError("'pumps_efficiency_data_raw' must be an instance of 'numpy.ndarray' " +
211
268
  f"but not of '{type(pumps_efficiency_data_raw)}'")
269
+ if isinstance(pumps_efficiency_data_raw, bsr_array) and not frozen_sensor_config:
270
+ raise ValueError("'pumps_efficiency_data_raw' can only be an instance of " +
271
+ "'scipy.sparse.bsr_array' if 'frozen_sensor_config=True'")
212
272
  if len(sensor_faults) != 0:
213
273
  if any(not isinstance(f, SensorFault) for f in sensor_faults):
214
274
  raise TypeError("'sensor_faults' must be a list of " +
@@ -274,14 +334,17 @@ class ScadaData(Serializable):
274
334
  if not tanks_volume_data_raw.shape[0] == n_time_steps:
275
335
  __raise_shape_mismatch("tanks_volume_data_raw")
276
336
  if bulk_species_node_concentration_raw is not None:
277
- if bulk_species_node_concentration_raw.shape[0] != n_time_steps:
278
- __raise_shape_mismatch("bulk_species_node_concentration_raw")
337
+ if isinstance(bulk_species_node_concentration_raw, np.ndarray):
338
+ if bulk_species_node_concentration_raw.shape[0] != n_time_steps:
339
+ __raise_shape_mismatch("bulk_species_node_concentration_raw")
279
340
  if bulk_species_link_concentration_raw is not None:
280
- if bulk_species_link_concentration_raw.shape[0] != n_time_steps:
281
- __raise_shape_mismatch("bulk_species_link_concentration_raw")
341
+ if isinstance(bulk_species_link_concentration_raw, np.ndarray):
342
+ if bulk_species_link_concentration_raw.shape[0] != n_time_steps:
343
+ __raise_shape_mismatch("bulk_species_link_concentration_raw")
282
344
  if surface_species_concentration_raw is not None:
283
- if surface_species_concentration_raw.shape[0] != n_time_steps:
284
- __raise_shape_mismatch("surface_species_concentration_raw")
345
+ if isinstance(surface_species_concentration_raw, np.ndarray):
346
+ if surface_species_concentration_raw.shape[0] != n_time_steps:
347
+ __raise_shape_mismatch("surface_species_concentration_raw")
285
348
  if pumps_energy_usage_data_raw is not None:
286
349
  if pumps_energy_usage_data_raw.shape[0] != n_time_steps:
287
350
  __raise_shape_mismatch("pumps_energy_usage_data_raw")
@@ -331,35 +394,64 @@ class ScadaData(Serializable):
331
394
  else:
332
395
  return data[:, idx]
333
396
 
397
+ if isinstance(pressure_data_raw, bsr_array):
398
+ pressure_data_raw = pressure_data_raw.todense()
334
399
  self.__pressure_data_raw = __reduce_data(data=pressure_data_raw,
335
400
  item_to_idx=node_to_idx,
336
401
  sensors=sensor_config.pressure_sensors)
402
+
403
+ if isinstance(flow_data_raw, bsr_array):
404
+ flow_data_raw = flow_data_raw.todense()
337
405
  self.__flow_data_raw = __reduce_data(data=flow_data_raw,
338
406
  item_to_idx=link_to_idx,
339
407
  sensors=sensor_config.flow_sensors)
408
+
409
+ if isinstance(demand_data_raw, bsr_array):
410
+ demand_data_raw = demand_data_raw.todense()
340
411
  self.__demand_data_raw = __reduce_data(data=demand_data_raw,
341
412
  item_to_idx=node_to_idx,
342
413
  sensors=sensor_config.demand_sensors)
414
+
415
+ if isinstance(node_quality_data_raw, bsr_array):
416
+ node_quality_data_raw = node_quality_data_raw.todense()
343
417
  self.__node_quality_data_raw = __reduce_data(data=node_quality_data_raw,
344
418
  item_to_idx=node_to_idx,
345
419
  sensors=sensor_config.quality_node_sensors)
420
+
421
+ if isinstance(link_quality_data_raw, bsr_array):
422
+ link_quality_data_raw = link_quality_data_raw.todense()
346
423
  self.__link_quality_data_raw = __reduce_data(data=link_quality_data_raw,
347
424
  item_to_idx=link_to_idx,
348
425
  sensors=sensor_config.quality_link_sensors)
426
+
427
+ if isinstance(pumps_state_data_raw, bsr_array):
428
+ pumps_state_data_raw = pumps_state_data_raw.todense()
349
429
  self.__pumps_state_data_raw = __reduce_data(data=pumps_state_data_raw,
350
430
  item_to_idx=pump_to_idx,
351
431
  sensors=sensor_config.pump_state_sensors)
432
+
433
+ if isinstance(pumps_energy_usage_data_raw, bsr_array):
434
+ pumps_energy_usage_data_raw = pumps_energy_usage_data_raw.todense()
352
435
  self.__pumps_energy_usage_data_raw = \
353
436
  __reduce_data(data=pumps_energy_usage_data_raw,
354
437
  item_to_idx=pump_to_idx,
355
438
  sensors=sensor_config.pump_energyconsumption_sensors)
439
+
440
+ if isinstance(pumps_efficiency_data_raw, bsr_array):
441
+ pumps_efficiency_data_raw = pumps_efficiency_data_raw.todense()
356
442
  self.__pumps_efficiency_data_raw = \
357
443
  __reduce_data(data=pumps_efficiency_data_raw,
358
444
  item_to_idx=pump_to_idx,
359
445
  sensors=sensor_config.pump_efficiency_sensors)
446
+
447
+ if isinstance(valves_state_data_raw, bsr_array):
448
+ valves_state_data_raw = valves_state_data_raw.todense()
360
449
  self.__valves_state_data_raw = __reduce_data(data=valves_state_data_raw,
361
450
  item_to_idx=valve_to_idx,
362
451
  sensors=sensor_config.valve_state_sensors)
452
+
453
+ if isinstance(tanks_volume_data_raw, bsr_array):
454
+ tanks_volume_data_raw = tanks_volume_data_raw.todense()
363
455
  self.__tanks_volume_data_raw = __reduce_data(data=tanks_volume_data_raw,
364
456
  item_to_idx=tank_to_idx,
365
457
  sensors=sensor_config.tank_volume_sensors)
@@ -376,29 +468,56 @@ class ScadaData(Serializable):
376
468
 
377
469
  return np.concatenate(r, axis=1)
378
470
 
379
- node_bulk_species_idx = [(sensor_config.map_bulkspecies_id_to_idx(s),
380
- [sensor_config.map_node_id_to_idx(node_id)
381
- for node_id in sensor_config.bulk_species_node_sensors[s]
382
- ]) for s in sensor_config.bulk_species_node_sensors.keys()]
383
- self.__bulk_species_node_concentration_raw = \
384
- __reduce_msx_data(data=bulk_species_node_concentration_raw,
385
- sensors=node_bulk_species_idx)
386
-
387
- bulk_species_link_idx = [(sensor_config.map_bulkspecies_id_to_idx(s),
388
- [sensor_config.map_link_id_to_idx(link_id)
389
- for link_id in sensor_config.bulk_species_link_sensors[s]
390
- ]) for s in sensor_config.bulk_species_link_sensors.keys()]
391
- self.__bulk_species_link_concentration_raw = \
392
- __reduce_msx_data(data=bulk_species_link_concentration_raw,
393
- sensors=bulk_species_link_idx)
394
-
395
- surface_species_idx = [(sensor_config.map_surfacespecies_id_to_idx(s),
396
- [sensor_config.map_link_id_to_idx(link_id)
397
- for link_id in sensor_config.surface_species_sensors[s]
398
- ]) for s in sensor_config.surface_species_sensors.keys()]
399
- self.__surface_species_concentration_raw = \
400
- __reduce_msx_data(data=surface_species_concentration_raw,
401
- sensors=surface_species_idx)
471
+ def __reduce_msx_dict_data(data: dict, species_senors: dict[str, list[str]],
472
+ map_sensor_id_to_idx: Callable[str, int]) -> np.ndarray:
473
+ r = []
474
+
475
+ for species_id in data:
476
+ data_ = data[species_id].todense()
477
+ for sensor_id in species_senors[species_id]:
478
+ data_idx = map_sensor_id_to_idx(sensor_id)
479
+ r.append(data_[:, data_idx].reshape(-1, 1))
480
+
481
+ return np.concatenate(r, axis=1)
482
+
483
+ if isinstance(bulk_species_node_concentration_raw, dict):
484
+ self.__bulk_species_node_concentration_raw = __reduce_msx_dict_data(
485
+ bulk_species_node_concentration_raw, sensor_config.bulk_species_node_sensors,
486
+ sensor_config.map_node_id_to_idx)
487
+ else:
488
+ node_bulk_species_idx = [(sensor_config.map_bulkspecies_id_to_idx(s),
489
+ [sensor_config.map_node_id_to_idx(node_id)
490
+ for node_id in sensor_config.bulk_species_node_sensors[s]])
491
+ for s in sensor_config.bulk_species_node_sensors.keys()]
492
+ self.__bulk_species_node_concentration_raw = \
493
+ __reduce_msx_data(data=bulk_species_node_concentration_raw,
494
+ sensors=node_bulk_species_idx)
495
+
496
+ if isinstance(bulk_species_link_concentration_raw, dict):
497
+ self.__bulk_species_link_concentration_raw = __reduce_msx_dict_data(
498
+ bulk_species_link_concentration_raw, sensor_config.bulk_species_link_sensors,
499
+ sensor_config.map_link_id_to_idx)
500
+ else:
501
+ bulk_species_link_idx = [(sensor_config.map_bulkspecies_id_to_idx(s),
502
+ [sensor_config.map_link_id_to_idx(link_id)
503
+ for link_id in sensor_config.bulk_species_link_sensors[s]])
504
+ for s in sensor_config.bulk_species_link_sensors.keys()]
505
+ self.__bulk_species_link_concentration_raw = \
506
+ __reduce_msx_data(data=bulk_species_link_concentration_raw,
507
+ sensors=bulk_species_link_idx)
508
+
509
+ if isinstance(surface_species_concentration_raw, dict):
510
+ self.__surface_species_concentration_raw = __reduce_msx_dict_data(
511
+ surface_species_concentration_raw, sensor_config.surface_species_sensors,
512
+ sensor_config.map_link_id_to_idx)
513
+ else:
514
+ surface_species_idx = [(sensor_config.map_surfacespecies_id_to_idx(s),
515
+ [sensor_config.map_link_id_to_idx(link_id)
516
+ for link_id in sensor_config.surface_species_sensors[s]])
517
+ for s in sensor_config.surface_species_sensors.keys()]
518
+ self.__surface_species_concentration_raw = \
519
+ __reduce_msx_data(data=surface_species_concentration_raw,
520
+ sensors=surface_species_idx)
402
521
 
403
522
  self.__init()
404
523
 
@@ -755,15 +874,17 @@ class ScadaData(Serializable):
755
874
  return 5.450992969
756
875
 
757
876
  # Convert units
758
- pressure_data = self.pressure_data_raw
759
- flow_data = self.flow_data_raw
760
- demand_data = self.demand_data_raw
761
- quality_node_data = self.node_quality_data_raw
762
- quality_link_data = self.link_quality_data_raw
763
- tanks_volume_data = self.tanks_volume_data_raw
764
- surface_species_concentrations = self.surface_species_concentration_raw
765
- bulk_species_node_concentrations = self.bulk_species_node_concentration_raw
766
- bulk_species_link_concentrations = self.bulk_species_link_concentration_raw
877
+ attributes = self.get_attributes()
878
+
879
+ pressure_data = attributes["pressure_data_raw"]
880
+ flow_data = attributes["flow_data_raw"]
881
+ demand_data = attributes["demand_data_raw"]
882
+ quality_node_data = attributes["node_quality_data_raw"]
883
+ quality_link_data = attributes["link_quality_data_raw"]
884
+ tanks_volume_data = attributes["tanks_volume_data_raw"]
885
+ surface_species_concentrations = attributes["surface_species_concentration_raw"]
886
+ bulk_species_node_concentrations = attributes["bulk_species_node_concentration_raw"]
887
+ bulk_species_link_concentrations = attributes["bulk_species_link_concentration_raw"]
767
888
 
768
889
  if flow_unit is not None:
769
890
  old_flow_unit = self.__sensor_config.flow_unit
@@ -808,23 +929,23 @@ class ScadaData(Serializable):
808
929
  if bulk_species_mass_unit is not None:
809
930
  # Convert bulk species concentrations
810
931
  if self.__frozen_sensor_config is True:
811
- for i, species_id, _ in enumerate(self.__sensor_config.bulk_species_node_sensors):
932
+ for i, species_id in enumerate(self.__sensor_config.bulk_species_node_sensors):
812
933
  species_idx = self.__sensor_config.bulk_species.index(species_id)
813
934
  new_mass_unit = bulk_species_mass_unit[species_idx]
814
935
  old_mass_unit = self.__sensor_config.bulk_species_mass_unit[species_idx]
815
936
 
816
937
  if new_mass_unit != old_mass_unit:
817
938
  convert_factor = __get_mass_convert_factor(new_mass_unit, old_mass_unit)
818
- bulk_species_node_concentrations[:, i, :] *= convert_factor
939
+ bulk_species_node_concentrations[species_id] *= convert_factor
819
940
 
820
- for i, species_id, _ in enumerate(self.__sensor_config.bulk_species_link_sensors):
941
+ for i, species_id, in enumerate(self.__sensor_config.bulk_species_link_sensors):
821
942
  species_idx = self.__sensor_config.bulk_species.index(species_id)
822
943
  new_mass_unit = bulk_species_mass_unit[species_idx]
823
944
  old_mass_unit = self.__sensor_config.bulk_species_mass_unit[species_idx]
824
945
 
825
946
  if new_mass_unit != old_mass_unit:
826
947
  convert_factor = __get_mass_convert_factor(new_mass_unit, old_mass_unit)
827
- bulk_species_link_concentrations[:, i, :] *= convert_factor
948
+ bulk_species_link_concentrations[species_id] *= convert_factor
828
949
  else:
829
950
  for i in range(bulk_species_node_concentrations.shape[1]):
830
951
  if bulk_species_mass_unit[i] != self.__sensor_config.bulk_species_mass_unit[i]:
@@ -845,7 +966,7 @@ class ScadaData(Serializable):
845
966
 
846
967
  if new_mass_unit != old_mass_unit:
847
968
  convert_factor = __get_mass_convert_factor(new_mass_unit, old_mass_unit)
848
- surface_species_concentrations[:, i, :] *= convert_factor
969
+ surface_species_concentrations[species_id] *= convert_factor
849
970
  else:
850
971
  for i in range(surface_species_concentrations.shape[1]):
851
972
  old_mass_unit = self.__sensor_config.surface_species_mass_unit[i]
@@ -1272,6 +1393,131 @@ class ScadaData(Serializable):
1272
1393
  "pumps_energy_usage_data_raw": self.__pumps_energy_usage_data_raw,
1273
1394
  "pumps_efficiency_data_raw": self.__pumps_efficiency_data_raw}
1274
1395
 
1396
+ if self.__frozen_sensor_config is True:
1397
+ def __create_sparse_array(sensors: list[str], map_sensor_to_idx: Callable[str, int],
1398
+ n_all_items: int,
1399
+ get_data: Callable[list[str], np.ndarray]) -> bsr_array:
1400
+ row = []
1401
+ col = []
1402
+ data = []
1403
+
1404
+ for sensor_id in sensors:
1405
+ idx = map_sensor_to_idx(sensor_id)
1406
+ data_ = get_data([sensor_id])
1407
+
1408
+ data += data_.flatten().tolist()
1409
+ row += list(range(len(self.__sensor_readings_time)))
1410
+ col += [idx] * len(self.__sensor_readings_time)
1411
+
1412
+ return bsr_array((data, (row, col)),
1413
+ shape=(len(self.__sensor_readings_time), n_all_items))
1414
+
1415
+ def __msx_create_sparse_array(sensors: list[str], map_sensor_to_idx: Callable[str, int],
1416
+ species_id: str, n_all_items: int,
1417
+ get_data: Callable[dict[str, list[str]], np.ndarray]
1418
+ ) -> bsr_array:
1419
+ row = []
1420
+ col = []
1421
+ data = []
1422
+
1423
+ for sensor_id in sensors:
1424
+ item_idx = map_sensor_to_idx(sensor_id)
1425
+
1426
+ row += list(range(len(self.__sensor_readings_time)))
1427
+ col += [item_idx] * len(self.__sensor_readings_time)
1428
+ data += get_data({species_id: [sensor_id]}).flatten().tolist()
1429
+
1430
+ return bsr_array((data, (row, col)),
1431
+ shape=(len(self.__sensor_readings_time), n_all_items))
1432
+
1433
+ if self.__pressure_data_raw is not None:
1434
+ attr["pressure_data_raw"] = __create_sparse_array(
1435
+ self.__sensor_config.pressure_sensors,
1436
+ self.__sensor_config.map_node_id_to_idx,
1437
+ len(self.__sensor_config.nodes),
1438
+ self.get_data_pressures)
1439
+ if self.__flow_data_raw is not None:
1440
+ attr["flow_data_raw"] = __create_sparse_array(
1441
+ self.__sensor_config.flow_sensors,
1442
+ self.__sensor_config.map_link_id_to_idx,
1443
+ len(self.__sensor_config.links),
1444
+ self.get_data_flows)
1445
+ if self.__demand_data_raw is not None:
1446
+ attr["demand_data_raw"] = __create_sparse_array(
1447
+ self.__sensor_config.demand_sensors,
1448
+ self.__sensor_config.map_node_id_to_idx,
1449
+ len(self.__sensor_config.nodes),
1450
+ self.get_data_demands)
1451
+ if self.__node_quality_data_raw is not None:
1452
+ attr["node_quality_data_raw"] = __create_sparse_array(
1453
+ self.__sensor_config.quality_node_sensors,
1454
+ self.__sensor_config.map_node_id_to_idx,
1455
+ len(self.__sensor_config.nodes),
1456
+ self.get_data_nodes_quality)
1457
+ if self.__link_quality_data_raw is not None:
1458
+ attr["link_quality_data_raw"] = __create_sparse_array(
1459
+ self.__sensor_config.quality_link_sensors,
1460
+ self.__sensor_config.map_link_id_to_idx,
1461
+ len(self.__sensor_config.links),
1462
+ self.get_data_links_quality)
1463
+ if self.__pumps_state_data_raw is not None:
1464
+ attr["pumps_state_data_raw"] = __create_sparse_array(
1465
+ self.__sensor_config.pump_state_sensors,
1466
+ self.__sensor_config.map_pump_id_to_idx,
1467
+ len(self.__sensor_config.pumps),
1468
+ self.get_data_pumps_state)
1469
+ if self.__valves_state_data_raw is not None:
1470
+ attr["valves_state_data_raw"] = __create_sparse_array(
1471
+ self.__sensor_config.valve_state_sensors,
1472
+ self.__sensor_config.map_valve_id_to_idx,
1473
+ len(self.__sensor_config.valves),
1474
+ self.get_data_valves_state)
1475
+ if self.__tanks_volume_data_raw is not None:
1476
+ attr["tanks_volume_data_raw"] = __create_sparse_array(
1477
+ self.__sensor_config.tank_volume_sensors,
1478
+ self.__sensor_config.map_tank_id_to_idx,
1479
+ len(self.__sensor_config.tanks),
1480
+ self.get_data_tanks_water_volume)
1481
+ if self.__pumps_energy_usage_data_raw is not None:
1482
+ attr["pumps_energy_usage_data_raw"] = __create_sparse_array(
1483
+ self.__sensor_config.pump_energyconsumption_sensors,
1484
+ self.__sensor_config.map_pump_id_to_idx,
1485
+ len(self.__sensor_config.pumps),
1486
+ self.get_data_pumps_energyconsumption)
1487
+ if self.__pumps_efficiency_data_raw is not None:
1488
+ attr["pumps_efficiency_data_raw"] = __create_sparse_array(
1489
+ self.__sensor_config.pump_efficiency_sensors,
1490
+ self.__sensor_config.map_pump_id_to_idx,
1491
+ len(self.__sensor_config.pumps),
1492
+ self.get_data_pumps_efficiency)
1493
+ if self.__surface_species_concentration_raw is not None:
1494
+ data = {}
1495
+ for s in self.__sensor_config.surface_species_sensors.keys():
1496
+ data[s] = __msx_create_sparse_array(self.__sensor_config.surface_species_sensors[s],
1497
+ self.__sensor_config.map_link_id_to_idx,
1498
+ s, len(self.__sensor_config.links),
1499
+ self.get_data_surface_species_concentration)
1500
+
1501
+ attr["surface_species_concentration_raw"] = data
1502
+ if self.__bulk_species_node_concentration_raw is not None:
1503
+ data = {}
1504
+ for s in self.__sensor_config.bulk_species_node_sensors.keys():
1505
+ data[s] = __msx_create_sparse_array(self.__sensor_config.bulk_species_node_sensors[s],
1506
+ self.__sensor_config.map_node_id_to_idx,
1507
+ s, len(self.__sensor_config.nodes),
1508
+ self.get_data_bulk_species_node_concentration)
1509
+
1510
+ attr["bulk_species_node_concentration_raw"] = data
1511
+ if self.__bulk_species_link_concentration_raw is not None:
1512
+ data = {}
1513
+ for s in self.__sensor_config.bulk_species_link_sensors.keys():
1514
+ data[s] = __msx_create_sparse_array(self.__sensor_config.bulk_species_link_sensors[s],
1515
+ self.__sensor_config.map_link_id_to_idx,
1516
+ s, len(self.__sensor_config.links),
1517
+ self.get_data_bulk_species_link_concentration)
1518
+
1519
+ attr["bulk_species_link_concentration_raw"] = data
1520
+
1275
1521
  return super().get_attributes() | attr
1276
1522
 
1277
1523
  def __eq__(self, other) -> bool:
@@ -1412,6 +1658,120 @@ class ScadaData(Serializable):
1412
1658
  self.__sensor_reading_events = sensor_reading_events
1413
1659
  self.__init()
1414
1660
 
1661
+ def extract_time_window(self, start_time: int, end_time: int):
1662
+ """
1663
+ Extracts a time window of SCADA data from this SCADA data instance --
1664
+ i.e. creating a new SCADA data instance containing data from the requested
1665
+ time period only.
1666
+
1667
+ Parameters
1668
+ ----------
1669
+ start_time : `int`
1670
+ Start time -- i.e. beginning of the time window.
1671
+ end_time : `int`, optional
1672
+ End time -- i.e. end of the time window.
1673
+ If None, all sensor readings from the start time onward are included
1674
+
1675
+ The default is None.
1676
+
1677
+ Returns
1678
+ -------
1679
+ :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
1680
+ New SCADA data instance containing data from the requested time period only.
1681
+ """
1682
+ if not isinstance(start_time, int):
1683
+ raise TypeError("'start_time' must be an instance of `int` but not of " +
1684
+ f"'{type(start_time)}'")
1685
+ if start_time not in self.__sensor_readings_time:
1686
+ raise ValueError("No data found for 'start_time'")
1687
+ if end_time is not None:
1688
+ if not isinstance(end_time, int):
1689
+ raise TypeError("'end_time' must be an instance of `int` but not of " +
1690
+ f"'{type(end_time)}'")
1691
+ if end_time not in self.__sensor_readings_time:
1692
+ raise ValueError("No data found for 'end_time'")
1693
+ else:
1694
+ end_time = self.__sensor_readings_time[-1]
1695
+
1696
+ start_idx = self.__sensor_readings_time.tolist().index(start_time)
1697
+ end_idx = self.__sensor_readings_time.tolist().index(end_time) + 1
1698
+
1699
+ pressure_data_raw = None
1700
+ if self.__pressure_data_raw is not None:
1701
+ pressure_data_raw = self.__pressure_data_raw[start_idx:end_idx, :]
1702
+
1703
+ flow_data_raw = None
1704
+ if self.__flow_data_raw is not None:
1705
+ flow_data_raw = self.__flow_data_raw[start_idx:end_idx, :]
1706
+
1707
+ demand_data_raw = None
1708
+ if self.__demand_data_raw is not None:
1709
+ demand_data_raw = self.__demand_data_raw[start_idx:end_idx, :]
1710
+
1711
+ node_quality_data_raw = None
1712
+ if self.__node_quality_data_raw is not None:
1713
+ node_quality_data_raw = self.__node_quality_data_raw[start_idx:end_idx, :]
1714
+
1715
+ link_quality_data_raw = None
1716
+ if self.__link_quality_data_raw is not None:
1717
+ link_quality_data_raw = self.__link_quality_data_raw[start_idx:end_idx, :]
1718
+
1719
+ pumps_state_data_raw = None
1720
+ if self.__pumps_state_data_raw is not None:
1721
+ pumps_state_data_raw = self.__pumps_state_data_raw[start_idx:end_idx, :]
1722
+
1723
+ valves_state_data_raw = None
1724
+ if self.__valves_state_data_raw is not None:
1725
+ valves_state_data_raw = self.__valves_state_data_raw[start_idx:end_idx, :]
1726
+
1727
+ tanks_volume_data_raw = None
1728
+ if self.__tanks_volume_data_raw is not None:
1729
+ tanks_volume_data_raw = self.__tanks_volume_data_raw[start_idx:end_idx, :]
1730
+
1731
+ surface_species_concentration_raw = None
1732
+ if self.__surface_species_concentration_raw is not None:
1733
+ surface_species_concentration_raw = \
1734
+ self.__surface_species_concentration_raw[start_idx:end_idx, :]
1735
+
1736
+ bulk_species_node_concentration_raw = None
1737
+ if self.__bulk_species_node_concentration_raw is not None:
1738
+ bulk_species_node_concentration_raw = \
1739
+ self.__bulk_species_node_concentration_raw[start_idx:end_idx, :]
1740
+
1741
+ bulk_species_link_concentration_raw = None
1742
+ if self.__bulk_species_link_concentration_raw is not None:
1743
+ bulk_species_link_concentration_raw = \
1744
+ self.__bulk_species_link_concentration_raw[start_idx:end_idx, :]
1745
+
1746
+ pumps_energy_usage_data_raw = None
1747
+ if self.__pumps_energy_usage_data_raw is not None:
1748
+ pumps_energy_usage_data_raw = self.__pumps_energy_usage_data_raw[start_idx:end_idx, :]
1749
+
1750
+ pumps_efficiency_data_raw = None
1751
+ if self.__pumps_efficiency_data_raw is not None:
1752
+ pumps_efficiency_data_raw = self.__pumps_efficiency_data_raw[start_idx:end_idx, :]
1753
+
1754
+ return ScadaData(sensor_config=self.sensor_config,
1755
+ sensor_readings_time=self.sensor_readings_time[start_idx:end_idx],
1756
+ frozen_sensor_config=self.frozen_sensor_config,
1757
+ sensor_noise=self.sensor_noise,
1758
+ sensor_reading_events=self.sensor_reading_events,
1759
+ sensor_reading_attacks=self.sensor_reading_attacks,
1760
+ sensor_faults=self.sensor_faults,
1761
+ pressure_data_raw=pressure_data_raw,
1762
+ flow_data_raw=flow_data_raw,
1763
+ demand_data_raw=demand_data_raw,
1764
+ node_quality_data_raw=node_quality_data_raw,
1765
+ link_quality_data_raw=link_quality_data_raw,
1766
+ valves_state_data_raw=valves_state_data_raw,
1767
+ pumps_state_data_raw=pumps_state_data_raw,
1768
+ tanks_volume_data_raw=tanks_volume_data_raw,
1769
+ surface_species_concentration_raw=surface_species_concentration_raw,
1770
+ bulk_species_node_concentration_raw=bulk_species_node_concentration_raw,
1771
+ bulk_species_link_concentration_raw=bulk_species_link_concentration_raw,
1772
+ pumps_energy_usage_data_raw=pumps_energy_usage_data_raw,
1773
+ pumps_efficiency_data_raw=pumps_efficiency_data_raw)
1774
+
1415
1775
  def join(self, other) -> None:
1416
1776
  """
1417
1777
  Joins two :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` instances based
@@ -2921,3 +3281,91 @@ class ScadaData(Serializable):
2921
3281
  x_axis_label=self.__get_x_axis_label(),
2922
3282
  y_axis_label=y_axis_label,
2923
3283
  show=show, save_to_file=save_to_file, ax=ax)
3284
+
3285
+ def to_pandas_dataframe(self, export_raw_data: bool = False) -> pd.DataFrame:
3286
+ """
3287
+ Exports this SCADA data to a Pandas dataframe.
3288
+
3289
+ Parameters
3290
+ ----------
3291
+ export_raw_data : `bool`, optional
3292
+ If True, the raw measurements (i.e. sensor reading without any noise or faults)
3293
+ are exported instead of the final sensor readings.
3294
+
3295
+ The default is False.
3296
+
3297
+ Returns
3298
+ -------
3299
+ `pandas.DataFrame`
3300
+ Exported data.
3301
+ """
3302
+ from .scada_data_export import ScadaDataExport
3303
+
3304
+ old_sensor_config = None
3305
+ if export_raw_data is True:
3306
+ # Backup old sensor config and set a new one with sensors everywhere
3307
+ old_sensor_config = self.sensor_config
3308
+ self.change_sensor_config(ScadaDataExport.create_global_sensor_config(self))
3309
+
3310
+ sensor_readings = self.get_data()
3311
+ col_desc = ScadaDataExport.create_column_desc(self)
3312
+ columns = [f"{sensor_type} [{unit_desc}] at {item_id}" for sensor_type, item_id, unit_desc in col_desc]
3313
+
3314
+ data = {col_desc: sensor_readings[:, c_id] for c_id, col_desc in enumerate(columns)}
3315
+
3316
+ if export_raw_data is True:
3317
+ # Restore old sensor config
3318
+ self.change_sensor_config(old_sensor_config)
3319
+
3320
+ return pd.DataFrame(data)
3321
+
3322
+ def to_numpy_file(self, f_out: str, export_raw_data: bool = False) -> None:
3323
+ """
3324
+ Exporting this SCADA data to Numpy (.npz file).
3325
+
3326
+ Parameters
3327
+ ----------
3328
+ f_out : `str`
3329
+ Path to the .npz file to which the SCADA data will be exported.
3330
+ export_raw_data : `bool`, optional
3331
+ If True, the raw measurements (i.e. sensor reading without any noise or faults)
3332
+ are exported instead of the final sensor readings.
3333
+
3334
+ The default is False.
3335
+ """
3336
+ from .scada_data_export import ScadaDataNumpyExport
3337
+ ScadaDataNumpyExport(f_out, export_raw_data).export(self)
3338
+
3339
+ def to_excel_file(self, f_out: str, export_raw_data: bool = False) -> None:
3340
+ """
3341
+ Exporting this SCADA data to MS Excel (.xlsx file).
3342
+
3343
+ Parameters
3344
+ ----------
3345
+ f_out : `str`
3346
+ Path to the .xlsx file to which the SCADA data will be exported.
3347
+ export_raw_data : `bool`, optional
3348
+ If True, the raw measurements (i.e. sensor reading without any noise or faults)
3349
+ are exported instead of the final sensor readings.
3350
+
3351
+ The default is False.
3352
+ """
3353
+ from .scada_data_export import ScadaDataXlsxExport
3354
+ ScadaDataXlsxExport(f_out, export_raw_data).export(self)
3355
+
3356
+ def to_matlab_file(self, f_out: str, export_raw_data: bool = False) -> None:
3357
+ """
3358
+ Exporting this SCADA data to Matlab (.mat file).
3359
+
3360
+ Parameters
3361
+ ----------
3362
+ f_out : `str`
3363
+ Path to the .mat file to which the SCADA data will be exported.
3364
+ export_raw_data : `bool`, optional
3365
+ If True, the raw measurements (i.e. sensor reading without any noise or faults)
3366
+ are exported instead of the final sensor readings.
3367
+
3368
+ The default is False.
3369
+ """
3370
+ from .scada_data_export import ScadaDataMatlabExport
3371
+ ScadaDataMatlabExport(f_out, export_raw_data).export(self)