Source code for epyt_flow.simulation.scada.scada_data

   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
[docs] 1566 def extract_time_window(self, start_time: int, end_time: int = None): 1567 """ 1568 Extracts a time window of SCADA data from this SCADA data instance -- 1569 i.e. creating a new SCADA data instance containing data from the requested 1570 time period only. 1571 1572 Parameters 1573 ---------- 1574 start_time : `int` 1575 Start time -- i.e. beginning of the time window. 1576 end_time : `int`, optional 1577 End time -- i.e. end of the time window. 1578 If None, all sensor readings from the start time onward are included 1579 1580 The default is None. 1581 1582 Returns 1583 ------- 1584 :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` 1585 New SCADA data instance containing data from the requested time period only. 1586 """ 1587 if not isinstance(start_time, int): 1588 raise TypeError("'start_time' must be an instance of `int` but not of " + 1589 f"'{type(start_time)}'") 1590 if start_time not in self.__sensor_readings_time: 1591 raise ValueError("No data found for 'start_time'") 1592 if end_time is not None: 1593 if not isinstance(end_time, int): 1594 raise TypeError("'end_time' must be an instance of `int` but not of " + 1595 f"'{type(end_time)}'") 1596 if end_time not in self.__sensor_readings_time: 1597 raise ValueError("No data found for 'end_time'") 1598 else: 1599 end_time = self.__sensor_readings_time[-1] 1600 1601 start_idx = self.__sensor_readings_time_to_idx[start_time] 1602 end_idx = self.__sensor_readings_time_to_idx[end_time] + 1 1603 1604 pressure_data_raw = None 1605 if self.__pressure_data_raw is not None: 1606 pressure_data_raw = self.__pressure_data_raw[start_idx:end_idx, :] 1607 1608 flow_data_raw = None 1609 if self.__flow_data_raw is not None: 1610 flow_data_raw = self.__flow_data_raw[start_idx:end_idx, :] 1611 1612 demand_data_raw = None 1613 if self.__demand_data_raw is not None: 1614 demand_data_raw = self.__demand_data_raw[start_idx:end_idx, :] 1615 1616 node_quality_data_raw = None 1617 if self.__node_quality_data_raw is not None: 1618 node_quality_data_raw = self.__node_quality_data_raw[start_idx:end_idx, :] 1619 1620 link_quality_data_raw = None 1621 if self.__link_quality_data_raw is not None: 1622 link_quality_data_raw = self.__link_quality_data_raw[start_idx:end_idx, :] 1623 1624 pumps_state_data_raw = None 1625 if self.__pumps_state_data_raw is not None: 1626 pumps_state_data_raw = self.__pumps_state_data_raw[start_idx:end_idx, :] 1627 1628 valves_state_data_raw = None 1629 if self.__valves_state_data_raw is not None: 1630 valves_state_data_raw = self.__valves_state_data_raw[start_idx:end_idx, :] 1631 1632 tanks_volume_data_raw = None 1633 if self.__tanks_volume_data_raw is not None: 1634 tanks_volume_data_raw = self.__tanks_volume_data_raw[start_idx:end_idx, :] 1635 1636 surface_species_concentration_raw = None 1637 if self.__surface_species_concentration_raw is not None: 1638 surface_species_concentration_raw = \ 1639 self.__surface_species_concentration_raw[start_idx:end_idx, :] 1640 1641 bulk_species_node_concentration_raw = None 1642 if self.__bulk_species_node_concentration_raw is not None: 1643 bulk_species_node_concentration_raw = \ 1644 self.__bulk_species_node_concentration_raw[start_idx:end_idx, :] 1645 1646 bulk_species_link_concentration_raw = None 1647 if self.__bulk_species_link_concentration_raw is not None: 1648 bulk_species_link_concentration_raw = \ 1649 self.__bulk_species_link_concentration_raw[start_idx:end_idx, :] 1650 1651 pumps_energy_usage_data_raw = None 1652 if self.__pumps_energy_usage_data_raw is not None: 1653 pumps_energy_usage_data_raw = self.__pumps_energy_usage_data_raw[start_idx:end_idx, :] 1654 1655 pumps_efficiency_data_raw = None 1656 if self.__pumps_efficiency_data_raw is not None: 1657 pumps_efficiency_data_raw = self.__pumps_efficiency_data_raw[start_idx:end_idx, :] 1658 1659 return ScadaData(network_topo=self.network_topo, sensor_config=self.sensor_config, 1660 sensor_readings_time=self.sensor_readings_time[start_idx:end_idx], 1661 warnings_code=self.__warnings_code[start_idx:end_idx], 1662 frozen_sensor_config=self.frozen_sensor_config, 1663 sensor_noise=self.sensor_noise, 1664 sensor_reading_events=self.sensor_reading_events, 1665 sensor_reading_attacks=self.sensor_reading_attacks, 1666 sensor_faults=self.sensor_faults, 1667 pressure_data_raw=pressure_data_raw, 1668 flow_data_raw=flow_data_raw, 1669 demand_data_raw=demand_data_raw, 1670 node_quality_data_raw=node_quality_data_raw, 1671 link_quality_data_raw=link_quality_data_raw, 1672 valves_state_data_raw=valves_state_data_raw, 1673 pumps_state_data_raw=pumps_state_data_raw, 1674 tanks_volume_data_raw=tanks_volume_data_raw, 1675 surface_species_concentration_raw=surface_species_concentration_raw, 1676 bulk_species_node_concentration_raw=bulk_species_node_concentration_raw, 1677 bulk_species_link_concentration_raw=bulk_species_link_concentration_raw, 1678 pumps_energy_usage_data_raw=pumps_energy_usage_data_raw, 1679 pumps_efficiency_data_raw=pumps_efficiency_data_raw)
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 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 2706 2738 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 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 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)