Source code for epyt_flow.simulation.sensor_config

   1"""
   2Module provides a class for implementing sensor configurations.
   3"""
   4from typing import Optional
   5from copy import deepcopy
   6import itertools
   7import numpy as np
   8from epanet_plus import EpanetConstants, EPyT
   9
  10from ..serialization import SENSOR_CONFIG_ID, JsonSerializable, serializable
  11from ..utils import flowunit_to_str, pressureunit_to_str, massunit_to_str, areaunit_to_str, \
  12    qualityunit_to_str, TIME_UNIT_HRS, MASS_UNIT_MG, MASS_UNIT_UG, MASS_UNIT_MOL, \
  13    MASS_UNIT_MMOL, MASS_UNIT_CUSTOM, AREA_UNIT_FT2, AREA_UNIT_M2, AREA_UNIT_CM2
  14
  15
  16SENSOR_TYPE_NODE_PRESSURE          = 1
  17SENSOR_TYPE_NODE_QUALITY           = 2
  18SENSOR_TYPE_NODE_DEMAND            = 3
  19SENSOR_TYPE_LINK_FLOW              = 4
  20SENSOR_TYPE_LINK_QUALITY           = 5
  21SENSOR_TYPE_VALVE_STATE            = 6
  22SENSOR_TYPE_PUMP_STATE             = 7
  23SENSOR_TYPE_TANK_VOLUME            = 8
  24SENSOR_TYPE_NODE_BULK_SPECIES      = 9
  25SENSOR_TYPE_LINK_BULK_SPECIES      = 10
  26SENSOR_TYPE_SURFACE_SPECIES        = 11
  27SENSOR_TYPE_PUMP_EFFICIENCY        = 12
  28SENSOR_TYPE_PUMP_ENERGYCONSUMPTION = 13
  29
  30
[docs] 31def valid_sensor_types() -> str: 32 """Returns a description of all valid sensor types.""" 33 return ( 34 f"{SENSOR_TYPE_NODE_PRESSURE} (Node Pressure), " 35 f"{SENSOR_TYPE_NODE_QUALITY} (Node Quality), " 36 f"{SENSOR_TYPE_NODE_DEMAND} (Node Demand), " 37 f"{SENSOR_TYPE_LINK_FLOW} (Link Flow), " 38 f"{SENSOR_TYPE_LINK_QUALITY} (Link Quality), " 39 f"{SENSOR_TYPE_VALVE_STATE} (Valve State), " 40 f"{SENSOR_TYPE_PUMP_STATE} (Pump State), " 41 f"{SENSOR_TYPE_TANK_VOLUME} (Tank Volume), " 42 f"{SENSOR_TYPE_NODE_BULK_SPECIES} (Node Bulk Species), " 43 f"{SENSOR_TYPE_LINK_BULK_SPECIES} (Link Bulk Species), " 44 f"{SENSOR_TYPE_SURFACE_SPECIES} (Surface Species), " 45 f"{SENSOR_TYPE_PUMP_EFFICIENCY} (Pump Efficiency), " 46 f"{SENSOR_TYPE_PUMP_ENERGYCONSUMPTION} (Pump Energy Consumption)" 47 )
48 49
[docs] 50@serializable(SENSOR_CONFIG_ID, ".epytflow_sensor_config") 51class SensorConfig(JsonSerializable): 52 """ 53 Class for storing a sensor configuration. 54 55 Parameters 56 ---------- 57 nodes : `list[str]` 58 List of all nodes (i.e. IDs) in the network. 59 links : `list[str]` 60 List of all links/pipes (i.e. IDs) in the network. 61 valves : `list[str]` 62 List of all valves (i.e. IDs) in the network. 63 pumps : `list[str]` 64 List of all pumps (i.e. IDs) in the network. 65 tanks : `list[str]` 66 List of all tanks (i.e. IDs) in the network. 67 species : `list[str]` 68 List of all (EPANET-MSX) species (i.e. IDs) in the network 69 sensor_ordering : `list[int]`, optional 70 Ordering of sensor types in this list specifies the ordering of the sensor readings. 71 The list must contain every sensor type, no matter if a sensor of that type is placed or not! 72 73 If None, the following default will be used: 74 - 1 -> pressure sensor 75 - 2 -> node quality sensor 76 - 3 -> demand sensor 77 - 4 -> flow sensor 78 - 5 -> link quality sensor 79 - 6 -> valve state sensor 80 - 7 -> pump state sensor 81 - 8 -> tank volume sensor 82 - 9 -> node bulk species sensor 83 - 10 -> link bulk species sensor 84 - 11 -> surface species sensor 85 - 12 -> pump efficiency sensor 86 - 13 -> pump energy consumption sensor 87 88 The default is None. 89 pressure_sensors : `list[str]`, optional 90 List of all nodes (i.e. IDs) at which a pressure sensor is placed. 91 92 The default is an empty list. 93 flow_sensors : `list[str]`, optional 94 List of all links/pipes (i.e. IDs) at which a flow sensor is placed. 95 96 The default is an empty list. 97 demand_sensors : `list[str]`, optional 98 List of all nodes (i.e. IDs) at which a demand sensor is placed. 99 100 The default is an empty list. 101 quality_node_sensors : `list[str]`, optional 102 List of all nodes (i.e. IDs) at which a quality sensor is placed. 103 104 The default is an empty list. 105 quality_link_sensors : `list[str]`, optional 106 List of all links/pipes (i.e. IDs) at which a flow sensor is placed. 107 108 The default is an empty list. 109 valve_state_sensors : `list[str]`, optional 110 List of all valves (i.e. IDs) at which a valve state sensor is placed. 111 112 The default is an empty list. 113 pump_state_sensors : `list[str]`, optional 114 List of all pumps (i.e. IDs) at which a pump state sensor is placed. 115 116 The default is an empty list. 117 tank_volume_sensors : `list[str]`, optional 118 List of all tanks (i.e. IDs) at which a tank volume sensor is placed. 119 120 The default is an empty list. 121 bulk_species_node_sensors : `dict`, optional 122 Bulk species node sensors as a dictionary -- i.e. bulk species ID are the keys, 123 and the sensor locations (node IDs) are the values. 124 125 The default is an empty list. 126 bulk_species_link_sensors : `dict`, optional 127 Bulk species link/pipe sensors as a dictionary -- i.e. bulk species ID are the keys, 128 and the sensor locations (link/pipe IDs) are the values. 129 130 The default is an empty list. 131 surface_species_sensors : `dict`, optional 132 Surface species sensors as a dictionary -- i.e. surface species ID are the keys, 133 and the sensor locations (link/pipe IDs) are the values. 134 135 The default is an empty list. 136 node_id_to_idx : `dict`, optional 137 Mapping of a node ID to the EPANET index (i.e. position in the raw sensor reading data). 138 139 If None is given, it is assumed that the nodes (in 'nodes') are 140 sorted according to their EPANET index. 141 142 The default is None. 143 link_id_to_idx : `dict`, optional 144 Mapping of a link/pipe ID to the EPANET index 145 (i.e. position in the raw sensor reading data). 146 147 If None is given, it is assumed that the links/pipes (in 'links') are 148 sorted according to their EPANET index.. 149 150 The default is None. 151 valve_id_to_idx : `dict`, optional 152 Mapping of a valve ID to the EPANET index (i.e. position in the raw sensor reading data). 153 154 If None is given, it is assumed that the valves (in 'valves') are 155 sorted according to their EPANET index. 156 157 The default is None. 158 pump_id_to_idx : `dict`, optional 159 Mapping of a pump ID to the EPANET index (i.e. position in the raw sensor reading data). 160 161 If None is given, it is assumed that the pumps (in 'pumps') are 162 sorted according to their EPANET index. 163 164 The default is None. 165 tank_id_to_idx : `dict`, optional 166 Mapping of a tank ID to the EPANET index (i.e. position in the raw sensor reading data). 167 168 If None is given, it is assumed that the tanks (in 'tanks') are 169 sorted according to their EPANET index. 170 171 The default is None. 172 bulkspecies_id_to_idx : `dict`, optional 173 Mapping of a surface species ID to the EPANET index 174 (i.e. position in the raw sensor reading data). 175 176 If None is given, it is assumed that the surface species (in 'surface_species') are 177 sorted according to their EPANET index. 178 179 The default is None. 180 flow_unit : `int` 181 Specifies the flow units and consequently many other hydraulic units 182 (US CUSTOMARY or SI METRIC) as well, except the pressure units which must be 183 specified separately. 184 185 Must be one of the following EPANET constants: 186 187 - EN_CFS = 0 (cubic foot/sec) 188 - EN_GPM = 1 (gal/min) 189 - EN_MGD = 2 (Million gal/day) 190 - EN_IMGD = 3 (Imperial MGD) 191 - EN_AFD = 4 (ac-foot/day) 192 - EN_LPS = 5 (liter/sec) 193 - EN_LPM = 6 (liter/min) 194 - EN_MLD = 7 (Megaliter/day) 195 - EN_CMH = 8 (cubic meter/hr) 196 - EN_CMD = 9 (cubic meter/day) 197 - EN_CMS = 10 (cubic meter/sec) 198 pressure_unit : `int` 199 Specifies the pressure units. 200 201 Must be one of the following EPANET constants: 202 203 - EN_PSI = 0 (Pounds per square inch) 204 - EN_KPA = 1 (Kilopascals) 205 - EN_METERS = 2 (Meters) 206 - EN_BAR = 3 (Bar) 207 - EN_FEET = 4 (Feet) 208 quality_unit : `str`, optional 209 Measurement unit (in a basic quality analysis) -- only relevant 210 if basic water quality is enabled. 211 212 Must be one of the following constants: 213 214 - MASS_UNIT_MG = 4 (mg/L) 215 - MASS_UNIT_UG = 5 (ug/L) 216 - TIME_UNIT_HRS = 8 (hrs) 217 218 bulk_species_mass_unit : `list[int]`, optional 219 Specifies the mass unit for each bulk species -- only relevant if EPANET-MSX is used. 220 221 Must be one of the following constants: 222 223 - MASS_UNIT_MG = 4 (milligram) 224 - MASS_UNIT_UG = 5 (microgram) 225 - MASS_UNIT_MOL = 6 (mole) 226 - MASS_UNIT_MMOL = 7 (millimole) 227 228 Note that the assumed ordering is the same as given in 'bulk_species'. 229 surface_species_mass_unit : `list[int]`, optional 230 Specifies the mass unit for each surface species -- only relevant if EPANET-MSX is used. 231 232 Must be one of the following constants: 233 234 - MASS_UNIT_MG = 4 (milligram) 235 - MASS_UNIT_UG = 5 (microgram) 236 - MASS_UNIT_MOL = 6 (mole) 237 - MASS_UNIT_MMOL = 7 (millimole) 238 239 Note that the assumed ordering is the same as given in 'surface_species'. 240 surface_species_area_unit : `int`, optional 241 Species the area unit of all surface species -- only relevant if EPANET-MSX is used. 242 Must be one of the following constants: 243 244 - AREA_UNIT_FT2 = 1 (square feet) 245 - AREA_UNIT_M2 = 2 (square meters) 246 - AREA_UNIT_CM2 = 3 (square centimeters) 247 """ 248 def __init__(self, nodes: list[str], links: list[str], valves: list[str], pumps: list[str], 249 tanks: list[str], bulk_species: list[str], surface_species: list[str], 250 flow_unit: int, pressure_unit: int, 251 sensor_ordering: list[int] = None, 252 pressure_sensors: list[str] = [], 253 flow_sensors: list[str] = [], 254 demand_sensors: list[str] = [], 255 quality_node_sensors: list[str] = [], 256 quality_link_sensors: list[str] = [], 257 valve_state_sensors: list[str] = [], 258 pump_state_sensors: list[str] = [], 259 pump_efficiency_sensors: list[str] = [], 260 pump_energyconsumption_sensors: list[str] = [], 261 tank_volume_sensors: list[str] = [], 262 bulk_species_node_sensors: dict = {}, 263 bulk_species_link_sensors: dict = {}, 264 surface_species_sensors: dict = {}, 265 node_id_to_idx: dict = None, link_id_to_idx: dict = None, 266 valve_id_to_idx: dict = None, pump_id_to_idx: dict = None, 267 tank_id_to_idx: dict = None, bulkspecies_id_to_idx: dict = None, 268 surfacespecies_id_to_idx: dict = None, 269 quality_unit: int = None, 270 bulk_species_mass_unit : list[int] = [], 271 surface_species_mass_unit : list[int] = [], 272 surface_species_area_unit : int = None, 273 **kwds): 274 if not isinstance(nodes, list): 275 raise TypeError("'nodes' must be an instance of 'list[str]' " + 276 f"but not of '{type(nodes)}'") 277 if len(nodes) == 0: 278 raise ValueError("'nodes' must be a list of all nodes (i.e. IDs) in the network.") 279 if any(not isinstance(n, str) for n in nodes): 280 raise TypeError("Each item in 'nodes' must be an instance of 'str' -- " + 281 "ID of a node in the network.") 282 283 if not isinstance(links, list): 284 raise TypeError("'links' must be an instance of 'list[str]' " + 285 f"but not of '{type(links)}'") 286 if len(links) == 0: 287 raise ValueError("'links' must be a list of all links/pipes (i.e. IDs) in the network.") 288 if any(not isinstance(link, str) for link in links): 289 raise TypeError("Each item in 'links' must be an instance of 'str' -- " + 290 "ID of a link/pipe in the network.") 291 292 if not isinstance(valves, list): 293 raise TypeError("'valves' must be an instance of 'list[str]' " + 294 f"but not of '{type(valves)}'") 295 if any(v not in links for v in valves): 296 raise ValueError("Each item in 'valves' must be in 'links'") 297 298 if not isinstance(pumps, list): 299 raise TypeError("'pumps' must be an instance of 'list[str]' " + 300 f"but not of '{type(pumps)}'") 301 if any(p not in links for p in pumps): 302 raise ValueError("Each item in 'pumps' must be in 'links'") 303 304 if not isinstance(tanks, list): 305 raise TypeError("'tanks' must be an instance of 'list[str]' " + 306 f"but not of '{type(tanks)}'") 307 if any(v not in nodes for v in tanks): 308 raise ValueError("Each item in 'tanks' must be in 'nodes'") 309 310 if not isinstance(bulk_species, list): 311 raise TypeError("'bulk_species' must be an instance of 'list[str]' " + 312 f"but not of '{type(bulk_species)}'") 313 if any(not isinstance(bulk_species_id, str) for bulk_species_id in bulk_species): 314 raise TypeError("Each item in 'bulk_species' must be an instance of 'str'") 315 316 if not isinstance(surface_species, list): 317 raise TypeError("'surface_species' must be an instance of 'list[str]' " + 318 f"but not of '{type(surface_species)}'") 319 if any(not isinstance(surface_species_id, str) for surface_species_id in surface_species): 320 raise TypeError("Each item in 'surface_species' must be an instance of 'str'") 321 322 default_sensor_ordering = [ 323 SENSOR_TYPE_NODE_PRESSURE, 324 SENSOR_TYPE_LINK_FLOW, 325 SENSOR_TYPE_NODE_DEMAND, 326 SENSOR_TYPE_NODE_QUALITY, 327 SENSOR_TYPE_LINK_QUALITY, 328 SENSOR_TYPE_VALVE_STATE, 329 SENSOR_TYPE_PUMP_STATE, 330 SENSOR_TYPE_PUMP_EFFICIENCY, 331 SENSOR_TYPE_PUMP_ENERGYCONSUMPTION, 332 SENSOR_TYPE_TANK_VOLUME, 333 SENSOR_TYPE_SURFACE_SPECIES, 334 SENSOR_TYPE_NODE_BULK_SPECIES, 335 SENSOR_TYPE_LINK_BULK_SPECIES, 336 ] 337 if sensor_ordering is not None: 338 if not isinstance(sensor_ordering, list): 339 raise TypeError("'sensor_ordering' must be an instance of 'list[int]' " + 340 f"but not of '{type(sensor_ordering)}'") 341 if any(s_id not in sensor_ordering for s_id in default_sensor_ordering) or \ 342 len(sensor_ordering) != len(default_sensor_ordering): 343 raise ValueError("Invalid 'sensor_ordering'") 344 345 if not isinstance(pressure_sensors, list): 346 raise TypeError("'pressure_sensors' must be an instance of 'list[str]' " + 347 f"but not of '{type(pressure_sensors)}'") 348 if any(n not in nodes for n in pressure_sensors): 349 raise ValueError("Each item in 'pressure_sensors' must be in 'nodes' -- " + 350 "cannot place a sensor at a non-existing node.") 351 352 if not isinstance(flow_sensors, list): 353 raise TypeError("'flow_sensors' must be an instance of 'list[str]' " + 354 f"but not of '{type(flow_sensors)}'") 355 if any(link not in links for link in flow_sensors): 356 raise ValueError("Each item in 'flow_sensors' must be in 'links' -- cannot " + 357 "place a sensor at a non-existing link/pipe.") 358 359 if not isinstance(demand_sensors, list): 360 raise TypeError("'demand_sensors' must be an instance of 'list[str]' " + 361 f"but not of '{type(demand_sensors)}'") 362 if any(n not in nodes for n in demand_sensors): 363 raise ValueError("Each item in 'demand_sensors' must be in 'nodes' -- cannot " + 364 "place a sensor at a non-existing node.") 365 366 if not isinstance(quality_node_sensors, list): 367 raise TypeError("'quality_node_sensors' must be an instance of 'list[str]' " + 368 f"but not of '{type(quality_node_sensors)}'") 369 if any(n not in nodes for n in quality_node_sensors): 370 raise ValueError("Each item in 'quality_node_sensors' must be in 'nodes' -- cannot " + 371 "place a sensor at a non-existing node.") 372 373 if not isinstance(quality_link_sensors, list): 374 raise TypeError("'quality_link_sensors' must be an instance of 'list[str]' " + 375 f"but not of '{type(quality_link_sensors)}'") 376 if any(link not in links for link in quality_link_sensors): 377 raise ValueError("Each item in 'quality_link_sensors' must be in 'links' -- cannot " + 378 "place a sensor at a non-existing link/pipe.") 379 380 if not isinstance(valve_state_sensors, list): 381 raise TypeError("'valve_state_sensors' must be an instance of 'list[str]' " + 382 f"but not of '{type(valve_state_sensors)}'") 383 if any(link not in valves for link in valve_state_sensors): 384 raise ValueError("Each item in 'valve_state_sensors' must be in 'valves' -- cannot " + 385 "place a sensor at a non-existing valve.") 386 387 if not isinstance(pump_state_sensors, list): 388 raise TypeError("'pump_state_sensors' must be an instance of 'list[str]' " + 389 f"but not of '{type(pump_state_sensors)}'") 390 if any(link not in pumps for link in pump_state_sensors): 391 raise ValueError("Each item in 'pump_state_sensors' must be in 'pumps' -- cannot " + 392 "place a sensor at a non-existing pump.") 393 394 if not isinstance(pump_efficiency_sensors, list): 395 raise TypeError("'pump_efficiency_sensors' must be an instance of 'list[str]' " + 396 f"but not of '{type(pump_efficiency_sensors)}'") 397 if any(link not in pumps for link in pump_efficiency_sensors): 398 raise ValueError("Each item in 'pump_efficiency_sensors' must be in 'pumps' -- cannot " + 399 "place a sensor at a non-existing pump.") 400 401 if not isinstance(pump_energyconsumption_sensors, list): 402 raise TypeError("'pump_energyconsumption_sensors' must be an instance of 'list[str]' " + 403 f"but not of '{type(pump_energyconsumption_sensors)}'") 404 if any(link not in pumps for link in pump_energyconsumption_sensors): 405 raise ValueError("Each item in 'pump_energyconsumption_sensors' must be in 'pumps' -- cannot " + 406 "place a sensor at a non-existing pump.") 407 408 if not isinstance(tank_volume_sensors, list): 409 raise TypeError("'tank_volume_sensors' must be an instance of 'list[str]' " + 410 f"but not of '{type(tank_volume_sensors)}'") 411 if any(n not in tanks for n in tank_volume_sensors): 412 raise ValueError("Each item in 'tank_volume_sensors' must be in 'tanks' -- cannot " + 413 "place a sensor at a non-existing tanks.") 414 415 if not isinstance(bulk_species_node_sensors, dict): 416 raise TypeError("'bulk_species_node_sensors' must be an instance of 'dict' but not " + 417 f"of '{type(bulk_species_node_sensors)}'") 418 if any(bulk_species_id not in bulk_species 419 for bulk_species_id in bulk_species_node_sensors.keys()): 420 raise ValueError("Unknown bulk species ID in 'bulk_species_node_sensors'") 421 if any(node_id not in nodes for node_id in list(itertools.chain( 422 *bulk_species_node_sensors.values()))): 423 raise ValueError("Unknown node ID in 'bulk_species_node_sensors'") 424 425 if not isinstance(bulk_species_link_sensors, dict): 426 raise TypeError("'bulk_species_link_sensors' must be an instance of 'dict' but not " + 427 f"of '{type(bulk_species_link_sensors)}'") 428 if any(bulk_species_id not in bulk_species 429 for bulk_species_id in bulk_species_link_sensors.keys()): 430 raise ValueError("Unknown bulk species ID in 'bulk_species_link_sensors'") 431 if any(link_id not in links for link_id in list(itertools.chain( 432 *bulk_species_link_sensors.values()))): 433 raise ValueError("Unknown link/pipe ID in 'bulk_species_link_sensors'") 434 435 if not isinstance(surface_species_sensors, dict): 436 raise TypeError("'surface_species_sensors' must be an instance of 'dict' but not " + 437 f"of '{type(surface_species_sensors)}'") 438 if any(surface_species_id not in surface_species_sensors 439 for surface_species_id in surface_species_sensors.keys()): 440 raise ValueError("Unknown surface species ID in 'surface_species_sensors'") 441 if any(link_id not in links for link_id in list(itertools.chain( 442 *surface_species_sensors.values()))): 443 raise ValueError("Unknown link ID in 'surface_species_sensors'") 444 445 if node_id_to_idx is not None: 446 if not isinstance(node_id_to_idx, dict): 447 raise TypeError("'node_id_to_idx' must be an instance of 'dict' " + 448 f"but not of '{type(node_id_to_idx)}'") 449 if any(n not in nodes for n in node_id_to_idx.keys()): 450 raise ValueError("Unknown node ID in 'node_id_to_idx'") 451 452 if link_id_to_idx is not None: 453 if not isinstance(link_id_to_idx, dict): 454 raise TypeError("'link_id_to_idx' must be an instance of 'dict' " + 455 f"but not of '{type(link_id_to_idx)}'") 456 if any(link_id not in links for link_id in link_id_to_idx.keys()): 457 raise ValueError("Unknown link/pipe ID in 'link_id_to_idx'") 458 459 if valve_id_to_idx is not None: 460 if not isinstance(valve_id_to_idx, dict): 461 raise TypeError("'valve_id_to_idx' must be an instance of 'dict' " + 462 f"but not of '{type(valve_id_to_idx)}'") 463 if any(v not in valves for v in valve_id_to_idx.keys()): 464 raise ValueError("Unknown valve ID in 'valve_id_to_idx'") 465 466 if pump_id_to_idx is not None: 467 if not isinstance(pump_id_to_idx, dict): 468 raise TypeError("'pump_id_to_idx' must be an instance of 'dict' " + 469 f"but not of '{type(pump_id_to_idx)}'") 470 if any(p not in valves for p in pump_id_to_idx.keys()): 471 raise ValueError("Unknown pump ID in 'pump_id_to_idx'") 472 473 if tank_id_to_idx is not None: 474 if not isinstance(tank_id_to_idx, dict): 475 raise TypeError("'tank_id_to_idx' must be an instance of 'dict' " + 476 f"but not of '{type(tank_id_to_idx)}'") 477 if any(t not in tanks for t in tank_id_to_idx.keys()): 478 raise ValueError("Unknown tank ID in 'tank_id_to_idx'") 479 480 if bulkspecies_id_to_idx is not None: 481 if not isinstance(bulkspecies_id_to_idx, dict): 482 raise TypeError("'bulkspecies_id_to_idx' must be an instance of 'dict' " + 483 f"but not of '{type(bulkspecies_id_to_idx)}'") 484 if any(s not in bulk_species for s in bulkspecies_id_to_idx.keys()): 485 raise ValueError("Unknown bulk species ID in 'bulkspecies_id_to_idx'") 486 487 if surfacespecies_id_to_idx is not None: 488 if not isinstance(surfacespecies_id_to_idx, dict): 489 raise TypeError("'surfacespecies_id_to_idx' must be an instance of 'dict' " + 490 f"but not of '{type(surfacespecies_id_to_idx)}'") 491 if any(s not in surface_species for s in surfacespecies_id_to_idx.keys()): 492 raise ValueError("Unknown surface species ID in 'surfacespecies_id_to_idx'") 493 494 if not isinstance(flow_unit, int): 495 raise TypeError("'flow_unit' must be a an instance of 'int' " + 496 f"but not of '{type(flow_unit)}'") 497 if flow_unit not in range(11): 498 raise ValueError("Invalid value of 'flow_unit'") 499 500 if not isinstance(pressure_unit, int): 501 raise TypeError("'pressure_unit' must be a an instance of 'int' " + 502 f"but not of '{type(pressure_unit)}'") 503 if pressure_unit not in range(5): 504 raise ValueError("Invalid value of 'pressure_unit'") 505 506 if quality_unit is not None: 507 if not isinstance(quality_unit, int): 508 raise TypeError("'quality_mass_unit' must be an instance of 'int' " + 509 f"but not of '{type(quality_unit)}'") 510 if quality_unit not in [MASS_UNIT_MG, MASS_UNIT_UG, TIME_UNIT_HRS]: 511 raise ValueError("Invalid value of 'quality_unit'") 512 513 if len(bulk_species_mass_unit) != len(bulk_species): 514 raise ValueError("Inconsistency between 'bulk_species_mass_unit' and 'bulk_species'") 515 if any(not isinstance(mass_unit, int) for mass_unit in bulk_species_mass_unit): 516 raise TypeError("All items in 'bulk_species_mass_unit' must be an instance of 'int'") 517 if any(mass_unit not in [MASS_UNIT_MG, MASS_UNIT_UG, MASS_UNIT_MOL, MASS_UNIT_MMOL, 518 MASS_UNIT_CUSTOM] 519 for mass_unit in bulk_species_mass_unit): 520 raise ValueError("Invalid mass unit in 'bulk_species_mass_unit'") 521 522 if len(surface_species_mass_unit) != len(surface_species): 523 raise ValueError("Inconsistency between 'surface_species_mass_unit' " + 524 "and 'surface_species'") 525 if any(not isinstance(mass_unit, int) for mass_unit in surface_species_mass_unit): 526 raise TypeError("All items in 'surface_species_mass_unit' must be an instance of 'int'") 527 if any(mass_unit not in [MASS_UNIT_MG, MASS_UNIT_UG, MASS_UNIT_MOL, MASS_UNIT_MMOL, 528 MASS_UNIT_CUSTOM] 529 for mass_unit in surface_species_mass_unit): 530 raise ValueError("Invalid mass unit in 'surface_species_mass_unit'") 531 532 if surface_species_area_unit is not None: 533 if not isinstance(surface_species_area_unit, int): 534 raise TypeError("'surface_species_area_unit' must be a an instance of 'int' " + 535 f"but not of '{type(surface_species_area_unit)}'") 536 if surface_species_area_unit not in [AREA_UNIT_FT2, AREA_UNIT_M2, AREA_UNIT_CM2]: 537 raise ValueError("Invalid area unit 'surface_species_area_unit'") 538 539 self.__nodes = nodes 540 self.__links = links 541 self.__valves = valves 542 self.__pumps = pumps 543 self.__tanks = tanks 544 self.__bulk_species = bulk_species 545 self.__surface_species = surface_species 546 self.__pressure_sensors = pressure_sensors 547 self.__flow_sensors = flow_sensors 548 self.__demand_sensors = demand_sensors 549 self.__quality_node_sensors = quality_node_sensors 550 self.__quality_link_sensors = quality_link_sensors 551 self.__valve_state_sensors = valve_state_sensors 552 self.__pump_state_sensors = pump_state_sensors 553 self.__pump_energyconsumption_sensors = pump_energyconsumption_sensors 554 self.__pump_efficiency_sensors = pump_efficiency_sensors 555 self.__tank_volume_sensors = tank_volume_sensors 556 self.__bulk_species_node_sensors = bulk_species_node_sensors 557 self.__bulk_species_link_sensors = bulk_species_link_sensors 558 self.__surface_species_sensors = surface_species_sensors 559 self.__node_id_to_idx = node_id_to_idx 560 self.__link_id_to_idx = link_id_to_idx 561 self.__valve_id_to_idx = valve_id_to_idx 562 self.__pump_id_to_idx = pump_id_to_idx 563 self.__tank_id_to_idx = tank_id_to_idx 564 self.__bulkspecies_id_to_idx = bulkspecies_id_to_idx 565 self.__surfacespecies_id_to_idx = surfacespecies_id_to_idx 566 self.__flow_unit = flow_unit 567 self.__pressure_unit = pressure_unit 568 self.__quality_unit = quality_unit 569 self.__bulk_species_mass_unit = bulk_species_mass_unit 570 self.__surface_species_mass_unit = surface_species_mass_unit 571 self.__surface_species_area_unit = surface_species_area_unit 572 self.__sensor_ordering = default_sensor_ordering if sensor_ordering is None \ 573 else sensor_ordering 574 575 self.__compute_indices() # Compute indices 576 577 super().__init__(**kwds) 578
[docs] 579 @staticmethod 580 def create_empty_sensor_config(sensor_config): 581 """ 582 Creates an empty sensor configuration from a given sensor configuration 583 -- i.e. a clone of the given sensor configuration except that no sensors are set. 584 585 Parameters 586 ---------- 587 sensor_config : :class:`~epyt_flow.simulation.sensor_config.SensorConfig` 588 Sensor configuration used as a basis. 589 590 Returns 591 ------- 592 :class:`epyt_flow.simulation.sensor_config.SensorConfig` 593 Empty sensor configuration. 594 """ 595 return SensorConfig(nodes=sensor_config.nodes, 596 links=sensor_config.links, 597 valves=sensor_config.valves, 598 pumps=sensor_config.pumps, 599 tanks=sensor_config.tanks, 600 flow_unit=sensor_config.flow_unit, 601 pressure_unit=sensor_config.pressure_unit, 602 quality_unit=sensor_config.quality_unit, 603 bulk_species=sensor_config.bulk_species, 604 surface_species=sensor_config.surface_species, 605 bulk_species_mass_unit=sensor_config.bulk_species_mass_unit, 606 surface_species_mass_unit=sensor_config.surface_species_mass_unit, 607 surface_species_area_unit=sensor_config.surface_species_area_unit, 608 node_id_to_idx=sensor_config.node_id_to_idx, 609 link_id_to_idx=sensor_config.link_id_to_idx, 610 valve_id_to_idx=sensor_config.valve_id_to_idx, 611 pump_id_to_idx=sensor_config.pump_id_to_idx, 612 tank_id_to_idx=sensor_config.tank_id_to_idx, 613 bulkspecies_id_to_idx=sensor_config.bulkspecies_id_to_idx, 614 surfacespecies_id_to_idx=sensor_config.surfacespecies_id_to_idx)
615
[docs] 616 def is_empty(self) -> bool: 617 """ 618 Checks if the sensor configuration is empty -- i.e. no sensors are placed. 619 620 Returns 621 ------- 622 `bool` 623 True if no sensors are placed, False otherwise. 624 """ 625 if self.__pressure_sensors == [] and self.__flow_sensors == [] \ 626 and self.__demand_sensors == [] and self.__quality_node_sensors == [] \ 627 and self.__quality_link_sensors == [] and self.__valve_state_sensors == [] \ 628 and self.__pump_state_sensors == [] \ 629 and self.__pump_energyconsumption_sensors == [] \ 630 and self.__pump_efficiency_sensors == [] and self.__tank_volume_sensors == [] \ 631 and self.__bulk_species_node_sensors == [] \ 632 and self.__bulk_species_link_sensors == [] \ 633 and self.__surface_species_sensors == []: 634 return True 635 else: 636 return False
637
[docs] 638 def place_sensors_everywhere(self) -> None: 639 """ 640 Places sensors everywhere -- i.e. every possible quantity is monitored 641 at every position in the network. 642 """ 643 self.__pressure_sensors = self.__nodes[:] 644 self.__demand_sensors = self.__nodes[:] 645 self.__flow_sensors = self.__links[:] 646 self.__quality_node_sensors = self.__nodes[:] 647 self.__quality_link_sensors = self.__links[:] 648 self.__pump_state_sensors = self.__pumps[:] 649 self.__pump_energyconsumption_sensors = self.__pumps[:] 650 self.__pump_efficiency_sensors = self.__pumps[:] 651 self.__tank_volume_sensors = self.__tanks[:] 652 self.__bulk_species_node_sensors = {species_id: self.__nodes[:] 653 for species_id in self.__bulk_species} 654 self.__bulk_species_link_sensors = {species_id: self.__links[:] 655 for species_id in self.__bulk_species} 656 self.__surface_species_sensors = {species_id: self.__links[:] 657 for species_id in self.__surface_species} 658 659 self.__compute_indices()
660 661 @property 662 def node_id_to_idx(self) -> dict: 663 """ 664 Mapping of a surface node ID to the EPANET index 665 (i.e. position in the raw sensor reading data). 666 667 If None, it is assumed that the nodes (in 'nodes') are 668 sorted according to their EPANET index. 669 670 Returns 671 ------- 672 `dict` 673 Node ID to index mapping. 674 """ 675 return self.__node_id_to_idx 676 677 @property 678 def link_id_to_idx(self) -> dict: 679 """ 680 Mapping of a link/pipe ID to the EPANET index 681 (i.e. position in the raw sensor reading data). 682 683 If None is given, it is assumed that the links/pipes (in 'links') are 684 sorted according to their EPANET index. 685 686 Returns 687 ------- 688 `dict` 689 Link/Pipe ID to index mapping. 690 """ 691 return self.__link_id_to_idx 692 693 @property 694 def valve_id_to_idx(self) -> dict: 695 """ 696 Mapping of a valve ID to the EPANET index 697 (i.e. position in the raw sensor reading data). 698 699 If None, it is assumed that the valves (in 'valves') are 700 sorted according to their EPANET index. 701 702 Returns 703 ------- 704 `dict` 705 Valve ID to index mapping. 706 """ 707 return self.__valve_id_to_idx 708 709 @property 710 def pump_id_to_idx(self) -> dict: 711 """ 712 Mapping of a pump ID to the EPANET index 713 (i.e. position in the raw sensor reading data). 714 715 If None, it is assumed that the pumps (in 'pumps') are 716 sorted according to their EPANET index. 717 718 Returns 719 ------- 720 `dict` 721 Pump ID to index mapping. 722 """ 723 return self.__pump_id_to_idx 724 725 @property 726 def tank_id_to_idx(self) -> dict: 727 """ 728 Mapping of a tank ID to the EPANET index 729 (i.e. position in the raw sensor reading data). 730 731 If None, it is assumed that the tanks (in 'tanks') are 732 sorted according to their EPANET index. 733 734 Returns 735 ------- 736 `dict` 737 Tank ID to index mapping. 738 """ 739 return self.__tank_id_to_idx 740 741 @property 742 def bulkspecies_id_to_idx(self) -> dict: 743 """ 744 Mapping of a bulk species ID to the EPANET index 745 (i.e. position in the raw sensor reading data). 746 747 If None, it is assumed that the bulk species (in 'bulk_species') are 748 sorted according to their EPANET index. 749 750 Returns 751 ------- 752 `dict` 753 Bulk species ID to index mapping. 754 """ 755 return self.__bulkspecies_id_to_idx 756 757 @property 758 def surfacespecies_id_to_idx(self) -> dict: 759 """ 760 Mapping of a surface species ID to the EPANET index 761 (i.e. position in the raw sensor reading data). 762 763 If None, it is assumed that the surface species (in 'surface_species') are 764 sorted according to their EPANET index. 765 766 Returns 767 ------- 768 `dict` 769 Surface species ID to index mapping. 770 """ 771 return self.__surfacespecies_id_to_idx 772 773 @property 774 def sensor_ordering(self) -> list[int]: 775 """ 776 Returns the order in which sensors are included in ScadaData objects 777 i.e. if you call a ScadaData's get_data() method, the resulting array 778 will contain sensor readings in the order returned by this method. 779 780 Returns 781 ------- 782 `list[int]` 783 List of sensor types, specifying the ordering of sensor readings. 784 785 Constants have the following meaning: 786 - 1 -> pressure sensor 787 - 2 -> node quality sensor 788 - 3 -> demand sensor 789 - 4 -> flow sensor 790 - 5 -> link quality sensor 791 - 6 -> valve state sensor 792 - 7 -> pump state sensor 793 - 8 -> tank volume sensor 794 - 9 -> node bulk species sensor 795 - 10 -> link bulk species sensor 796 - 11 -> surface species sensor 797 - 12 -> pump efficiency sensor 798 - 13 -> pump energy consumption sensor 799 """ 800 return self.__sensor_ordering.copy() 801 802 @sensor_ordering.setter 803 def sensor_ordering(self, new_sensor_ordering: list[int]) -> None: 804 """ 805 Specifies a new ordering of the sensor readings. 806 807 Parameters 808 ---------- 809 new_sensor_ordering : `list[int]` 810 Ordering of sensor types in this list specifies the ordering of the sensor readings. 811 The list must contain every sensor type, no matter if a sensor of that type is placed or not! 812 813 List of all existing sensor types: 814 - 1 -> pressure sensor 815 - 2 -> node quality sensor 816 - 3 -> demand sensor 817 - 4 -> flow sensor 818 - 5 -> link quality sensor 819 - 6 -> valve state sensor 820 - 7 -> pump state sensor 821 - 8 -> tank volume sensor 822 - 9 -> node bulk species sensor 823 - 10 -> link bulk species sensor 824 - 11 -> surface species sensor 825 - 12 -> pump efficiency sensor 826 - 13 -> pump energy consumption sensor 827 """ 828 if len(new_sensor_ordering) != len(self.__sensor_ordering) or \ 829 any(s_id not in new_sensor_ordering for s_id in self.__sensor_ordering): 830 raise ValueError("Invalid 'new_sensor_ordering'") 831 832 self.__sensor_ordering = new_sensor_ordering.copy() 833
[docs] 834 def map_node_id_to_idx(self, node_id: str) -> int: 835 """ 836 Gets the index of a given node ID. 837 838 Parameters 839 ---------- 840 node_id : `str` 841 Node ID. 842 843 Returns 844 ------- 845 `int` 846 Index of the given node. 847 """ 848 if self.__node_id_to_idx is not None: 849 return self.__node_id_to_idx[node_id] 850 else: 851 return self.__nodes.index(node_id)
852 871
[docs] 872 def map_valve_id_to_idx(self, valve_id: str) -> int: 873 """ 874 Gets the index of a given valve ID. 875 876 Parameters 877 ---------- 878 valve_id : `str` 879 Valve ID. 880 881 Returns 882 ------- 883 `int` 884 Index of the given valve. 885 """ 886 if self.__valve_id_to_idx is not None: 887 return self.__valve_id_to_idx[valve_id] 888 else: 889 return self.__valves.index(valve_id)
890
[docs] 891 def map_pump_id_to_idx(self, pump_id: str) -> int: 892 """ 893 Gets the index of a given pump ID. 894 895 Parameters 896 ---------- 897 pump_id : `str` 898 Pump ID. 899 900 Returns 901 ------- 902 `int` 903 Index of the given pump. 904 """ 905 if self.__pump_id_to_idx is not None: 906 return self.__pump_id_to_idx[pump_id] 907 else: 908 return self.__pumps.index(pump_id)
909
[docs] 910 def map_tank_id_to_idx(self, tank_id: str) -> int: 911 """ 912 Gets the index of a given tank ID. 913 914 Parameters 915 ---------- 916 tank_id : `str` 917 Tank ID. 918 919 Returns 920 ------- 921 `int` 922 Index of the given tank. 923 """ 924 if self.__tank_id_to_idx is not None: 925 return self.__tank_id_to_idx[tank_id] 926 else: 927 return self.__tanks.index(tank_id)
928
[docs] 929 def map_bulkspecies_id_to_idx(self, bulk_species_id: str) -> int: 930 """ 931 Gets the index of a given bulk species ID. 932 933 Parameters 934 ---------- 935 bulk_species_id : `str` 936 Bulk species ID. 937 938 Returns 939 ------- 940 `int` 941 Index of the given bulk species. 942 """ 943 if self.__bulkspecies_id_to_idx is not None: 944 return self.__bulkspecies_id_to_idx[bulk_species_id] 945 else: 946 return self.__bulk_species.index(bulk_species_id)
947
[docs] 948 def map_surfacespecies_id_to_idx(self, surface_species_id: str) -> int: 949 """ 950 Gets the index of a given surface species ID. 951 952 Parameters 953 ---------- 954 surface_species_id : `str` 955 Surface species ID. 956 957 Returns 958 ------- 959 `int` 960 Index of the given surface species. 961 """ 962 if self.__surfacespecies_id_to_idx is not None: 963 return self.__surfacespecies_id_to_idx[surface_species_id] 964 else: 965 return self.__surface_species.index(surface_species_id)
966 967 def __compute_indices(self): 968 self.__pressure_idx = np.array([self.map_node_id_to_idx(n) 969 for n in self.__pressure_sensors], dtype=np.int32) 970 self.__flow_idx = np.array([self.map_link_id_to_idx(link) 971 for link in self.__flow_sensors], dtype=np.int32) 972 self.__demand_idx = np.array([self.map_node_id_to_idx(n) 973 for n in self.__demand_sensors], dtype=np.int32) 974 self.__quality_node_idx = np.array([self.map_node_id_to_idx(n) 975 for n in self.__quality_node_sensors], dtype=np.int32) 976 self.__quality_link_idx = np.array([self.map_link_id_to_idx(link) 977 for link in self.__quality_link_sensors], 978 dtype=np.int32) 979 self.__valve_state_idx = np.array([self.map_valve_id_to_idx(v) 980 for v in self.__valve_state_sensors], dtype=np.int32) 981 self.__pump_state_idx = np.array([self.map_pump_id_to_idx(p) 982 for p in self.__pump_state_sensors], dtype=np.int32) 983 self.__pump_efficiency_idx = np.array([self.map_pump_id_to_idx(p) 984 for p in self.__pump_efficiency_sensors], 985 dtype=np.int32) 986 self.__pump_energyconsumption_idx = np.array([self.map_pump_id_to_idx(p) 987 for p in self.__pump_energyconsumption_sensors], 988 dtype=np.int32) 989 self.__tank_volume_idx = np.array([self.map_tank_id_to_idx(t) 990 for t in self.__tank_volume_sensors], dtype=np.int32) 991 self.__bulk_species_node_idx = np.array([(self.map_bulkspecies_id_to_idx(s), 992 [self.map_node_id_to_idx(node_id) 993 for node_id in self.__bulk_species_node_sensors[s]]) 994 for s in self.__bulk_species_node_sensors.keys()], 995 dtype=object) 996 self.__bulk_species_link_idx = np.array([(self.map_bulkspecies_id_to_idx(s), 997 [self.map_link_id_to_idx(link_id) 998 for link_id in self.__bulk_species_link_sensors[s]]) 999 for s in self.__bulk_species_link_sensors.keys()], 1000 dtype=object) 1001 self.__surface_species_idx = np.array([(self.map_surfacespecies_id_to_idx(s), 1002 [self.map_link_id_to_idx(link_id) 1003 for link_id in self.__surface_species_sensors[s]]) 1004 for s in self.__surface_species_sensors.keys()], 1005 dtype=object) 1006 1007 n_pressure_sensors = len(self.__pressure_sensors) 1008 n_flow_sensors = len(self.__flow_sensors) 1009 n_demand_sensors = len(self.__demand_sensors) 1010 n_node_quality_sensors = len(self.__quality_node_sensors) 1011 n_link_quality_sensors = len(self.__quality_link_sensors) 1012 n_valve_state_sensors = len(self.__valve_state_sensors) 1013 n_pump_state_sensors = len(self.__pump_state_sensors) 1014 n_pump_efficiency_sensors = len(self.__pump_efficiency_sensors) 1015 n_pump_energyconsumption_sensors = len(self.__pump_energyconsumption_sensors) 1016 n_tank_volume_sensors = len(self.__tank_volume_sensors) 1017 n_bulk_species_node_sensors = len(list(itertools.chain( 1018 *self.__bulk_species_node_sensors.values()))) 1019 n_bulk_species_link_sensors = len(list(itertools.chain( 1020 *self.__bulk_species_link_sensors.values()))) 1021 n_surface_species_sensors = len(list(itertools.chain( 1022 *self.__surface_species_sensors.values()))) 1023 1024 current_shift = 0 1025 for sensor_type in self.__sensor_ordering: 1026 if sensor_type == SENSOR_TYPE_NODE_PRESSURE: 1027 pressure_idx_shift = current_shift 1028 current_shift += n_pressure_sensors 1029 elif sensor_type == SENSOR_TYPE_LINK_FLOW: 1030 flow_idx_shift = current_shift 1031 current_shift += n_flow_sensors 1032 elif sensor_type == SENSOR_TYPE_NODE_QUALITY: 1033 node_quality_idx_shift = current_shift 1034 current_shift += n_node_quality_sensors 1035 elif sensor_type == SENSOR_TYPE_NODE_DEMAND: 1036 demand_idx_shift = current_shift 1037 current_shift += n_demand_sensors 1038 elif sensor_type == SENSOR_TYPE_LINK_QUALITY: 1039 link_quality_idx_shift = current_shift 1040 current_shift += n_link_quality_sensors 1041 elif sensor_type == SENSOR_TYPE_VALVE_STATE: 1042 valve_state_idx_shift = current_shift 1043 current_shift += n_valve_state_sensors 1044 elif sensor_type == SENSOR_TYPE_PUMP_STATE: 1045 pump_state_idx_shift = current_shift 1046 current_shift += n_pump_state_sensors 1047 elif sensor_type == SENSOR_TYPE_PUMP_EFFICIENCY: 1048 pump_efficiency_idx_shift = current_shift 1049 current_shift += n_pump_efficiency_sensors 1050 elif sensor_type == SENSOR_TYPE_PUMP_ENERGYCONSUMPTION: 1051 pump_energyconsumption_idx_shift = current_shift 1052 current_shift += n_pump_energyconsumption_sensors 1053 elif sensor_type == SENSOR_TYPE_TANK_VOLUME: 1054 tank_volume_idx_shift = current_shift 1055 current_shift += n_tank_volume_sensors 1056 elif sensor_type == SENSOR_TYPE_NODE_BULK_SPECIES: 1057 bulk_species_node_idx_shift = current_shift 1058 current_shift += n_bulk_species_node_sensors 1059 elif sensor_type == SENSOR_TYPE_LINK_BULK_SPECIES: 1060 bulk_species_link_idx_shift = current_shift 1061 current_shift += n_bulk_species_link_sensors 1062 elif sensor_type == SENSOR_TYPE_SURFACE_SPECIES: 1063 surface_species_idx_shift = current_shift 1064 current_shift += n_surface_species_sensors 1065 else: 1066 raise ValueError( 1067 f"Invalid sensor type: {sensor_type}. " 1068 f"Valid sensor types are:\n{valid_sensor_types()}" 1069 ) 1070 1071 def __build_sensors_id_to_idx(sensors: list[str], initial_idx_shift: int) -> dict: 1072 return {sensor_id: i + initial_idx_shift 1073 for sensor_id, i in zip(sensors, range(len(sensors)))} 1074 1075 def __build_species_sensors_id_to_idx(species_sensors: dict, initial_idx_shift) -> dict: 1076 r = {} 1077 1078 cur_idx_shift = initial_idx_shift 1079 for species_id in species_sensors: 1080 r[species_id] = {} 1081 for sensor_id in species_sensors[species_id]: 1082 r[species_id][sensor_id] = cur_idx_shift 1083 cur_idx_shift += 1 1084 1085 return r 1086 1087 mapping = {"pressure": __build_sensors_id_to_idx(self.__pressure_sensors, 1088 pressure_idx_shift), 1089 "flow": __build_sensors_id_to_idx(self.__flow_sensors,flow_idx_shift), 1090 "demand": __build_sensors_id_to_idx(self.__demand_sensors,demand_idx_shift), 1091 "quality_node": __build_sensors_id_to_idx(self.__quality_node_sensors, 1092 node_quality_idx_shift), 1093 "quality_link": __build_sensors_id_to_idx(self.__quality_link_sensors, 1094 link_quality_idx_shift), 1095 "valve_state": __build_sensors_id_to_idx(self.__valve_state_sensors, 1096 valve_state_idx_shift), 1097 "pump_state": __build_sensors_id_to_idx(self.__pump_state_sensors, 1098 pump_state_idx_shift), 1099 "pump_efficiency": __build_sensors_id_to_idx(self.__pump_efficiency_sensors, 1100 pump_efficiency_idx_shift), 1101 "pump_energyconsumption": 1102 __build_sensors_id_to_idx(self.__pump_energyconsumption_sensors, 1103 pump_energyconsumption_idx_shift), 1104 "tank_volume": __build_sensors_id_to_idx(self.__tank_volume_sensors, 1105 tank_volume_idx_shift), 1106 "bulk_species_node": 1107 __build_species_sensors_id_to_idx(self.__bulk_species_node_sensors, 1108 bulk_species_node_idx_shift), 1109 "bulk_species_link": 1110 __build_species_sensors_id_to_idx(self.__bulk_species_link_sensors, 1111 bulk_species_link_idx_shift), 1112 "surface_species": 1113 __build_species_sensors_id_to_idx(self.__surface_species_sensors, 1114 surface_species_idx_shift)} 1115 self.__sensors_id_to_idx = mapping 1116
[docs] 1117 def validate(self, epanet_api: EPyT) -> None: 1118 """ 1119 Validates this sensor configuration -- 1120 i.e. checks whether all nodes, etc. exist in the .inp file. 1121 1122 Parameters 1123 ---------- 1124 epanet_api : `epanet_plus.EPyT <https://epanet-plus.readthedocs.io/en/stable/api.html#epanet_plus.epanet_toolkit.EPyT>`_ 1125 EPANET and EPANET-MSX API. 1126 """ 1127 if not isinstance(epanet_api, EPyT): 1128 raise TypeError("'epanet_api' must be an instance of 'epanet-plus.EPyT' " + 1129 f"but not of '{type(epanet_api)}'") 1130 1131 nodes = epanet_api.get_all_nodes_id() 1132 links = epanet_api.get_all_links_id() 1133 valves = epanet_api.get_all_valves_id() 1134 pumps = epanet_api.get_all_pumps_id() 1135 tanks = epanet_api.get_all_tanks_id() 1136 1137 bulk_species = [] 1138 surface_species = [] 1139 if epanet_api.msx_file is not None: 1140 for species_id, species_info in zip(epanet_api.get_all_msx_species_id(), 1141 epanet_api.get_all_msx_species_info()): 1142 if species_info["type"] == EpanetConstants.MSX_BULK: 1143 bulk_species.append(species_id) 1144 elif species_info["type"] == EpanetConstants.MSX_WALL: 1145 surface_species.append(species_id) 1146 1147 if any(node_id not in nodes for node_id in self.__nodes): 1148 raise ValueError("Invalid node ID detected -- " + 1149 "all given node IDs must exist in the .inp file") 1150 if any(link_id not in links for link_id in self.__links): 1151 raise ValueError("Invalid link/pipe ID detected -- all given link/pipe IDs " + 1152 "must exist in the .inp file") 1153 if any(valve_id not in valves for valve_id in self.__valves): 1154 raise ValueError("Invalid valve ID detected -- all given valve IDs must exist " + 1155 "in the .inp file") 1156 if any(pump_id not in pumps for pump_id in self.__pumps): 1157 raise ValueError("Invalid pump ID detected -- all given pump IDs must exist " + 1158 "in the .inp file") 1159 if any(tank_id not in tanks for tank_id in self.__tanks): 1160 raise ValueError("Invalid tank ID detected -- all given tank IDs must exist " + 1161 "in the .inp file") 1162 if any(surface_species_id not in surface_species 1163 for surface_species_id in self.__surface_species): 1164 raise ValueError("Invalid surface species ID detected") 1165 if any(bulk_species_id not in bulk_species for bulk_species_id in self.__bulk_species): 1166 raise ValueError("Invalid bulk species ID detected")
1167 1168 @property 1169 def nodes(self) -> list[str]: 1170 """ 1171 Gets all node IDs. 1172 1173 Returns 1174 ------- 1175 `list[str]` 1176 All node IDs. 1177 """ 1178 return self.__nodes.copy() 1179 1180 @property 1181 def links(self) -> list[str]: 1182 """ 1183 Gets all link IDs. 1184 1185 Returns 1186 ------- 1187 `list[str]` 1188 All link IDs. 1189 """ 1190 return self.__links.copy() 1191 1192 @property 1193 def junctions(self) -> list[str]: 1194 """ 1195 Returns all junction IDs. 1196 1197 Returns 1198 ------- 1199 `list[str]` 1200 All juncitons IDs. 1201 """ 1202 junctions = self.nodes 1203 for tank_id in self.tanks: 1204 junctions.remove(tank_id) 1205 1206 return junctions 1207 1208 @property 1209 def valves(self) -> list[str]: 1210 """ 1211 Gets all valve IDs (subset of link IDs). 1212 1213 Returns 1214 ------- 1215 `list[str]` 1216 All valve IDs. 1217 """ 1218 return self.__valves.copy() 1219 1220 @property 1221 def pumps(self) -> list[str]: 1222 """ 1223 Gets all pump IDs (subset of link IDs). 1224 1225 Returns 1226 ------- 1227 `list[str]` 1228 All pump IDs. 1229 """ 1230 return self.__pumps.copy() 1231 1232 @property 1233 def tanks(self) -> list[str]: 1234 """ 1235 Gets all tank IDs (subset of node IDs). 1236 1237 Returns 1238 ------- 1239 `list[str]` 1240 All tank IDs. 1241 """ 1242 return self.__tanks.copy() 1243 1244 @property 1245 def flow_unit(self) -> int: 1246 """ 1247 Returns the flow units. 1248 Note that this also specifies all other hydraulic units, except pressure. 1249 1250 Will be one of the following EPANET constants: 1251 1252 - EN_CFS = 0 (cubic foot/sec) 1253 - EN_GPM = 1 (gal/min) 1254 - EN_MGD = 2 (Million gal/day) 1255 - EN_IMGD = 3 (Imperial MGD) 1256 - EN_AFD = 4 (ac-foot/day) 1257 - EN_LPS = 5 (liter/sec) 1258 - EN_LPM = 6 (liter/min) 1259 - EN_MLD = 7 (Megaliter/day) 1260 - EN_CMH = 8 (cubic meter/hr) 1261 - EN_CMD = 9 (cubic meter/day) 1262 - EN_CMD = 10 (cubic meter/sec) 1263 1264 Returns 1265 ------- 1266 `int` 1267 Flow unit ID. 1268 """ 1269 return self.__flow_unit 1270 1271 @property 1272 def pressure_unit(self) -> int: 1273 """ 1274 Returns the pressure units. 1275 1276 Will be one of the following EPANET constants: 1277 1278 - EN_PSI = 0 (Pounds per square inch) 1279 - EN_KPA = 1 (Kilopascals) 1280 - EN_METERS = 2 (Meters) 1281 - EN_BAR = 3 (Bar) 1282 - EN_FEET = 4 (Feet) 1283 1284 Returns 1285 ------- 1286 `int` 1287 Pressure unit ID. 1288 """ 1289 return self.__pressure_unit 1290 1291 @property 1292 def quality_unit(self) -> int: 1293 """ 1294 Gets the measurement unit ID used in the basic quality analysis. 1295 1296 Will be one of the following constants: 1297 1298 - MASS_UNIT_MG = 4 (milligram) 1299 - MASS_UNIT_UG = 5 (microgram) 1300 - TIME_UNIT_HRS = 6 (hours) 1301 1302 Returns 1303 ------- 1304 `int` 1305 Mass unit ID. 1306 """ 1307 return self.__quality_unit 1308 1309 @property 1310 def bulk_species(self) -> list[str]: 1311 """ 1312 Gets all bulk species IDs -- i.e. species that live in the water. 1313 1314 Returns 1315 ------- 1316 `list[str]` 1317 All species IDs. 1318 """ 1319 return self.__bulk_species.copy() 1320 1321 @property 1322 def surface_species(self) -> list[str]: 1323 """ 1324 Gets all surface species IDs -- i.e. species that live links/pipes. 1325 1326 Returns 1327 ------- 1328 `list[str]` 1329 All species IDs. 1330 """ 1331 return self.__surface_species.copy() 1332 1333 @property 1334 def bulk_species_mass_unit(self) -> list[int]: 1335 """ 1336 Gets the mass unit of each bulk species. 1337 1338 Will be one of the following constants: 1339 1340 - MASS_UNIT_MG = 4 (milligram) 1341 - MASS_UNIT_UG = 5 (microgram) 1342 - MASS_UNIT_MOL = 6 (mole) 1343 - MASS_UNIT_MMOL = 7 (millimole) 1344 1345 Returns 1346 ------- 1347 `int` 1348 Mass unit ID. 1349 """ 1350 return self.__bulk_species_mass_unit 1351 1352 @property 1353 def surface_species_mass_unit(self) -> list[int]: 1354 """ 1355 Gets the mass unit of each surface species. 1356 1357 Will be one of the following constants: 1358 1359 - MASS_UNIT_MG = 4 (milligram) 1360 - MASS_UNIT_UG = 5 (microgram) 1361 - MASS_UNIT_MOL = 6 (mole) 1362 - MASS_UNIT_MMOL = 7 (millimole) 1363 1364 Returns 1365 ------- 1366 `int` 1367 Mass unit ID. 1368 """ 1369 return self.__surface_species_mass_unit 1370 1371 @property 1372 def surface_species_area_unit(self) -> int: 1373 """ 1374 Gets the surface species area unit. 1375 1376 Will be one of the following constants: 1377 1378 - AREA_UNIT_FT2 = 1 (square feet) 1379 - AREA_UNIT_M2 = 2 (square meters) 1380 - AREA_UNIT_CM2 = 3 (square centimeters) 1381 1382 Returns 1383 ------- 1384 `int` 1385 Area unit ID. 1386 """ 1387 return self.__surface_species_area_unit 1388 1389 @property 1390 def pressure_sensors(self) -> list[str]: 1391 """ 1392 Gets all pressure sensors (i.e. IDs of nodes at which a pressure sensor is placed). 1393 1394 Returns 1395 ------- 1396 `list[str]` 1397 All node IDs with a pressure sensor. 1398 """ 1399 return self.__pressure_sensors.copy() 1400 1401 @pressure_sensors.setter 1402 def pressure_sensors(self, pressure_sensors: list[str]) -> None: 1403 if not isinstance(pressure_sensors, list): 1404 raise TypeError("'pressure_sensors' must be an instance of 'list[str]' " + 1405 f"but not of '{type(pressure_sensors)}'") 1406 if any(n not in self.__nodes for n in pressure_sensors): 1407 raise ValueError("Each item in 'pressure_sensors' must be in 'nodes' -- cannot " + 1408 "place a sensor at a non-existing node.") 1409 1410 self.__pressure_sensors = pressure_sensors 1411 1412 self.__compute_indices() 1413 1414 @property 1415 def flow_sensors(self) -> list[str]: 1416 """ 1417 Gets all flow sensors (i.e. IDs of links at which a flow sensor is placed). 1418 1419 Returns 1420 ------- 1421 `list[str]` 1422 All link IDs with a flow sensor. 1423 """ 1424 return self.__flow_sensors.copy() 1425 1426 @flow_sensors.setter 1427 def flow_sensors(self, flow_sensors: list[str]) -> None: 1428 if not isinstance(flow_sensors, list): 1429 raise TypeError("'pressure_sensors' must be an instance of 'list[str]' " + 1430 f"but not of '{type(flow_sensors)}'") 1431 if any(link not in self.__links for link in flow_sensors): 1432 raise ValueError("Each item in 'flow_sensors' must be in 'links' -- cannot " + 1433 "place a sensor at a non-existing link/pipe.") 1434 1435 self.__flow_sensors = flow_sensors 1436 1437 self.__compute_indices() 1438 1439 @property 1440 def demand_sensors(self) -> list[str]: 1441 """ 1442 Gets all demand sensors (i.e. IDs of nodes at which a demand sensor is placed). 1443 1444 Returns 1445 ------- 1446 `list[str]` 1447 All node IDs with a demand sensor. 1448 """ 1449 return self.__demand_sensors.copy() 1450 1451 @demand_sensors.setter 1452 def demand_sensors(self, demand_sensors: list[str]) -> None: 1453 if not isinstance(demand_sensors, list): 1454 raise TypeError("'demand_sensors' must be an instance of 'list[str]' " + 1455 f"but not of '{type(demand_sensors)}'") 1456 if any(n not in self.__nodes for n in demand_sensors): 1457 raise ValueError("Each item in 'demand_sensors' must be in 'nodes' -- cannot " + 1458 "place a sensor at a non-existing node.") 1459 1460 self.__demand_sensors = demand_sensors 1461 1462 self.__compute_indices() 1463 1464 @property 1465 def quality_node_sensors(self) -> list[str]: 1466 """ 1467 Gets all node quality sensors (i.e. IDs of nodes at which a node quality sensor is placed). 1468 1469 Returns 1470 ------- 1471 `list[str]` 1472 All node IDs with a node quality sensor. 1473 """ 1474 return self.__quality_node_sensors.copy() 1475 1476 @quality_node_sensors.setter 1477 def quality_node_sensors(self, quality_node_sensors: list[str]) -> None: 1478 if not isinstance(quality_node_sensors, list): 1479 raise TypeError("'quality_node_sensors' must be an instance of 'list[str]' " + 1480 f"but not of '{type(quality_node_sensors)}'") 1481 if any(n not in self.__nodes for n in quality_node_sensors): 1482 raise ValueError("Each item in 'quality_node_sensors' must be in 'nodes' -- cannot " + 1483 "place a sensor at a non-existing node.") 1484 1485 self.__quality_node_sensors = quality_node_sensors 1486 1487 self.__compute_indices() 1488 1489 @property 1490 def quality_link_sensors(self) -> list[str]: 1491 """ 1492 Gets all link quality sensors (i.e. IDs of links at which a link quality sensor is placed). 1493 1494 Returns 1495 ------- 1496 `list[str]` 1497 All link IDs with a link quality sensor. 1498 """ 1499 return self.__quality_link_sensors.copy() 1500 1501 @quality_link_sensors.setter 1502 def quality_link_sensors(self, quality_link_sensors: list[str]) -> None: 1503 if not isinstance(quality_link_sensors, list): 1504 raise TypeError("'quality_link_sensors' must be an instance of 'list[str]' " + 1505 f"but not of '{type(quality_link_sensors)}'") 1506 if any(link not in self.__links for link in quality_link_sensors): 1507 raise ValueError("Each item in 'quality_link_sensors' must be in 'links' -- cannot " + 1508 "place a sensor at a non-existing link/pipe.") 1509 1510 self.__quality_link_sensors = quality_link_sensors 1511 1512 self.__compute_indices() 1513 1514 @property 1515 def valve_state_sensors(self) -> list[str]: 1516 """ 1517 Gets all valve state sensors (i.e. IDs of valves at which a valve state sensor is placed). 1518 1519 Returns 1520 ------- 1521 `list[str]` 1522 All valve IDs with a valve state sensor. 1523 """ 1524 return self.__valve_state_sensors.copy() 1525 1526 @valve_state_sensors.setter 1527 def valve_state_sensors(self, valve_state_sensors: list[str]) -> None: 1528 if not isinstance(valve_state_sensors, list): 1529 raise TypeError("'valve_state_sensors' must be an instance of 'list[str]' " + 1530 f"but not of '{type(valve_state_sensors)}'") 1531 if any(link not in self.__valves for link in valve_state_sensors): 1532 raise ValueError("Each item in 'valve_state_sensors' must be in 'valves' -- cannot " + 1533 "place a sensor at a non-existing valves.") 1534 1535 self.__valve_state_sensors = valve_state_sensors 1536 1537 self.__compute_indices() 1538 1539 @property 1540 def pump_state_sensors(self) -> list[str]: 1541 """ 1542 Gets all pump state sensors (i.e. IDs of pumps at which a pump state sensor is placed). 1543 1544 Returns 1545 ------- 1546 `list[str]` 1547 All link IDs with a pump state sensor. 1548 """ 1549 return self.__pump_state_sensors.copy() 1550 1551 @pump_state_sensors.setter 1552 def pump_state_sensors(self, pump_state_sensors: list[str]) -> None: 1553 if not isinstance(pump_state_sensors, list): 1554 raise TypeError("'pump_state_sensors' must be an instance of 'list[str]' " + 1555 f"but not of '{type(pump_state_sensors)}'") 1556 if any(link not in self.__pumps for link in pump_state_sensors): 1557 raise ValueError("Each item in 'pump_state_sensors' must be in 'pumps' -- cannot " + 1558 "place a sensor at a non-existing pump.") 1559 1560 self.__pump_state_sensors = pump_state_sensors 1561 1562 self.__compute_indices() 1563 1564 @property 1565 def pump_energyconsumption_sensors(self) -> list[str]: 1566 """ 1567 Gets all pump energy consumption sensors 1568 (i.e. IDs of pumps at which the energy consumption is monitored). 1569 1570 Returns 1571 ------- 1572 `list[str]` 1573 All pump IDs with an energy consumption sensor. 1574 """ 1575 return self.__pump_energyconsumption_sensors.copy() 1576 1577 @pump_energyconsumption_sensors.setter 1578 def pump_energyconsumption_sensors(self, pump_energyconsumption_sensors: list[str]) -> None: 1579 if not isinstance(pump_energyconsumption_sensors, list): 1580 raise TypeError("'pump_energyconsumption_sensors' must be an instance of 'list[str]' " + 1581 f"but not of '{type(pump_energyconsumption_sensors)}'") 1582 if any(link not in self.__pumps for link in pump_energyconsumption_sensors): 1583 raise ValueError("Each item in 'pump_energyconsumption_sensors' must be in 'pumps' " + 1584 "-- cannot place a sensor at a non-existing pump.") 1585 1586 self.__pump_energyconsumption_sensors = pump_energyconsumption_sensors 1587 1588 self.__compute_indices() 1589 1590 @property 1591 def pump_efficiency_sensors(self) -> list[str]: 1592 """ 1593 Gets all pump efficiency sensors 1594 (i.e. IDs of pumps at which the efficiency is monitored). 1595 1596 Returns 1597 ------- 1598 `list[str]` 1599 All pump IDs with an efficiency sensor. 1600 """ 1601 return self.__pump_efficiency_sensors.copy() 1602 1603 @pump_efficiency_sensors.setter 1604 def pump_efficiency_sensors(self, pump_efficiency_sensors: list[str]) -> None: 1605 if not isinstance(pump_efficiency_sensors, list): 1606 raise TypeError("'pump_efficiency_sensors' must be an instance of 'list[str]' " + 1607 f"but not of '{type(pump_efficiency_sensors)}'") 1608 if any(link not in self.__pumps for link in pump_efficiency_sensors): 1609 raise ValueError("Each item in 'pump_efficiency_sensors' must be in 'pumps' " + 1610 "-- cannot place a sensor at a non-existing pump.") 1611 1612 self.__pump_efficiency_sensors = pump_efficiency_sensors 1613 1614 self.__compute_indices() 1615 1616 @property 1617 def tank_volume_sensors(self) -> list[str]: 1618 """ 1619 Gets all tank volume sensors (i.e. IDs of tanks at which a tank volume sensor is placed). 1620 1621 Returns 1622 ------- 1623 `list[str]` 1624 All tank IDs with a tank volume sensor. 1625 """ 1626 return self.__tank_volume_sensors.copy() 1627 1628 @tank_volume_sensors.setter 1629 def tank_volume_sensors(self, tank_volume_sensors: list[str]) -> None: 1630 if not isinstance(tank_volume_sensors, list): 1631 raise TypeError("'tank_volume_sensors' must be an instance of 'list[str]' " + 1632 f"but not of '{type(tank_volume_sensors)}'") 1633 if any(n not in self.__tanks for n in tank_volume_sensors): 1634 raise ValueError("Each item in 'tank_volume_sensors' must be in 'tanks' -- cannot " + 1635 "place a sensor at a non-existing tanks.") 1636 1637 self.__tank_volume_sensors = tank_volume_sensors 1638 1639 self.__compute_indices() 1640 1641 @property 1642 def bulk_species_node_sensors(self) -> dict: 1643 """ 1644 Gets all bulk species node sensors as a dictionary -- 1645 i.e. bulk species IDs as keys and node IDs as values. 1646 1647 Returns 1648 ------- 1649 `dict` 1650 Bulk species sensors -- keys: bulk species IDs, values: node IDs. 1651 """ 1652 return deepcopy(self.__bulk_species_node_sensors) 1653 1654 @bulk_species_node_sensors.setter 1655 def bulk_species_node_sensors(self, bulk_species_sensors: dict) -> None: 1656 if not isinstance(bulk_species_sensors, dict): 1657 raise TypeError("'bulk_species_sensors' must be an instance of 'dict' " + 1658 f"but not of '{type(bulk_species_sensors)}'") 1659 if any(species_id not in self.__bulk_species for species_id in bulk_species_sensors.keys()): 1660 raise ValueError("Unknown bulk species ID in 'bulk_species_sensors'") 1661 if any(node_id not in self.__nodes for node_id in sum(bulk_species_sensors.values(), [])): 1662 raise ValueError("Unknown node ID in 'bulk_species_sensors'") 1663 1664 self.__bulk_species_node_sensors = bulk_species_sensors 1665 1666 self.__compute_indices() 1667 1668 @property 1669 def bulk_species_link_sensors(self) -> dict: 1670 """ 1671 Gets all bulk species link/pipe sensors as a dictionary -- 1672 i.e. bulk species IDs as keys and link/pipe IDs as values. 1673 1674 Returns 1675 ------- 1676 `dict` 1677 Bulk species sensors -- keys: bulk species IDs, values: link/pipe IDs. 1678 """ 1679 return deepcopy(self.__bulk_species_link_sensors) 1680 1681 @bulk_species_link_sensors.setter 1682 def bulk_species_link_sensors(self, bulk_species_sensors: dict) -> None: 1683 if not isinstance(bulk_species_sensors, dict): 1684 raise TypeError("'bulk_species_sensors' must be an instance of 'dict' " + 1685 f"but not of '{type(bulk_species_sensors)}'") 1686 if any(species_id not in self.__bulk_species for species_id in bulk_species_sensors.keys()): 1687 raise ValueError("Unknown bulk species ID in 'bulk_species_sensors'") 1688 if any(link_id not in self.__links for link_id in list(itertools.chain( 1689 *bulk_species_sensors.values()))): 1690 raise ValueError("Unknown link/pipe ID in 'bulk_species_sensors'") 1691 1692 self.__bulk_species_link_sensors = bulk_species_sensors 1693 1694 self.__compute_indices() 1695 1696 @property 1697 def surface_species_sensors(self) -> dict: 1698 """ 1699 Gets all surface species sensors as a dictionary -- 1700 i.e. surface species IDs as keys and link/pipe IDs as values. 1701 1702 Returns 1703 ------- 1704 `dict` 1705 Surface species sensors -- keys: surface species IDs, values: link/pipe IDs. 1706 """ 1707 return deepcopy(self.__surface_species_sensors) 1708 1709 @surface_species_sensors.setter 1710 def surface_species_sensors(self, surface_species_sensors: dict) -> None: 1711 if not isinstance(surface_species_sensors, dict): 1712 raise TypeError("'surface_species_sensors' must be an instance of 'dict' " + 1713 f"but not of '{type(surface_species_sensors)}'") 1714 if any(species_id not in self.__surface_species 1715 for species_id in surface_species_sensors.keys()): 1716 raise ValueError("Unknown surface species ID in 'surface_species_sensors'") 1717 if any(link_id not in self.__links 1718 for link_id in list(itertools.chain(*surface_species_sensors.values()))): 1719 raise ValueError("Unknown link/pipe ID in 'surface_species_sensors'") 1720 1721 self.__surface_species_sensors = surface_species_sensors 1722 1723 self.__compute_indices() 1724 1725 @property 1726 def sensors_id_to_idx(self) -> dict: 1727 """ 1728 Gets a mapping of sensor IDs to indices in the final `Numpy array <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ returned by `get_data()`. 1729 1730 Returns 1731 ------- 1732 `dict` 1733 Mapping of sensor IDs to indices in the final `Numpy array <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_. 1734 """ 1735 return deepcopy(self.__sensors_id_to_idx) 1736
[docs] 1737 def get_as_dict(self) -> dict: 1738 """ 1739 Gets the sensor configuration as a dictionary. 1740 1741 Returns 1742 ------- 1743 `dict` 1744 Dictionary of set sensors -- the keys are the sensor types. 1745 """ 1746 r = {} 1747 1748 if self.__pressure_sensors != []: 1749 r["pressure"] = self.__pressure_sensors 1750 if self.__flow_sensors != []: 1751 r["flow"] = self.__flow_sensors 1752 if self.__demand_sensors != []: 1753 r["demand"] = self.__demand_sensors 1754 if self.__tank_volume_sensors != []: 1755 r["tank_volume"] = self.__tank_volume_sensors 1756 if self.__valve_state_sensors != []: 1757 r["valve_state"] = self.__valve_state_sensors 1758 if self.__pump_state_sensors != []: 1759 r["pump_state"] = self.__pump_state_sensors 1760 if self.__pump_efficiency_sensors != []: 1761 r["pump_efficiency"] = self.__pump_efficiency_sensors 1762 if self.__pump_energyconsumption_sensors != []: 1763 r["pump_energyconsumption"] = self.__pump_energyconsumption_sensors 1764 if self.__quality_node_sensors != []: 1765 r["node_quality"] = self.__quality_node_sensors 1766 if self.__quality_link_sensors != []: 1767 r["link_quality"] = self.__quality_link_sensors 1768 if self.__bulk_species_node_sensors != {}: 1769 r["node_bulk_species"] = self.__bulk_species_node_sensors 1770 if self.__bulk_species_link_sensors != {}: 1771 r["link_bulk_species"] = self.__bulk_species_link_sensors 1772 if self.__surface_species_sensors != {}: 1773 r["surface_species"] = self.__surface_species_sensors 1774 1775 return r
1776
[docs] 1777 def get_attributes(self) -> dict: 1778 attr = {"nodes": self.__nodes, "links": self.__links, 1779 "valves": self.__valves, "pumps": self.__pumps, 1780 "tanks": self.__tanks, "bulk_species": self.__bulk_species, 1781 "surface_species": self.__surface_species, 1782 "sensor_ordering": self.__sensor_ordering, 1783 "pressure_sensors": self.__pressure_sensors, 1784 "flow_sensors": self.__flow_sensors, 1785 "demand_sensors": self.__demand_sensors, 1786 "quality_node_sensors": self.__quality_node_sensors, 1787 "quality_link_sensors": self.__quality_link_sensors, 1788 "valve_state_sensors": self.__valve_state_sensors, 1789 "pump_state_sensors": self.__pump_state_sensors, 1790 "pump_efficiency_sensors": self.__pump_efficiency_sensors, 1791 "pump_energyconsumption_sensors": self.__pump_energyconsumption_sensors, 1792 "tank_volume_sensors": self.__tank_volume_sensors, 1793 "bulk_species_node_sensors": self.__bulk_species_node_sensors, 1794 "bulk_species_link_sensors": self.__bulk_species_link_sensors, 1795 "surface_species_sensors": self.__surface_species_sensors, 1796 "node_id_to_idx": self.__node_id_to_idx, 1797 "link_id_to_idx": self.__link_id_to_idx, 1798 "valve_id_to_idx": self.__valve_id_to_idx, 1799 "pump_id_to_idx": self.__pump_id_to_idx, 1800 "tank_id_to_idx": self.__tank_id_to_idx, 1801 "bulkspecies_id_to_idx": self.__bulkspecies_id_to_idx, 1802 "surfacespecies_id_to_idx": self.__surfacespecies_id_to_idx, 1803 "flow_unit": self.__flow_unit, 1804 "pressure_unit": self.__pressure_unit, 1805 "quality_unit": self.__quality_unit, 1806 "bulk_species_mass_unit": self.__bulk_species_mass_unit, 1807 "surface_species_mass_unit": self.__surface_species_mass_unit, 1808 "surface_species_area_unit": self.__surface_species_area_unit} 1809 1810 return super().get_attributes() | attr
1811 1812 def __eq__(self, other) -> bool: 1813 if not isinstance(other, SensorConfig): 1814 raise TypeError("Can not compare 'SensorConfig' instance " + 1815 f"with '{type(other)}' instance") 1816 1817 return self.__nodes == other.nodes and self.__links == other.links \ 1818 and self.__valves == other.valves and self.__pumps == other.pumps \ 1819 and self.__tanks == other.tanks and self.__bulk_species == other.bulk_species \ 1820 and self.__surface_species == other.surface_species \ 1821 and self.__sensor_ordering == other.sensor_ordering \ 1822 and self.__pressure_sensors == other.pressure_sensors \ 1823 and self.__flow_sensors == other.flow_sensors \ 1824 and self.__demand_sensors == other.demand_sensors \ 1825 and self.__quality_node_sensors == other.quality_node_sensors \ 1826 and self.__quality_link_sensors == other.quality_link_sensors \ 1827 and self.__valve_state_sensors == other.valve_state_sensors \ 1828 and self.__pump_state_sensors == other.pump_state_sensors \ 1829 and self.__pump_efficiency_sensors == other.pump_efficiency_sensors \ 1830 and self.__pump_energyconsumption_sensors == other.pump_energyconsumption_sensors \ 1831 and self.__tank_volume_sensors == other.tank_volume_sensors \ 1832 and self.__bulk_species_node_sensors == other.bulk_species_node_sensors \ 1833 and self.__bulk_species_link_sensors == other.bulk_species_link_sensors \ 1834 and self.__surface_species_sensors == other.surface_species_sensors \ 1835 and self.__flow_unit == other.flow_unit \ 1836 and self.__pressure_unit == other.pressure_unit \ 1837 and self.__quality_unit == other.quality_unit \ 1838 and self.__bulk_species_mass_unit == other.bulk_species_mass_unit \ 1839 and self.__surface_species_mass_unit == other.surface_species_mass_unit \ 1840 and self.__surface_species_area_unit == other.surface_species_area_unit \ 1841 and self.__node_id_to_idx == other.node_id_to_idx \ 1842 and self.__link_id_to_idx == other.link_id_to_idx \ 1843 and self.__valve_id_to_idx == other.valve_id_to_idx \ 1844 and self.__pump_id_to_idx == other.pump_id_to_idx \ 1845 and self.__tank_id_to_idx == other.tank_id_to_idx \ 1846 and self.__bulkspecies_id_to_idx == other.bulkspecies_id_to_idx \ 1847 and self.__surfacespecies_id_to_idx == other.surfacespecies_id_to_idx 1848 1849 def __str__(self) -> str: 1850 return f"nodes: {self.__nodes} links: {self.__links} valves: {self.__valves} " +\ 1851 f"pumps: {self.__pumps} tanks: {self.__tanks} bulk_species: {self.__bulk_species} " +\ 1852 f"surface_species: {self.__surface_species} " + \ 1853 f"sensor ordering: {self.__sensor_ordering} " + \ 1854 f"node_id_to_idx: {self.__node_id_to_idx} link_id_to_idx: {self.__link_id_to_idx} " +\ 1855 f"pump_id_to_idx: {self.__pump_id_to_idx} tank_id_to_idx: {self.__tank_id_to_idx} " +\ 1856 f"valve_id_to_idx: {self.__valve_id_to_idx} " +\ 1857 f"bulkspecies_id_to_idx: {self.__bulkspecies_id_to_idx} " +\ 1858 f"surfacespecies_id_to_idx: {self.__surfacespecies_id_to_idx} " +\ 1859 f"pressure_sensors: {self.__pressure_sensors} flow_sensors: {self.__flow_sensors} " +\ 1860 f"demand_sensors: {self.__demand_sensors} " +\ 1861 f"quality_node_sensors: {self.__quality_node_sensors} " +\ 1862 f"quality_link_sensors: {self.__quality_link_sensors} " +\ 1863 f"valve_state_sensors: {self.__valve_state_sensors} " +\ 1864 f"pump_state_sensors: {self.__pump_state_sensors} " +\ 1865 f"pump_efficiency_sensors: {self.__pump_efficiency_sensors} " +\ 1866 f"pump_energyconsumption_sensors: {self.__pump_energyconsumption_sensors} " +\ 1867 f"tank_volume_sensors: {self.__tank_volume_sensors} " +\ 1868 f"bulk_species_node_sensors: {self.__bulk_species_node_sensors} " +\ 1869 f"bulk_species_link_sensors: {self.__bulk_species_link_sensors} " +\ 1870 f"surface_species_sensors: {self.__surface_species_sensors} " +\ 1871 f"flow_unit: {flowunit_to_str(self.__flow_unit)} " +\ 1872 f"pressure_unit: {pressureunit_to_str(self.__pressure_unit)} " +\ 1873 f"quality_unit: {qualityunit_to_str(self.__quality_unit)} " +\ 1874 "bulk_species_mass_unit: " +\ 1875 f"{list(map(massunit_to_str, self.__bulk_species_mass_unit))} " +\ 1876 "surface_species_mass_unit: " +\ 1877 f"{list(map(massunit_to_str, self.__surface_species_mass_unit))} " +\ 1878 f"surface_species_area_unit: {areaunit_to_str(self.__surface_species_area_unit)}" 1879
[docs] 1880 def get_bulk_species_mass_unit_id(self, bulk_species_id: str) -> int: 1881 """ 1882 Returns the mass unit of a given bulk species. 1883 1884 Parameters 1885 ---------- 1886 bulk_species_id : `str` 1887 ID of the bulk species. 1888 1889 Returns 1890 ------- 1891 `int` 1892 ID of the mass unit. 1893 1894 Will be one of the following constant: 1895 1896 - MASS_UNIT_MG = 4 1897 - MASS_UNIT_UG = 5 1898 - MASS_UNIT_MOL = 6 1899 - MASS_UNIT_MMOL = 7 1900 """ 1901 return self.__bulk_species_mass_unit[self.map_bulkspecies_id_to_idx(bulk_species_id)]
1902
[docs] 1903 def get_surface_species_mass_unit_id(self, surface_species_id: str) -> int: 1904 """ 1905 Returns the mass unit of a given surface species. 1906 1907 Parameters 1908 ---------- 1909 surface_species_id : `str` 1910 ID of the surface species. 1911 1912 Returns 1913 ------- 1914 `int` 1915 ID of the mass unit. 1916 1917 Will be one of the following constant: 1918 1919 - MASS_UNIT_MG = 4 1920 - MASS_UNIT_UG = 5 1921 - MASS_UNIT_MOL = 6 1922 - MASS_UNIT_MMOL = 7 1923 """ 1924 return self.__surface_species_mass_unit[self.map_surfacespecies_id_to_idx( 1925 surface_species_id)]
1926 1927 def _append_readings_if_possible(self, data: list, reading: Optional[np.ndarray], reading_idx: list, request_condition: bool, sensor_description: str) -> list: 1928 if reading is not None: 1929 data.append(reading[:, reading_idx]) 1930 else: 1931 if request_condition: 1932 raise ValueError( 1933 f"{sensor_description} readings requested, " 1934 f"but no such data is given" 1935 ) 1936 return data 1937
[docs] 1938 def compute_readings(self, pressures: np.ndarray, flows: np.ndarray, demands: np.ndarray, 1939 nodes_quality: np.ndarray, links_quality: np.ndarray, 1940 pumps_state: np.ndarray, pumps_efficiency: np.ndarray, 1941 pumps_energyconsumption: np.ndarray, valves_state: np.ndarray, 1942 tanks_volume: np.ndarray, bulk_species_node_concentrations: np.ndarray, 1943 bulk_species_link_concentrations: np.ndarray, 1944 surface_species_concentrations: np.ndarray) -> np.ndarray: 1945 """ 1946 Applies the sensor configuration to a set of raw simulation results -- i.e. computes 1947 the sensor readings as an array. 1948 1949 Columns (i.e. sensor readings) are ordered as according to sensor_ordering. 1950 If not changed, the following default order applies: 1951 1952 1. Pressures 1953 2. Flows 1954 3. Demands 1955 4. Nodes quality 1956 5. Links quality 1957 6. Valve state 1958 7. Pumps state 1959 8. Pumps efficiency 1960 9. Pumps energy consumption 1961 10. Tanks volume 1962 11. Surface species concentrations 1963 12. Bulk species nodes concentrations 1964 13. Bulk species links concentrations 1965 1966 Parameters 1967 ---------- 1968 pressures : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 1969 Pressure values at all nodes. 1970 flows : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 1971 Flow values at all links/pipes. 1972 demands : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 1973 Demand values at all nodes. 1974 nodes_quality : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 1975 Quality values at all nodes. 1976 links_quality : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 1977 Quality values at all links/pipes. 1978 pumps_state : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 1979 States of all pumps. 1980 pumps_efficiency : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 1981 Efficiency of all pumps. 1982 pumps_energyconsumption : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 1983 Energy consumption of all pumps. 1984 valves_state : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 1985 States of all valves. 1986 tanks_volume : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 1987 Water volume in all tanks. 1988 bulk_species_node_concentrations : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 1989 Bulk species concentrations at all nodes. 1990 1991 Expect a three-dimensional array: First dimension denotes time, 1992 second dimension corresponds to species ID, 1993 and third dimension contains the concentration. 1994 bulk_species_link_concentrations : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 1995 Bulk species concentrations at all links/pipes. 1996 1997 Expect a three-dimensional array: First dimension denotes time, 1998 second dimension corresponds to species ID, 1999 and third dimension contains the concentration. 2000 surface_species_concentrations : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 2001 Surface species concentrations at all links/pipes. 2002 2003 Expect a three-dimensional array: First dimension denotes time, 2004 second dimension corresponds to species ID, 2005 and third dimension contains the concentration. 2006 2007 Returns 2008 ------- 2009 `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_ 2010 Sensor readings. 2011 """ 2012 data = [] 2013 2014 for sensor_type in self.sensor_ordering: 2015 if sensor_type==SENSOR_TYPE_NODE_PRESSURE: 2016 data = self._append_readings_if_possible( 2017 data, pressures, self.__pressure_idx, 2018 len(self.__pressure_sensors) != 0, 2019 "Pressure" 2020 ) 2021 elif sensor_type==SENSOR_TYPE_NODE_QUALITY: 2022 data = self._append_readings_if_possible( 2023 data, nodes_quality, self.__quality_node_idx, 2024 len(self.__quality_node_sensors) != 0, 2025 "Node water quality" 2026 ) 2027 elif sensor_type==SENSOR_TYPE_NODE_DEMAND: 2028 data = self._append_readings_if_possible( 2029 data, demands, self.__demand_idx, 2030 len(self.__demand_sensors) != 0, 2031 "Demand" 2032 ) 2033 elif sensor_type==SENSOR_TYPE_LINK_FLOW: 2034 data = self._append_readings_if_possible( 2035 data, flows, self.__flow_idx, 2036 len(self.__flow_sensors) != 0, 2037 "Flow" 2038 ) 2039 elif sensor_type==SENSOR_TYPE_LINK_QUALITY: 2040 data = self._append_readings_if_possible( 2041 data, links_quality, self.__quality_link_idx, 2042 len(self.__quality_link_sensors) != 0, 2043 "Link/Pipe water quality" 2044 ) 2045 elif sensor_type==SENSOR_TYPE_VALVE_STATE: 2046 data = self._append_readings_if_possible( 2047 data, valves_state, self.__valve_state_idx, 2048 len(self.__valve_state_sensors) != 0, 2049 "Valve state" 2050 ) 2051 elif sensor_type==SENSOR_TYPE_PUMP_STATE: 2052 data = self._append_readings_if_possible( 2053 data, pumps_state, self.__pump_state_idx, 2054 len(self.__pump_state_sensors) != 0, 2055 "Pump state" 2056 ) 2057 elif sensor_type==SENSOR_TYPE_TANK_VOLUME: 2058 data = self._append_readings_if_possible( 2059 data, tanks_volume, self.__tank_volume_idx, 2060 len(self.__tank_volume_sensors) != 0, 2061 "Tank water volume" 2062 ) 2063 elif sensor_type==SENSOR_TYPE_NODE_BULK_SPECIES: 2064 if bulk_species_node_concentrations is not None: 2065 for species_idx, nodes_idx in self.__bulk_species_node_idx: 2066 data.append( 2067 bulk_species_node_concentrations[ 2068 :, species_idx, nodes_idx 2069 ].reshape(-1, len(nodes_idx)) 2070 ) 2071 else: 2072 if len(self.__bulk_species_node_sensors) != 0: 2073 raise ValueError("Bulk species concentratinons requested but no " + 2074 "bulk species node concentration data is given") 2075 elif sensor_type==SENSOR_TYPE_LINK_BULK_SPECIES: 2076 if bulk_species_link_concentrations is not None: 2077 for species_idx, links_idx in self.__bulk_species_link_idx: 2078 data.append( 2079 bulk_species_link_concentrations[ 2080 :, species_idx, links_idx 2081 ].reshape(-1, len(links_idx)) 2082 ) 2083 else: 2084 if len(self.__bulk_species_link_sensors) != 0: 2085 raise ValueError("Bulk species concentratinons requested but no " + 2086 "bulk species link/pipe concentration data is given") 2087 elif sensor_type==SENSOR_TYPE_SURFACE_SPECIES: 2088 if surface_species_concentrations is not None: 2089 for species_idx, links_idx in self.__surface_species_idx: 2090 data.append( 2091 surface_species_concentrations[ 2092 :, species_idx, links_idx 2093 ].reshape(-1, len(links_idx)) 2094 ) 2095 else: 2096 if len(self.__surface_species_sensors) != 0: 2097 raise ValueError("Surface species concentratinons requested but no " + 2098 "surface species concentration data is given") 2099 elif sensor_type==SENSOR_TYPE_PUMP_EFFICIENCY: 2100 data = self._append_readings_if_possible( 2101 data, pumps_efficiency, self.__pump_efficiency_idx, 2102 len(self.__pump_efficiency_sensors) != 0, 2103 "Pump efficiency" 2104 ) 2105 elif sensor_type==SENSOR_TYPE_PUMP_ENERGYCONSUMPTION: 2106 data = self._append_readings_if_possible( 2107 data, pumps_energyconsumption, self.__pump_energyconsumption_idx, 2108 len(self.__pump_energyconsumption_sensors) != 0, 2109 "Pump energy consumption" 2110 ) 2111 else: 2112 raise ValueError( 2113 f"Unknown sensor type '{sensor_type}'. " 2114 f"Valid sensor types are\n{valid_sensor_types()}" 2115 ) 2116 return np.concatenate(data, axis=1)
2117
[docs] 2118 def get_index_of_reading(self, pressure_sensor: str = None, flow_sensor: str = None, 2119 demand_sensor: str = None, node_quality_sensor: str = None, 2120 link_quality_sensor: str = None, valve_state_sensor: str = None, 2121 pump_state_sensor: str = None, pump_efficiency_sensor: str = None, 2122 pump_energyconsumption_sensor: str = None, 2123 tank_volume_sensor: str = None, 2124 bulk_species_node_sensor: tuple[str, str] = None, 2125 bulk_species_link_sensor: tuple[str, str] = None, 2126 surface_species_sensor: tuple[str, str] = None) -> int: 2127 """ 2128 Gets the index of a particular sensor in the final sensor readings array. 2129 2130 Note that only one sensor ID is converted to an index. In case of multiple sensor IDs, 2131 call this function for each sensor ID separately. 2132 2133 .. note:: 2134 2135 This function only returns the correct results if the sensor configuraton is NOT frozen! 2136 2137 Parameters 2138 ---------- 2139 pressure_sensor : `str` 2140 ID of the pressure sensor. 2141 flow_sensor : `str` 2142 ID of the flow sensor. 2143 demand_sensor : `str` 2144 ID of the demand sensor. 2145 node_quality_sensor : `str` 2146 ID of the quality sensor (at a node). 2147 link_quality_sensor : `str` 2148 ID of the quality sensor (at a link/pipe). 2149 valve_state_sensor : `str` 2150 ID of the state sensor (at a valve). 2151 pump_state_sensor : `str` 2152 ID of the state sensor (at a pump). 2153 pump_efficiency_sensor : `str` 2154 ID of the efficiency sensor (at a pump). 2155 pump_energyconsumption_sensor : `str` 2156 ID of the energy consumption sensor (at a pump). 2157 tank_volume_sensor : `str` 2158 ID of the water volume sensor (at a tank) 2159 bulk_species_node_sensor : `tuple[str, str]` 2160 Tuple of bulk species ID and sensor node ID. 2161 bulk_species_link_sensor : `tuple[str, str]` 2162 Tuple of bulk species ID and sensor link/pipe ID. 2163 surface_species_sensor : `tuple[str, str]` 2164 Tuple of surface species ID and sensor link/pipe ID. 2165 """ 2166 if pressure_sensor is not None: 2167 return self.__sensors_id_to_idx["pressure"][pressure_sensor] 2168 elif flow_sensor is not None: 2169 return self.__sensors_id_to_idx["flow"][flow_sensor] 2170 elif demand_sensor is not None: 2171 return self.__sensors_id_to_idx["demand"][demand_sensor] 2172 elif node_quality_sensor is not None: 2173 return self.__sensors_id_to_idx["quality_node"][node_quality_sensor] 2174 elif link_quality_sensor is not None: 2175 return self.__sensors_id_to_idx["quality_link"][link_quality_sensor] 2176 elif valve_state_sensor is not None: 2177 return self.__sensors_id_to_idx["valve_state"][valve_state_sensor] 2178 elif pump_state_sensor is not None: 2179 return self.__sensors_id_to_idx["pump_state"][pump_state_sensor] 2180 elif pump_efficiency_sensor is not None: 2181 return self.__sensors_id_to_idx["pump_efficiency"][pump_efficiency_sensor] 2182 elif pump_energyconsumption_sensor is not None: 2183 return self.__sensors_id_to_idx["pump_energyconsumption"][pump_energyconsumption_sensor] 2184 elif tank_volume_sensor is not None: 2185 return self.__sensors_id_to_idx["tank_volume"][tank_volume_sensor] 2186 elif surface_species_sensor is not None: 2187 species_id, sensor_id = surface_species_sensor 2188 return self.__sensors_id_to_idx["surface_species"][species_id][sensor_id] 2189 elif bulk_species_node_sensor is not None: 2190 species_id, sensor_id = bulk_species_node_sensor 2191 return self.__sensors_id_to_idx["bulk_species_node"][species_id][sensor_id] 2192 elif bulk_species_link_sensor is not None: 2193 species_id, sensor_id = bulk_species_link_sensor 2194 return self.__sensors_id_to_idx["bulk_species_link"][species_id][sensor_id] 2195 else: 2196 raise ValueError("No sensor given")