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
[docs]
853 def map_link_id_to_idx(self, link_id: str) -> int:
854 """
855 Gets the index of a given link ID.
856
857 Parameters
858 ----------
859 link_id : `str`
860 Link ID.
861
862 Returns
863 -------
864 `int`
865 Index of the given link.
866 """
867 if self.__node_id_to_idx is not None:
868 return self.__link_id_to_idx[link_id]
869 else:
870 return self.__links.index(link_id)
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")