epyt-flow 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. epyt_flow/EPANET/EPANET/SRC_engines/AUTHORS +28 -0
  2. epyt_flow/EPANET/EPANET/SRC_engines/LICENSE +21 -0
  3. epyt_flow/EPANET/EPANET/SRC_engines/Readme_SRC_Engines.txt +18 -0
  4. epyt_flow/EPANET/EPANET/SRC_engines/enumstxt.h +134 -0
  5. epyt_flow/EPANET/EPANET/SRC_engines/epanet.c +5578 -0
  6. epyt_flow/EPANET/EPANET/SRC_engines/epanet2.c +865 -0
  7. epyt_flow/EPANET/EPANET/SRC_engines/epanet2.def +131 -0
  8. epyt_flow/EPANET/EPANET/SRC_engines/errors.dat +73 -0
  9. epyt_flow/EPANET/EPANET/SRC_engines/funcs.h +193 -0
  10. epyt_flow/EPANET/EPANET/SRC_engines/genmmd.c +1000 -0
  11. epyt_flow/EPANET/EPANET/SRC_engines/hash.c +177 -0
  12. epyt_flow/EPANET/EPANET/SRC_engines/hash.h +28 -0
  13. epyt_flow/EPANET/EPANET/SRC_engines/hydcoeffs.c +1151 -0
  14. epyt_flow/EPANET/EPANET/SRC_engines/hydraul.c +1117 -0
  15. epyt_flow/EPANET/EPANET/SRC_engines/hydsolver.c +720 -0
  16. epyt_flow/EPANET/EPANET/SRC_engines/hydstatus.c +476 -0
  17. epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2.h +431 -0
  18. epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_2.h +1786 -0
  19. epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_enums.h +468 -0
  20. epyt_flow/EPANET/EPANET/SRC_engines/inpfile.c +810 -0
  21. epyt_flow/EPANET/EPANET/SRC_engines/input1.c +707 -0
  22. epyt_flow/EPANET/EPANET/SRC_engines/input2.c +864 -0
  23. epyt_flow/EPANET/EPANET/SRC_engines/input3.c +2170 -0
  24. epyt_flow/EPANET/EPANET/SRC_engines/main.c +93 -0
  25. epyt_flow/EPANET/EPANET/SRC_engines/mempool.c +142 -0
  26. epyt_flow/EPANET/EPANET/SRC_engines/mempool.h +24 -0
  27. epyt_flow/EPANET/EPANET/SRC_engines/output.c +852 -0
  28. epyt_flow/EPANET/EPANET/SRC_engines/project.c +1359 -0
  29. epyt_flow/EPANET/EPANET/SRC_engines/quality.c +685 -0
  30. epyt_flow/EPANET/EPANET/SRC_engines/qualreact.c +743 -0
  31. epyt_flow/EPANET/EPANET/SRC_engines/qualroute.c +694 -0
  32. epyt_flow/EPANET/EPANET/SRC_engines/report.c +1489 -0
  33. epyt_flow/EPANET/EPANET/SRC_engines/rules.c +1362 -0
  34. epyt_flow/EPANET/EPANET/SRC_engines/smatrix.c +871 -0
  35. epyt_flow/EPANET/EPANET/SRC_engines/text.h +497 -0
  36. epyt_flow/EPANET/EPANET/SRC_engines/types.h +874 -0
  37. epyt_flow/EPANET/EPANET-MSX/MSX_Updates.txt +53 -0
  38. epyt_flow/EPANET/EPANET-MSX/Src/dispersion.h +27 -0
  39. epyt_flow/EPANET/EPANET-MSX/Src/hash.c +107 -0
  40. epyt_flow/EPANET/EPANET-MSX/Src/hash.h +28 -0
  41. epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx.h +102 -0
  42. epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx_export.h +42 -0
  43. epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.c +937 -0
  44. epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.h +39 -0
  45. epyt_flow/EPANET/EPANET-MSX/Src/mempool.c +204 -0
  46. epyt_flow/EPANET/EPANET-MSX/Src/mempool.h +24 -0
  47. epyt_flow/EPANET/EPANET-MSX/Src/msxchem.c +1285 -0
  48. epyt_flow/EPANET/EPANET-MSX/Src/msxcompiler.c +368 -0
  49. epyt_flow/EPANET/EPANET-MSX/Src/msxdict.h +42 -0
  50. epyt_flow/EPANET/EPANET-MSX/Src/msxdispersion.c +586 -0
  51. epyt_flow/EPANET/EPANET-MSX/Src/msxerr.c +116 -0
  52. epyt_flow/EPANET/EPANET-MSX/Src/msxfile.c +260 -0
  53. epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.c +175 -0
  54. epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.h +35 -0
  55. epyt_flow/EPANET/EPANET-MSX/Src/msxinp.c +1504 -0
  56. epyt_flow/EPANET/EPANET-MSX/Src/msxout.c +401 -0
  57. epyt_flow/EPANET/EPANET-MSX/Src/msxproj.c +791 -0
  58. epyt_flow/EPANET/EPANET-MSX/Src/msxqual.c +2010 -0
  59. epyt_flow/EPANET/EPANET-MSX/Src/msxrpt.c +400 -0
  60. epyt_flow/EPANET/EPANET-MSX/Src/msxtank.c +422 -0
  61. epyt_flow/EPANET/EPANET-MSX/Src/msxtoolkit.c +1164 -0
  62. epyt_flow/EPANET/EPANET-MSX/Src/msxtypes.h +551 -0
  63. epyt_flow/EPANET/EPANET-MSX/Src/msxutils.c +524 -0
  64. epyt_flow/EPANET/EPANET-MSX/Src/msxutils.h +56 -0
  65. epyt_flow/EPANET/EPANET-MSX/Src/newton.c +158 -0
  66. epyt_flow/EPANET/EPANET-MSX/Src/newton.h +34 -0
  67. epyt_flow/EPANET/EPANET-MSX/Src/rk5.c +287 -0
  68. epyt_flow/EPANET/EPANET-MSX/Src/rk5.h +39 -0
  69. epyt_flow/EPANET/EPANET-MSX/Src/ros2.c +293 -0
  70. epyt_flow/EPANET/EPANET-MSX/Src/ros2.h +35 -0
  71. epyt_flow/EPANET/EPANET-MSX/Src/smatrix.c +816 -0
  72. epyt_flow/EPANET/EPANET-MSX/Src/smatrix.h +29 -0
  73. epyt_flow/EPANET/EPANET-MSX/readme.txt +14 -0
  74. epyt_flow/EPANET/compile.sh +4 -0
  75. epyt_flow/VERSION +1 -0
  76. epyt_flow/__init__.py +24 -0
  77. epyt_flow/data/__init__.py +0 -0
  78. epyt_flow/data/benchmarks/__init__.py +11 -0
  79. epyt_flow/data/benchmarks/batadal.py +257 -0
  80. epyt_flow/data/benchmarks/batadal_data.py +28 -0
  81. epyt_flow/data/benchmarks/battledim.py +473 -0
  82. epyt_flow/data/benchmarks/battledim_data.py +51 -0
  83. epyt_flow/data/benchmarks/gecco_water_quality.py +267 -0
  84. epyt_flow/data/benchmarks/leakdb.py +592 -0
  85. epyt_flow/data/benchmarks/leakdb_data.py +18923 -0
  86. epyt_flow/data/benchmarks/water_usage.py +123 -0
  87. epyt_flow/data/networks.py +650 -0
  88. epyt_flow/gym/__init__.py +4 -0
  89. epyt_flow/gym/control_gyms.py +47 -0
  90. epyt_flow/gym/scenario_control_env.py +101 -0
  91. epyt_flow/metrics.py +404 -0
  92. epyt_flow/models/__init__.py +2 -0
  93. epyt_flow/models/event_detector.py +31 -0
  94. epyt_flow/models/sensor_interpolation_detector.py +118 -0
  95. epyt_flow/rest_api/__init__.py +4 -0
  96. epyt_flow/rest_api/base_handler.py +70 -0
  97. epyt_flow/rest_api/res_manager.py +95 -0
  98. epyt_flow/rest_api/scada_data_handler.py +476 -0
  99. epyt_flow/rest_api/scenario_handler.py +352 -0
  100. epyt_flow/rest_api/server.py +106 -0
  101. epyt_flow/serialization.py +438 -0
  102. epyt_flow/simulation/__init__.py +5 -0
  103. epyt_flow/simulation/events/__init__.py +6 -0
  104. epyt_flow/simulation/events/actuator_events.py +259 -0
  105. epyt_flow/simulation/events/event.py +81 -0
  106. epyt_flow/simulation/events/leakages.py +404 -0
  107. epyt_flow/simulation/events/sensor_faults.py +267 -0
  108. epyt_flow/simulation/events/sensor_reading_attack.py +185 -0
  109. epyt_flow/simulation/events/sensor_reading_event.py +170 -0
  110. epyt_flow/simulation/events/system_event.py +88 -0
  111. epyt_flow/simulation/parallel_simulation.py +147 -0
  112. epyt_flow/simulation/scada/__init__.py +3 -0
  113. epyt_flow/simulation/scada/advanced_control.py +134 -0
  114. epyt_flow/simulation/scada/scada_data.py +1589 -0
  115. epyt_flow/simulation/scada/scada_data_export.py +255 -0
  116. epyt_flow/simulation/scenario_config.py +608 -0
  117. epyt_flow/simulation/scenario_simulator.py +1897 -0
  118. epyt_flow/simulation/scenario_visualizer.py +61 -0
  119. epyt_flow/simulation/sensor_config.py +1289 -0
  120. epyt_flow/topology.py +290 -0
  121. epyt_flow/uncertainty/__init__.py +3 -0
  122. epyt_flow/uncertainty/model_uncertainty.py +302 -0
  123. epyt_flow/uncertainty/sensor_noise.py +73 -0
  124. epyt_flow/uncertainty/uncertainties.py +555 -0
  125. epyt_flow/uncertainty/utils.py +206 -0
  126. epyt_flow/utils.py +306 -0
  127. epyt_flow-0.1.0.dist-info/LICENSE +21 -0
  128. epyt_flow-0.1.0.dist-info/METADATA +139 -0
  129. epyt_flow-0.1.0.dist-info/RECORD +131 -0
  130. epyt_flow-0.1.0.dist-info/WHEEL +5 -0
  131. epyt_flow-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1289 @@
1
+ """
2
+ Module provides a class for implementing sensor configurations.
3
+ """
4
+ from copy import deepcopy
5
+ import numpy as np
6
+ import epyt
7
+
8
+ from ..serialization import SENSOR_CONFIG_ID, JsonSerializable, serializable
9
+
10
+
11
+ SENSOR_TYPE_NODE_PRESSURE = 1
12
+ SENSOR_TYPE_NODE_QUALITY = 2
13
+ SENSOR_TYPE_NODE_DEMAND = 3
14
+ SENSOR_TYPE_LINK_FLOW = 4
15
+ SENSOR_TYPE_LINK_QUALITY = 5
16
+ SENSOR_TYPE_VALVE_STATE = 6
17
+ SENSOR_TYPE_PUMP_STATE = 7
18
+ SENSOR_TYPE_TANK_VOLUME = 8
19
+ SENSOR_TYPE_NODE_BULK_SPECIES = 9
20
+ SENSOR_TYPE_LINK_BULK_SPECIES = 10
21
+ SENSOR_TYPE_SURFACE_SPECIES = 11
22
+
23
+
24
+ @serializable(SENSOR_CONFIG_ID, ".epytflow_sensor_config")
25
+ class SensorConfig(JsonSerializable):
26
+ """
27
+ Class for storing a sensor configuration.
28
+
29
+ Parameters
30
+ ----------
31
+ nodes : `list[str]`
32
+ List of all nodes (i.e. IDs) in the network.
33
+ links : `list[str]`
34
+ List of all links/pipes (i.e. IDs) in the network.
35
+ valves : `list[str]`
36
+ List of all valves (i.e. IDs) in the network.
37
+ pumps : `list[str]`
38
+ List of all pumps (i.e. IDs) in the network.
39
+ tanks : `list[str]`
40
+ List of all tanks (i.e. IDs) in the network.
41
+ species : `list[str]`
42
+ List of all (EPANET-MSX) species (i.e. IDs) in the network
43
+ pressure_sensors : `list[str]`, optional
44
+ List of all nodes (i.e. IDs) at which a pressure sensor is placed.
45
+
46
+ The default is an empty list.
47
+ flow_sensors : `list[str]`, optional
48
+ List of all links/pipes (i.e. IDs) at which a flow sensor is placed.
49
+
50
+ The default is an empty list.
51
+ demand_sensors : `list[str]`, optional
52
+ List of all nodes (i.e. IDs) at which a demand sensor is placed.
53
+
54
+ The default is an empty list.
55
+ quality_node_sensors : `list[str]`, optional
56
+ List of all nodes (i.e. IDs) at which a quality sensor is placed.
57
+
58
+ The default is an empty list.
59
+ quality_link_sensors : `list[str]`, optional
60
+ List of all links/pipes (i.e. IDs) at which a flow sensor is placed.
61
+
62
+ The default is an empty list.
63
+ valve_state_sensors : `list[str]`, optional
64
+ List of all valves (i.e. IDs) at which a valve state sensor is placed.
65
+
66
+ The default is an empty list.
67
+ pump_state_sensors : `list[str]`, optional
68
+ List of all pumps (i.e. IDs) at which a pump state sensor is placed.
69
+
70
+ The default is an empty list.
71
+ tank_volume_sensors : `list[str]`, optional
72
+ List of all tanks (i.e. IDs) at which a tank volume sensor is placed.
73
+
74
+ The default is an empty list.
75
+ bulk_species_node_sensors : `dict`, optional
76
+ Bulk species node sensors as a dictionary -- i.e. bulk species ID are the keys,
77
+ and the sensor locations (node IDs) are the values.
78
+
79
+ The default is an empty list.
80
+ bulk_species_link_sensors : `dict`, optional
81
+ Bulk species link/pipe sensors as a dictionary -- i.e. bulk species ID are the keys,
82
+ and the sensor locations (link/pipe IDs) are the values.
83
+
84
+ The default is an empty list.
85
+ surface_species_sensors : `dict`, optional
86
+ Surface species sensors as a dictionary -- i.e. surface species ID are the keys,
87
+ and the sensor locations (link/pipe IDs) are the values.
88
+
89
+ The default is an empty list.
90
+ node_id_to_idx : `dict`, optional
91
+ Mapping of a node ID to the EPANET index (i.e. position in the raw sensor reading data).
92
+
93
+ If None is given, it is assumed that the nodes (in 'nodes') are
94
+ sorted according to their EPANET index.
95
+
96
+ The default is None.
97
+ link_id_to_idx : `dict`, optional
98
+ Mapping of a link/pipe ID to the EPANET index
99
+ (i.e. position in the raw sensor reading data).
100
+
101
+ If None is given, it is assumed that the links/pipes (in 'links') are
102
+ sorted according to their EPANET index..
103
+
104
+ The default is None.
105
+ valve_id_to_idx : `dict`, optional
106
+ Mapping of a valve ID to the EPANET index (i.e. position in the raw sensor reading data).
107
+
108
+ If None is given, it is assumed that the valves (in 'valves') are
109
+ sorted according to their EPANET index.
110
+
111
+ The default is None.
112
+ pump_id_to_idx : `dict`, optional
113
+ Mapping of a pump ID to the EPANET index (i.e. position in the raw sensor reading data).
114
+
115
+ If None is given, it is assumed that the pumps (in 'pumps') are
116
+ sorted according to their EPANET index.
117
+
118
+ The default is None.
119
+ tank_id_to_idx : `dict`, optional
120
+ Mapping of a tank ID to the EPANET index (i.e. position in the raw sensor reading data).
121
+
122
+ If None is given, it is assumed that the tanks (in 'tanks') are
123
+ sorted according to their EPANET index.
124
+
125
+ The default is None.
126
+ bulkspecies_id_to_idx : `dict`, optional
127
+ Mapping of a surface species ID to the EPANET index
128
+ (i.e. position in the raw sensor reading data).
129
+
130
+ If None is given, it is assumed that the surface species (in 'surface_species') are
131
+ sorted according to their EPANET index.
132
+
133
+ The default is None.
134
+ """
135
+ def __init__(self, nodes: list[str], links: list[str], valves: list[str], pumps: list[str],
136
+ tanks: list[str], bulk_species: list[str], surface_species: list[str],
137
+ pressure_sensors: list[str] = [],
138
+ flow_sensors: list[str] = [],
139
+ demand_sensors: list[str] = [],
140
+ quality_node_sensors: list[str] = [],
141
+ quality_link_sensors: list[str] = [],
142
+ valve_state_sensors: list[str] = [],
143
+ pump_state_sensors: list[str] = [],
144
+ tank_volume_sensors: list[str] = [],
145
+ bulk_species_node_sensors: dict = {},
146
+ bulk_species_link_sensors: dict = {},
147
+ surface_species_sensors: dict = {},
148
+ node_id_to_idx: dict = None, link_id_to_idx: dict = None,
149
+ valve_id_to_idx: dict = None, pump_id_to_idx: dict = None,
150
+ tank_id_to_idx: dict = None, bulkspecies_id_to_idx: dict = None,
151
+ surfacespecies_id_to_idx: dict = None, **kwds):
152
+ if not isinstance(nodes, list):
153
+ raise TypeError("'nodes' must be an instance of 'list[str]' " +
154
+ f"but not of '{type(nodes)}'")
155
+ if len(nodes) == 0:
156
+ raise ValueError("'nodes' must be a list of all nodes (i.e. IDs) in the network.")
157
+ if any(not isinstance(n, str) for n in nodes):
158
+ raise TypeError("Each item in 'nodes' must be an instance of 'str' -- " +
159
+ "ID of a node in the network.")
160
+
161
+ if not isinstance(links, list):
162
+ raise TypeError("'links' must be an instance of 'list[str]' " +
163
+ f"but not of '{type(links)}'")
164
+ if len(links) == 0:
165
+ raise ValueError("'links' must be a list of all links/pipes (i.e. IDs) in the network.")
166
+ if any(not isinstance(link, str) for link in links):
167
+ raise TypeError("Each item in 'links' must be an instance of 'str' -- " +
168
+ "ID of a link/pipe in the network.")
169
+
170
+ if not isinstance(valves, list):
171
+ raise TypeError("'valves' must be an instance of 'list[str]' " +
172
+ f"but not of '{type(valves)}'")
173
+ if any(v not in links for v in valves):
174
+ raise ValueError("Each item in 'valves' must be in 'links'")
175
+
176
+ if not isinstance(pumps, list):
177
+ raise TypeError("'pumps' must be an instance of 'list[str]' " +
178
+ f"but not of '{type(pumps)}'")
179
+ if any(p not in links for p in pumps):
180
+ raise ValueError("Each item in 'pumps' must be in 'links'")
181
+
182
+ if not isinstance(tanks, list):
183
+ raise TypeError("'tanks' must be an instance of 'list[str]' " +
184
+ f"but not of '{type(tanks)}'")
185
+ if any(v not in nodes for v in tanks):
186
+ raise ValueError("Each item in 'tanks' must be in 'nodes'")
187
+
188
+ if not isinstance(bulk_species, list):
189
+ raise TypeError("'bulk_species' must be an instance of 'list[str]' " +
190
+ f"but not of '{type(bulk_species)}'")
191
+ if any(not isinstance(bulk_species_id, str) for bulk_species_id in bulk_species):
192
+ raise TypeError("Each item in 'bulk_species' must be an instance of 'str'")
193
+
194
+ if not isinstance(surface_species, list):
195
+ raise TypeError("'surface_species' must be an instance of 'list[str]' " +
196
+ f"but not of '{type(surface_species)}'")
197
+ if any(not isinstance(surface_species_id, str) for surface_species_id in surface_species):
198
+ raise TypeError("Each item in 'surface_species' must be an instance of 'str'")
199
+
200
+ if not isinstance(pressure_sensors, list):
201
+ raise TypeError("'pressure_sensors' must be an instance of 'list[str]' " +
202
+ f"but not of '{type(pressure_sensors)}'")
203
+ if any(n not in nodes for n in pressure_sensors):
204
+ raise ValueError("Each item in 'pressure_sensors' must be in 'nodes' -- " +
205
+ "cannot place a sensor at a non-existing node.")
206
+
207
+ if not isinstance(flow_sensors, list):
208
+ raise TypeError("'flow_sensors' must be an instance of 'list[str]' " +
209
+ f"but not of '{type(flow_sensors)}'")
210
+ if any(link not in links for link in flow_sensors):
211
+ raise ValueError("Each item in 'flow_sensors' must be in 'links' -- cannot " +
212
+ "place a sensor at a non-existing link/pipe.")
213
+
214
+ if not isinstance(demand_sensors, list):
215
+ raise TypeError("'demand_sensors' must be an instance of 'list[str]' " +
216
+ f"but not of '{type(demand_sensors)}'")
217
+ if any(n not in nodes for n in demand_sensors):
218
+ raise ValueError("Each item in 'demand_sensors' must be in 'nodes' -- cannot " +
219
+ "place a sensor at a non-existing node.")
220
+
221
+ if not isinstance(quality_node_sensors, list):
222
+ raise TypeError("'quality_node_sensors' must be an instance of 'list[str]' " +
223
+ f"but not of '{type(quality_node_sensors)}'")
224
+ if any(n not in nodes for n in quality_node_sensors):
225
+ raise ValueError("Each item in 'quality_node_sensors' must be in 'nodes' -- cannot " +
226
+ "place a sensor at a non-existing node.")
227
+
228
+ if not isinstance(quality_link_sensors, list):
229
+ raise TypeError("'quality_link_sensors' must be an instance of 'list[str]' " +
230
+ f"but not of '{type(quality_link_sensors)}'")
231
+ if any(link not in links for link in quality_link_sensors):
232
+ raise ValueError("Each item in 'quality_link_sensors' must be in 'links' -- cannot " +
233
+ "place a sensor at a non-existing link/pipe.")
234
+
235
+ if not isinstance(valve_state_sensors, list):
236
+ raise TypeError("'valve_state_sensors' must be an instance of 'list[str]' " +
237
+ f"but not of '{type(valve_state_sensors)}'")
238
+ if any(link not in valves for link in valve_state_sensors):
239
+ raise ValueError("Each item in 'valve_state_sensors' must be in 'valves' -- cannot " +
240
+ "place a sensor at a non-existing valve.")
241
+
242
+ if not isinstance(pump_state_sensors, list):
243
+ raise TypeError("'pump_state_sensors' must be an instance of 'list[str]' " +
244
+ f"but not of '{type(pump_state_sensors)}'")
245
+ if any(link not in pumps for link in pump_state_sensors):
246
+ raise ValueError("Each item in 'pump_state_sensors' must be in 'pumps' -- cannot " +
247
+ "place a sensor at a non-existing pump.")
248
+
249
+ if not isinstance(tank_volume_sensors, list):
250
+ raise TypeError("'tank_volume_sensors' must be an instance of 'list[str]' " +
251
+ f"but not of '{type(tank_volume_sensors)}'")
252
+ if any(n not in tanks for n in tank_volume_sensors):
253
+ raise ValueError("Each item in 'tank_volume_sensors' must be in 'tanks' -- cannot " +
254
+ "place a sensor at a non-existing tanks.")
255
+
256
+ if not isinstance(bulk_species_node_sensors, dict):
257
+ raise TypeError("'bulk_species_node_sensors' must be an instance of 'dict' but not " +
258
+ f"of '{type(bulk_species_node_sensors)}'")
259
+ if any(bulk_species_id not in bulk_species
260
+ for bulk_species_id in bulk_species_node_sensors.keys()):
261
+ raise ValueError("Unknown bulk species ID in 'bulk_species_node_sensors'")
262
+ if any(node_id not in nodes for node_id in bulk_species_node_sensors.values()):
263
+ raise ValueError("Unknown node ID in 'bulk_species_node_sensors'")
264
+
265
+ if not isinstance(bulk_species_link_sensors, dict):
266
+ raise TypeError("'bulk_species_link_sensors' must be an instance of 'dict' but not " +
267
+ f"of '{type(bulk_species_link_sensors)}'")
268
+ if any(bulk_species_id not in bulk_species
269
+ for bulk_species_id in bulk_species_link_sensors.keys()):
270
+ raise ValueError("Unknown bulk species ID in 'bulk_species_link_sensors'")
271
+ if any(link_id not in links for link_id in bulk_species_link_sensors.values()):
272
+ raise ValueError("Unknown link/pipe ID in 'bulk_species_link_sensors'")
273
+
274
+ if not isinstance(surface_species_sensors, dict):
275
+ raise TypeError("'surface_species_sensors' must be an instance of 'dict' but not " +
276
+ f"of '{type(surface_species_sensors)}'")
277
+ if any(surface_species_id not in surface_species_sensors
278
+ for surface_species_id in surface_species_sensors.keys()):
279
+ raise ValueError("Unknown surface species ID in 'surface_species_sensors'")
280
+ if any(link_id not in links for link_id in surface_species_sensors.values()):
281
+ raise ValueError("Unknown link ID in 'surface_species_sensors'")
282
+
283
+ if node_id_to_idx is not None:
284
+ if not isinstance(node_id_to_idx, dict):
285
+ raise TypeError("'node_id_to_idx' must be an instance of 'dict' " +
286
+ f"but not of '{type(node_id_to_idx)}'")
287
+ if any(n not in nodes for n in node_id_to_idx.keys()):
288
+ raise ValueError("Unknown node ID in 'node_id_to_idx'")
289
+
290
+ if link_id_to_idx is not None:
291
+ if not isinstance(link_id_to_idx, dict):
292
+ raise TypeError("'link_id_to_idx' must be an instance of 'dict' " +
293
+ f"but not of '{type(link_id_to_idx)}'")
294
+ if any(link_id not in links for link_id in link_id_to_idx.keys()):
295
+ raise ValueError("Unknown link/pipe ID in 'link_id_to_idx'")
296
+
297
+ if valve_id_to_idx is not None:
298
+ if not isinstance(valve_id_to_idx, dict):
299
+ raise TypeError("'valve_id_to_idx' must be an instance of 'dict' " +
300
+ f"but not of '{type(valve_id_to_idx)}'")
301
+ if any(v not in valves for v in valve_id_to_idx.keys()):
302
+ raise ValueError("Unknown valve ID in 'valve_id_to_idx'")
303
+
304
+ if pump_id_to_idx is not None:
305
+ if not isinstance(pump_id_to_idx, dict):
306
+ raise TypeError("'pump_id_to_idx' must be an instance of 'dict' " +
307
+ f"but not of '{type(pump_id_to_idx)}'")
308
+ if any(p not in valves for p in pump_id_to_idx.keys()):
309
+ raise ValueError("Unknown pump ID in 'pump_id_to_idx'")
310
+
311
+ if tank_id_to_idx is not None:
312
+ if not isinstance(tank_id_to_idx, dict):
313
+ raise TypeError("'tank_id_to_idx' must be an instance of 'dict' " +
314
+ f"but not of '{type(tank_id_to_idx)}'")
315
+ if any(t not in tanks for t in tank_id_to_idx.keys()):
316
+ raise ValueError("Unknown tank ID in 'tank_id_to_idx'")
317
+
318
+ if bulkspecies_id_to_idx is not None:
319
+ if not isinstance(bulkspecies_id_to_idx, dict):
320
+ raise TypeError("'bulkspecies_id_to_idx' must be an instance of 'dict' " +
321
+ f"but not of '{type(bulkspecies_id_to_idx)}'")
322
+ if any(s not in bulk_species for s in bulkspecies_id_to_idx.keys()):
323
+ raise ValueError("Unknown bulk species ID in 'bulkspecies_id_to_idx'")
324
+
325
+ if surfacespecies_id_to_idx is not None:
326
+ if not isinstance(surfacespecies_id_to_idx, dict):
327
+ raise TypeError("'surfacespecies_id_to_idx' must be an instance of 'dict' " +
328
+ f"but not of '{type(surfacespecies_id_to_idx)}'")
329
+ if any(s not in surface_species for s in surfacespecies_id_to_idx.keys()):
330
+ raise ValueError("Unknown surface species ID in 'surfacespecies_id_to_idx'")
331
+
332
+ self.__nodes = nodes
333
+ self.__links = links
334
+ self.__valves = valves
335
+ self.__pumps = pumps
336
+ self.__tanks = tanks
337
+ self.__bulk_species = bulk_species
338
+ self.__surface_species = surface_species
339
+ self.__pressure_sensors = pressure_sensors
340
+ self.__flow_sensors = flow_sensors
341
+ self.__demand_sensors = demand_sensors
342
+ self.__quality_node_sensors = quality_node_sensors
343
+ self.__quality_link_sensors = quality_link_sensors
344
+ self.__valve_state_sensors = valve_state_sensors
345
+ self.__pump_state_sensors = pump_state_sensors
346
+ self.__tank_volume_sensors = tank_volume_sensors
347
+ self.__bulk_species_node_sensors = bulk_species_node_sensors
348
+ self.__bulk_species_link_sensors = bulk_species_link_sensors
349
+ self.__surface_species_sensors = surface_species_sensors
350
+ self.__node_id_to_idx = node_id_to_idx
351
+ self.__link_id_to_idx = link_id_to_idx
352
+ self.__valve_id_to_idx = valve_id_to_idx
353
+ self.__pump_id_to_idx = pump_id_to_idx
354
+ self.__tank_id_to_idx = tank_id_to_idx
355
+ self.__bulkspecies_id_to_idx = bulkspecies_id_to_idx
356
+ self.__surfacespecies_id_to_idx = surfacespecies_id_to_idx
357
+
358
+ self.__compute_indices() # Compute indices
359
+
360
+ super().__init__(**kwds)
361
+
362
+ def node_id_to_idx(self, node_id: str) -> int:
363
+ """
364
+ Gets the index of a given node ID.
365
+
366
+ Parameters
367
+ ----------
368
+ node_id : `str`
369
+ Node ID.
370
+
371
+ Returns
372
+ -------
373
+ `int`
374
+ Index of the given node.
375
+ """
376
+ if self.__node_id_to_idx is not None:
377
+ return self.__node_id_to_idx[node_id]
378
+ else:
379
+ return self.__nodes.index(node_id)
380
+
381
+ def link_id_to_idx(self, link_id: str) -> int:
382
+ """
383
+ Gets the index of a given link ID.
384
+
385
+ Parameters
386
+ ----------
387
+ link_id : `str`
388
+ Link ID.
389
+
390
+ Returns
391
+ -------
392
+ `int`
393
+ Index of the given link.
394
+ """
395
+ if self.__node_id_to_idx is not None:
396
+ return self.__link_id_to_idx[link_id]
397
+ else:
398
+ return self.__links.index(link_id)
399
+
400
+ def valve_id_to_idx(self, valve_id: str) -> int:
401
+ """
402
+ Gets the index of a given valve ID.
403
+
404
+ Parameters
405
+ ----------
406
+ valve_id : `str`
407
+ Valve ID.
408
+
409
+ Returns
410
+ -------
411
+ `int`
412
+ Index of the given valve.
413
+ """
414
+ if self.__valve_id_to_idx is not None:
415
+ return self.__valve_id_to_idx[valve_id]
416
+ else:
417
+ return self.__valves.index(valve_id)
418
+
419
+ def pump_id_to_idx(self, pump_id: str) -> int:
420
+ """
421
+ Gets the index of a given pump ID.
422
+
423
+ Parameters
424
+ ----------
425
+ pump_id : `str`
426
+ Pump ID.
427
+
428
+ Returns
429
+ -------
430
+ `int`
431
+ Index of the given pump.
432
+ """
433
+ if self.__pump_id_to_idx is not None:
434
+ return self.__pump_id_to_idx[pump_id]
435
+ else:
436
+ return self.__pumps.index(pump_id)
437
+
438
+ def tank_id_to_idx(self, tank_id: str) -> int:
439
+ """
440
+ Gets the index of a given tank ID.
441
+
442
+ Parameters
443
+ ----------
444
+ tank_id : `str`
445
+ Tank ID.
446
+
447
+ Returns
448
+ -------
449
+ `int`
450
+ Index of the given tank.
451
+ """
452
+ if self.__tank_id_to_idx is not None:
453
+ return self.__tank_id_to_idx[tank_id]
454
+ else:
455
+ return self.__tanks.index(tank_id)
456
+
457
+ def bulkspecies_id_to_idx(self, bulk_species_id: str) -> int:
458
+ """
459
+ Gets the index of a given bulk species ID.
460
+
461
+ Parameters
462
+ ----------
463
+ bulk_species_id : `str`
464
+ Bulk species ID.
465
+
466
+ Returns
467
+ -------
468
+ `int`
469
+ Index of the given bulk species.
470
+ """
471
+ if self.__bulkspecies_id_to_idx is not None:
472
+ return self.__bulkspecies_id_to_idx[bulk_species_id]
473
+ else:
474
+ return self.__bulk_species.index(bulk_species_id)
475
+
476
+ def surfacespecies_id_to_idx(self, surface_species_id: str) -> int:
477
+ """
478
+ Gets the index of a given surface species ID.
479
+
480
+ Parameters
481
+ ----------
482
+ surface_species_id : `str`
483
+ Surface species ID.
484
+
485
+ Returns
486
+ -------
487
+ `int`
488
+ Index of the given surface species.
489
+ """
490
+ if self.__surfacespecies_id_to_idx is not None:
491
+ return self.__surfacespecies_id_to_idx[surface_species_id]
492
+ else:
493
+ return self.__surface_species.index(surface_species_id)
494
+
495
+ def __compute_indices(self):
496
+ self.__pressure_idx = np.array([self.node_id_to_idx(n)
497
+ for n in self.__pressure_sensors], dtype=np.int32)
498
+ self.__flow_idx = np.array([self.link_id_to_idx(link)
499
+ for link in self.__flow_sensors], dtype=np.int32)
500
+ self.__demand_idx = np.array([self.node_id_to_idx(n)
501
+ for n in self.__demand_sensors], dtype=np.int32)
502
+ self.__quality_node_idx = np.array([self.node_id_to_idx(n)
503
+ for n in self.__quality_node_sensors], dtype=np.int32)
504
+ self.__quality_link_idx = np.array([self.link_id_to_idx(link)
505
+ for link in self.__quality_link_sensors],
506
+ dtype=np.int32)
507
+ self.__valve_state_idx = np.array([self.valve_id_to_idx(v)
508
+ for v in self.__valve_state_sensors], dtype=np.int32)
509
+ self.__pump_state_idx = np.array([self.pump_id_to_idx(p)
510
+ for p in self.__pump_state_sensors], dtype=np.int32)
511
+ self.__tank_volume_idx = np.array([self.tank_id_to_idx(t)
512
+ for t in self.__tank_volume_sensors], dtype=np.int32)
513
+ self.__bulk_species_node_idx = np.array([(self.bulkspecies_id_to_idx(s),
514
+ [self.node_id_to_idx(node_id)
515
+ for node_id in self.__bulk_species_node_sensors[s]])
516
+ for s in self.__bulk_species_node_sensors.keys()],
517
+ dtype=object)
518
+ self.__bulk_species_link_idx = np.array([(self.bulkspecies_id_to_idx(s),
519
+ [self.link_id_to_idx(link_id)
520
+ for link_id in self.__bulk_species_link_sensors[s]])
521
+ for s in self.__bulk_species_link_sensors.keys()],
522
+ dtype=object)
523
+ self.__surface_species_idx = np.array([(self.surfacespecies_id_to_idx(s),
524
+ [self.link_id_to_idx(link_id)
525
+ for link_id in self.__surface_species_sensors[s]])
526
+ for s in self.__surface_species_sensors.keys()],
527
+ dtype=object)
528
+
529
+ n_pressure_sensors = len(self.__pressure_sensors)
530
+ n_flow_sensors = len(self.__flow_sensors)
531
+ n_demand_sensors = len(self.__demand_sensors)
532
+ n_node_quality_sensors = len(self.__quality_node_sensors)
533
+ n_link_quality_sensors = len(self.__quality_link_sensors)
534
+ n_valve_state_sensors = len(self.__valve_state_sensors)
535
+ n_pump_state_sensors = len(self.__pump_state_sensors)
536
+ n_tank_volume_sensors = len(self.__tank_volume_sensors)
537
+ n_bulk_species_node_sensors = len(self.__bulk_species_node_sensors.values())
538
+ n_bulk_species_link_sensors = len(self.__bulk_species_link_sensors.values())
539
+
540
+ pressure_idx_shift = 0
541
+ flow_idx_shift = pressure_idx_shift + n_pressure_sensors
542
+ demand_idx_shift = flow_idx_shift + n_flow_sensors
543
+ node_quality_idx_shift = demand_idx_shift + n_demand_sensors
544
+ link_quality_idx_shift = node_quality_idx_shift + n_node_quality_sensors
545
+ valve_state_idx_shift = link_quality_idx_shift + n_link_quality_sensors
546
+ pump_state_idx_shift = valve_state_idx_shift + n_valve_state_sensors
547
+ tank_volume_idx_shift = pump_state_idx_shift + n_pump_state_sensors
548
+ bulk_species_node_idx_shift = tank_volume_idx_shift + n_tank_volume_sensors
549
+ bulk_species_link_idx_shift = bulk_species_node_idx_shift + n_bulk_species_node_sensors
550
+ surface_species_idx_shift = bulk_species_link_idx_shift + n_bulk_species_link_sensors
551
+
552
+ def __build_sensors_id_to_idx(sensors: list[str], initial_idx_shift: int) -> dict:
553
+ return {sensor_id: i + initial_idx_shift
554
+ for sensor_id, i in zip(sensors, range(len(sensors)))}
555
+
556
+ def __build_species_sensors_id_to_idx(species_sensors: dict, initial_idx_shift) -> dict:
557
+ r = {}
558
+
559
+ cur_idx_shift = initial_idx_shift
560
+ for species_id in species_sensors:
561
+ r[species_id] = {}
562
+ for sensor_id in species_sensors[species_id]:
563
+ r[species_id][sensor_id] = cur_idx_shift
564
+ cur_idx_shift += 1
565
+
566
+ return r
567
+
568
+ mapping = {"pressure": __build_sensors_id_to_idx(self.__pressure_sensors,
569
+ pressure_idx_shift),
570
+ "flow": __build_sensors_id_to_idx(self.__flow_sensors,flow_idx_shift),
571
+ "demand": __build_sensors_id_to_idx(self.__demand_sensors,demand_idx_shift),
572
+ "quality_node": __build_sensors_id_to_idx(self.__quality_node_sensors,
573
+ node_quality_idx_shift),
574
+ "quality_link": __build_sensors_id_to_idx(self.__quality_link_sensors,
575
+ link_quality_idx_shift),
576
+ "valve_state": __build_sensors_id_to_idx(self.__valve_state_sensors,
577
+ valve_state_idx_shift),
578
+ "pump_state": __build_sensors_id_to_idx(self.__pump_state_sensors,
579
+ pump_state_idx_shift),
580
+ "tank_volume": __build_sensors_id_to_idx(self.__tank_volume_sensors,
581
+ tank_volume_idx_shift),
582
+ "bulk_species_node":
583
+ __build_species_sensors_id_to_idx(self.__bulk_species_node_sensors,
584
+ bulk_species_node_idx_shift),
585
+ "bulk_species_link":
586
+ __build_species_sensors_id_to_idx(self.__bulk_species_link_sensors,
587
+ bulk_species_link_idx_shift),
588
+ "surface_species":
589
+ __build_species_sensors_id_to_idx(self.__surface_species_sensors,
590
+ surface_species_idx_shift)}
591
+ self.__sensors_id_to_idx = mapping
592
+
593
+ def validate(self, epanet_api: epyt.epanet) -> None:
594
+ """
595
+ Validates this sensor configuration --
596
+ i.e. checks whether all nodes, etc. exist in the .inp file.
597
+
598
+ Parameters
599
+ ----------
600
+ epanet_api : `epyt.epanet`
601
+ EPANET and EPANET-MSX API.
602
+ """
603
+ if not isinstance(epanet_api, epyt.epanet):
604
+ raise TypeError("'epanet_api' must be an instance of 'epyt.epanet' " +
605
+ f"but not of '{type(epanet_api)}'")
606
+
607
+ nodes = epanet_api.getNodeNameID()
608
+ links = epanet_api.getLinkNameID()
609
+ valves = epanet_api.getLinkValveNameID()
610
+ pumps = epanet_api.getLinkPumpNameID()
611
+ tanks = epanet_api.getNodeTankNameID()
612
+
613
+ bulk_species = []
614
+ surface_species = []
615
+ if hasattr(epanet_api, "msx"):
616
+ for species_id, species_type in zip(epanet_api.getMSXSpeciesNameID(),
617
+ epanet_api.getMSXSpeciesType()):
618
+ if species_type == "BULK":
619
+ bulk_species.append(species_id)
620
+ elif species_type == "WALL":
621
+ surface_species.append(species_id)
622
+
623
+ if any(node_id not in nodes for node_id in self.__nodes):
624
+ raise ValueError("Invalid node ID detected -- " +
625
+ "all given node IDs must exist in the .inp file")
626
+ if any(link_id not in links for link_id in self.__links):
627
+ raise ValueError("Invalid link/pipe ID detected -- all given link/pipe IDs " +
628
+ "must exist in the .inp file")
629
+ if any(valve_id not in valves for valve_id in self.__valves):
630
+ raise ValueError("Invalid valve ID detected -- all given valve IDs must exist " +
631
+ "in the .inp file")
632
+ if any(pump_id not in pumps for pump_id in self.__pumps):
633
+ raise ValueError("Invalid pump ID detected -- all given pump IDs must exist " +
634
+ "in the .inp file")
635
+ if any(tank_id not in tanks for tank_id in self.__tanks):
636
+ raise ValueError("Invalid tank ID detected -- all given tank IDs must exist " +
637
+ "in the .inp file")
638
+ if any(surface_species_id not in surface_species
639
+ for surface_species_id in self.__surface_species):
640
+ raise ValueError("Invalid surface species ID detected")
641
+ if any(bulk_species_id not in bulk_species for bulk_species_id in self.__bulk_species):
642
+ raise ValueError("Invalid bulk species ID detected")
643
+
644
+ @property
645
+ def nodes(self) -> list[str]:
646
+ """
647
+ Gets all node IDs.
648
+
649
+ Returns
650
+ -------
651
+ `list[str]`
652
+ All node IDs.
653
+ """
654
+ return self.__nodes.copy()
655
+
656
+ @property
657
+ def links(self) -> list[str]:
658
+ """
659
+ Gets all link IDs.
660
+
661
+ Returns
662
+ -------
663
+ `list[str]`
664
+ All link IDs.
665
+ """
666
+ return self.__links.copy()
667
+
668
+ @property
669
+ def valves(self) -> list[str]:
670
+ """
671
+ Gets all valve IDs (subset of link IDs).
672
+
673
+ Returns
674
+ -------
675
+ `list[str]`
676
+ All valve IDs.
677
+ """
678
+ return self.__valves.copy()
679
+
680
+ @property
681
+ def pumps(self) -> list[str]:
682
+ """
683
+ Gets all pump IDs (subset of link IDs).
684
+
685
+ Returns
686
+ -------
687
+ `list[str]`
688
+ All pump IDs.
689
+ """
690
+ return self.__pumps.copy()
691
+
692
+ @property
693
+ def tanks(self) -> list[str]:
694
+ """
695
+ Gets all tank IDs (subset of node IDs).
696
+
697
+ Returns
698
+ -------
699
+ `list[str]`
700
+ All tank IDs.
701
+ """
702
+ return self.__tanks.copy()
703
+
704
+ @property
705
+ def bulk_species(self) -> list[str]:
706
+ """
707
+ Gets all bulk species IDs -- i.e. species that live in the water.
708
+
709
+ Returns
710
+ -------
711
+ `list[str]`
712
+ All species IDs.
713
+ """
714
+ return self.__bulk_species.copy()
715
+
716
+ @property
717
+ def surface_species(self) -> list[str]:
718
+ """
719
+ Gets all surface species IDs -- i.e. species that live links/pipes.
720
+
721
+ Returns
722
+ -------
723
+ `list[str]`
724
+ All species IDs.
725
+ """
726
+ return self.__surface_species.copy()
727
+
728
+ @property
729
+ def pressure_sensors(self) -> list[str]:
730
+ """
731
+ Gets all pressure sensors (i.e. IDs of nodes at which a pressure sensor is placed).
732
+
733
+ Returns
734
+ -------
735
+ `list[str]`
736
+ All node IDs with a pressure sensor.
737
+ """
738
+ return self.__pressure_sensors.copy()
739
+
740
+ @pressure_sensors.setter
741
+ def pressure_sensors(self, pressure_sensors: list[str]) -> None:
742
+ if not isinstance(pressure_sensors, list):
743
+ raise TypeError("'pressure_sensors' must be an instance of 'list[str]' " +
744
+ f"but not of '{type(pressure_sensors)}'")
745
+ if any(n not in self.__nodes for n in pressure_sensors):
746
+ raise ValueError("Each item in 'pressure_sensors' must be in 'nodes' -- cannot " +
747
+ "place a sensor at a non-existing node.")
748
+
749
+ self.__pressure_sensors = pressure_sensors
750
+
751
+ self.__compute_indices()
752
+
753
+ @property
754
+ def flow_sensors(self) -> list[str]:
755
+ """
756
+ Gets all flow sensors (i.e. IDs of links at which a flow sensor is placed).
757
+
758
+ Returns
759
+ -------
760
+ `list[str]`
761
+ All link IDs with a flow sensor.
762
+ """
763
+ return self.__flow_sensors.copy()
764
+
765
+ @flow_sensors.setter
766
+ def flow_sensors(self, flow_sensors: list[str]) -> None:
767
+ if not isinstance(flow_sensors, list):
768
+ raise TypeError("'pressure_sensors' must be an instance of 'list[str]' " +
769
+ f"but not of '{type(flow_sensors)}'")
770
+ if any(link not in self.__links for link in flow_sensors):
771
+ raise ValueError("Each item in 'flow_sensors' must be in 'links' -- cannot " +
772
+ "place a sensor at a non-existing link/pipe.")
773
+
774
+ self.__flow_sensors = flow_sensors
775
+
776
+ self.__compute_indices()
777
+
778
+ @property
779
+ def demand_sensors(self) -> list[str]:
780
+ """
781
+ Gets all demand sensors (i.e. IDs of nodes at which a demand sensor is placed).
782
+
783
+ Returns
784
+ -------
785
+ `list[str]`
786
+ All node IDs with a demand sensor.
787
+ """
788
+ return self.__demand_sensors.copy()
789
+
790
+ @demand_sensors.setter
791
+ def demand_sensors(self, demand_sensors: list[str]) -> None:
792
+ if not isinstance(demand_sensors, list):
793
+ raise TypeError("'demand_sensors' must be an instance of 'list[str]' " +
794
+ f"but not of '{type(demand_sensors)}'")
795
+ if any(n not in self.__nodes for n in demand_sensors):
796
+ raise ValueError("Each item in 'demand_sensors' must be in 'nodes' -- cannot " +
797
+ "place a sensor at a non-existing node.")
798
+
799
+ self.__demand_sensors = demand_sensors
800
+
801
+ self.__compute_indices()
802
+
803
+ @property
804
+ def quality_node_sensors(self) -> list[str]:
805
+ """
806
+ Gets all node quality sensors (i.e. IDs of nodes at which a node quality sensor is placed).
807
+
808
+ Returns
809
+ -------
810
+ `list[str]`
811
+ All node IDs with a node quality sensor.
812
+ """
813
+ return self.__quality_node_sensors.copy()
814
+
815
+ @quality_node_sensors.setter
816
+ def quality_node_sensors(self, quality_node_sensors: list[str]) -> None:
817
+ if not isinstance(quality_node_sensors, list):
818
+ raise TypeError("'quality_node_sensors' must be an instance of 'list[str]' " +
819
+ f"but not of '{type(quality_node_sensors)}'")
820
+ if any(n not in self.__nodes for n in quality_node_sensors):
821
+ raise ValueError("Each item in 'quality_node_sensors' must be in 'nodes' -- cannot " +
822
+ "place a sensor at a non-existing node.")
823
+
824
+ self.__quality_node_sensors = quality_node_sensors
825
+
826
+ self.__compute_indices()
827
+
828
+ @property
829
+ def quality_link_sensors(self) -> list[str]:
830
+ """
831
+ Gets all link quality sensors (i.e. IDs of links at which a link quality sensor is placed).
832
+
833
+ Returns
834
+ -------
835
+ `list[str]`
836
+ All link IDs with a link quality sensor.
837
+ """
838
+ return self.__quality_link_sensors.copy()
839
+
840
+ @quality_link_sensors.setter
841
+ def quality_link_sensors(self, quality_link_sensors: list[str]) -> None:
842
+ if not isinstance(quality_link_sensors, list):
843
+ raise TypeError("'quality_link_sensors' must be an instance of 'list[str]' " +
844
+ f"but not of '{type(quality_link_sensors)}'")
845
+ if any(link not in self.__links for link in quality_link_sensors):
846
+ raise ValueError("Each item in 'quality_link_sensors' must be in 'links' -- cannot " +
847
+ "place a sensor at a non-existing link/pipe.")
848
+
849
+ self.__quality_link_sensors = quality_link_sensors
850
+
851
+ self.__compute_indices()
852
+
853
+ @property
854
+ def valve_state_sensors(self) -> list[str]:
855
+ """
856
+ Gets all valve state sensors (i.e. IDs of valves at which a valve state sensor is placed).
857
+
858
+ Returns
859
+ -------
860
+ `list[str]`
861
+ All valve IDs with a valve state sensor.
862
+ """
863
+ return self.__valve_state_sensors.copy()
864
+
865
+ @valve_state_sensors.setter
866
+ def valve_state_sensors(self, valve_state_sensors: list[str]) -> None:
867
+ if not isinstance(valve_state_sensors, list):
868
+ raise TypeError("'valve_state_sensors' must be an instance of 'list[str]' " +
869
+ f"but not of '{type(valve_state_sensors)}'")
870
+ if any(link not in self.__valves for link in valve_state_sensors):
871
+ raise ValueError("Each item in 'valve_state_sensors' must be in 'valves' -- cannot " +
872
+ "place a sensor at a non-existing valves.")
873
+
874
+ self.__valve_state_sensors = valve_state_sensors
875
+
876
+ self.__compute_indices()
877
+
878
+ @property
879
+ def pump_state_sensors(self) -> list[str]:
880
+ """
881
+ Gets all pump state sensors (i.e. IDs of pumps at which a pump state sensor is placed).
882
+
883
+ Returns
884
+ -------
885
+ `list[str]`
886
+ All link IDs with a pump state sensor.
887
+ """
888
+ return self.__pump_state_sensors.copy()
889
+
890
+ @pump_state_sensors.setter
891
+ def pump_state_sensors(self, pump_state_sensors: list[str]) -> None:
892
+ if not isinstance(pump_state_sensors, list):
893
+ raise TypeError("'pump_state_sensors' must be an instance of 'list[str]' " +
894
+ f"but not of '{type(pump_state_sensors)}'")
895
+ if any(link not in self.__pumps for link in pump_state_sensors):
896
+ raise ValueError("Each item in 'pump_state_sensors' must be in 'pumps' -- cannot " +
897
+ "place a sensor at a non-existing pump.")
898
+
899
+ self.__pump_state_sensors = pump_state_sensors
900
+
901
+ self.__compute_indices()
902
+
903
+ @property
904
+ def tank_volume_sensors(self) -> list[str]:
905
+ """
906
+ Gets all tank volume sensors (i.e. IDs of tanks at which a tank volume sensor is placed).
907
+
908
+ Returns
909
+ -------
910
+ `list[str]`
911
+ All tank IDs with a tank volume sensor.
912
+ """
913
+ return self.__tank_volume_sensors.copy()
914
+
915
+ @tank_volume_sensors.setter
916
+ def tank_volume_sensors(self, tank_volume_sensors: list[str]) -> None:
917
+ if not isinstance(tank_volume_sensors, list):
918
+ raise TypeError("'tank_volume_sensors' must be an instance of 'list[str]' " +
919
+ f"but not of '{type(tank_volume_sensors)}'")
920
+ if any(n not in self.__tanks for n in tank_volume_sensors):
921
+ raise ValueError("Each item in 'tank_volume_sensors' must be in 'tanks' -- cannot " +
922
+ "place a sensor at a non-existing tanks.")
923
+
924
+ self.__tank_volume_sensors = tank_volume_sensors
925
+
926
+ self.__compute_indices()
927
+
928
+ @property
929
+ def bulk_species_node_sensors(self) -> dict:
930
+ """
931
+ Gets all bulk species node sensors as a dictionary --
932
+ i.e. bulk species IDs as keys and node IDs as values.
933
+
934
+ Returns
935
+ -------
936
+ `dict`
937
+ Bulk species sensors -- keys: bulk species IDs, values: node IDs.
938
+ """
939
+ return deepcopy(self.__bulk_species_node_sensors)
940
+
941
+ @bulk_species_node_sensors.setter
942
+ def bulk_species_node_sensors(self, bulk_species_sensors: dict) -> None:
943
+ if not isinstance(bulk_species_sensors, dict):
944
+ raise TypeError("'bulk_species_sensors' must be an instance of 'dict' " +
945
+ f"but not of '{type(bulk_species_sensors)}'")
946
+ if any(species_id not in self.__bulk_species for species_id in bulk_species_sensors.keys()):
947
+ raise ValueError("Unknown bulk species ID in 'bulk_species_sensors'")
948
+ if any(node_id not in self.__nodes for node_id in sum(bulk_species_sensors.values(), [])):
949
+ raise ValueError("Unknown node ID in 'bulk_species_sensors'")
950
+
951
+ self.__bulk_species_node_sensors = bulk_species_sensors
952
+
953
+ self.__compute_indices()
954
+
955
+ @property
956
+ def bulk_species_link_sensors(self) -> dict:
957
+ """
958
+ Gets all bulk species link/pipe sensors as a dictionary --
959
+ i.e. bulk species IDs as keys and link/pipe IDs as values.
960
+
961
+ Returns
962
+ -------
963
+ `dict`
964
+ Bulk species sensors -- keys: bulk species IDs, values: link/pipe IDs.
965
+ """
966
+ return deepcopy(self.__bulk_species_link_sensors)
967
+
968
+ @bulk_species_link_sensors.setter
969
+ def bulk_species_link_sensors(self, bulk_species_sensors: dict) -> None:
970
+ if not isinstance(bulk_species_sensors, dict):
971
+ raise TypeError("'bulk_species_sensors' must be an instance of 'dict' " +
972
+ f"but not of '{type(bulk_species_sensors)}'")
973
+ if any(species_id not in self.__bulk_species for species_id in bulk_species_sensors.keys()):
974
+ raise ValueError("Unknown bulk species ID in 'bulk_species_sensors'")
975
+ if any(link_id not in self.__links for link_id in sum(bulk_species_sensors.values(), [])):
976
+ raise ValueError("Unknown link/pipe ID in 'bulk_species_sensors'")
977
+
978
+ self.__bulk_species_link_sensors = bulk_species_sensors
979
+
980
+ self.__compute_indices()
981
+
982
+ @property
983
+ def surface_species_sensors(self) -> dict:
984
+ """
985
+ Gets all surface species sensors as a dictionary --
986
+ i.e. surface species IDs as keys and link/pipe IDs as values.
987
+
988
+ Returns
989
+ -------
990
+ `dict`
991
+ Surface species sensors -- keys: surface species IDs, values: link/pipe IDs.
992
+ """
993
+ return deepcopy(self.__surface_species_sensors)
994
+
995
+ @surface_species_sensors.setter
996
+ def surface_species_sensors(self, surface_species_sensors: dict) -> None:
997
+ if not isinstance(surface_species_sensors, dict):
998
+ raise TypeError("'surface_species_sensors' must be an instance of 'dict' " +
999
+ f"but not of '{type(surface_species_sensors)}'")
1000
+ if any(species_id not in self.__surface_species
1001
+ for species_id in surface_species_sensors.keys()):
1002
+ raise ValueError("Unknown surface species ID in 'surface_species_sensors'")
1003
+ if any(link_id not in self.__links
1004
+ for link_id in sum(surface_species_sensors.values(), [])):
1005
+ raise ValueError("Unknown link/pipe ID in 'surface_species_sensors'")
1006
+
1007
+ self.__surface_species_sensors = surface_species_sensors
1008
+
1009
+ self.__compute_indices()
1010
+
1011
+ @property
1012
+ def sensors_id_to_idx(self) -> dict:
1013
+ """
1014
+ Gets a mapping of sensor IDs to indices in the final Numpy array returned by `get_data()`.
1015
+
1016
+ Returns
1017
+ -------
1018
+ `dict`
1019
+ Mapping of sensor IDs to indices in the final Numpy array.
1020
+ """
1021
+ return deepcopy(self.__sensors_id_to_idx)
1022
+
1023
+ def get_attributes(self) -> dict:
1024
+ attr = {"nodes": self.__nodes, "links": self.__links,
1025
+ "valves": self.__valves, "pumps": self.__pumps,
1026
+ "tanks": self.__tanks, "bulk_species": self.__bulk_species,
1027
+ "surface_species": self.__surface_species,
1028
+ "pressure_sensors": self.__pressure_sensors,
1029
+ "flow_sensors": self.__flow_sensors,
1030
+ "demand_sensors": self.__demand_sensors,
1031
+ "quality_node_sensors": self.__quality_node_sensors,
1032
+ "quality_link_sensors": self.__quality_link_sensors,
1033
+ "valve_state_sensors": self.__valve_state_sensors,
1034
+ "pump_state_sensors": self.__pump_state_sensors,
1035
+ "tank_volume_sensors": self.__tank_volume_sensors,
1036
+ "bulk_species_node_sensors": self.__bulk_species_node_sensors,
1037
+ "bulk_species_link_sensors": self.__bulk_species_link_sensors,
1038
+ "surface_species_sensors": self.__surface_species_sensors,
1039
+ "node_id_to_idx": self.__node_id_to_idx,
1040
+ "link_id_to_idx": self.__link_id_to_idx,
1041
+ "valve_id_to_idx": self.__valve_id_to_idx,
1042
+ "pump_id_to_idx": self.__pump_id_to_idx,
1043
+ "tank_id_to_idx": self.__tank_id_to_idx,
1044
+ "bulkspecies_id_to_idx": self.__bulkspecies_id_to_idx,
1045
+ "surfacespecies_id_to_idx": self.__surfacespecies_id_to_idx}
1046
+
1047
+ return super().get_attributes() | attr
1048
+
1049
+ def __eq__(self, other) -> bool:
1050
+ if not isinstance(other, SensorConfig):
1051
+ raise TypeError("Can not compare 'SensorConfig' instance " +
1052
+ f"with '{type(other)}' instance")
1053
+
1054
+ return self.__nodes == other.nodes and self.__links == other.links \
1055
+ and self.__valves == other.valves and self.__pumps == other.pumps \
1056
+ and self.__tanks == other.tanks and self.__bulk_species == other.bulk_species \
1057
+ and self.__surface_species == other.surface_species \
1058
+ and self.__pressure_sensors == other.pressure_sensors \
1059
+ and self.__flow_sensors == other.flow_sensors \
1060
+ and self.__demand_sensors == other.demand_sensors \
1061
+ and self.__quality_node_sensors == other.quality_node_sensors \
1062
+ and self.__quality_link_sensors == other.quality_link_sensors \
1063
+ and self.__valve_state_sensors == other.valve_state_sensors \
1064
+ and self.__pump_state_sensors == other.pump_state_sensors \
1065
+ and self.__tank_volume_sensors == other.tank_volume_sensors \
1066
+ and self.__bulk_species_node_sensors == other.bulk_species_node_sensors \
1067
+ and self.__bulk_species_link_sensors == other.bulk_species_link_sensors \
1068
+ and self.__surface_species_sensors == other.surface_species_sensors
1069
+
1070
+ def __str__(self) -> str:
1071
+ return f"nodes: {self.__nodes} links: {self.__links} valves: {self.__valves} " +\
1072
+ f"pumps: {self.__pumps} tanks: {self.__tanks} bulk_species: {self.__bulk_species} " +\
1073
+ f"surface_species: {self.__surface_species}" + \
1074
+ f"pressure_sensors: {self.__pressure_sensors} flow_sensors: {self.__flow_sensors} " +\
1075
+ f"demand_sensors: {self.__demand_sensors} " +\
1076
+ f"quality_node_sensors: {self.__quality_node_sensors} " +\
1077
+ f"quality_link_sensors: {self.__quality_link_sensors} " +\
1078
+ f"valve_state_sensors: {self.__valve_state_sensors} " +\
1079
+ f"pump_state_sensors: {self.__pump_state_sensors} " +\
1080
+ f"tank_volume_sensors: {self.__tank_volume_sensors}" +\
1081
+ f"bulk_species_node_sensors: {self.__bulk_species_node_sensors}" +\
1082
+ f"bulk_species_link_sensors: {self.__bulk_species_link_sensors}" +\
1083
+ f"surface_species_sensors: {self.__surface_species_sensors}"
1084
+
1085
+ def compute_readings(self, pressures: np.ndarray, flows: np.ndarray, demands: np.ndarray,
1086
+ nodes_quality: np.ndarray, links_quality: np.ndarray,
1087
+ pumps_state: np.ndarray, valves_state: np.ndarray,
1088
+ tanks_volume: np.ndarray, bulk_species_node_concentrations: np.ndarray,
1089
+ bulk_species_link_concentrations: np.ndarray,
1090
+ surface_species_concentrations: np.ndarray) -> np.ndarray:
1091
+ """
1092
+ Applies the sensor configuration to a set of raw simulation results --
1093
+ i.e. computes the sensor readings as an array.
1094
+
1095
+ Parameters
1096
+ ----------
1097
+ pressures : `numpy.ndarray`
1098
+ Pressure values at all nodes.
1099
+ flows : `numpy.ndarray`
1100
+ Flow values at all links/pipes.
1101
+ demands : `numpy.ndarray`
1102
+ Demand values at all nodes.
1103
+ nodes_quality : `numpy.ndarray`
1104
+ Quality values at all nodes.
1105
+ links_quality : `numpy.ndarray`
1106
+ Quality values at all links/pipes.
1107
+ pumps_state : `numpy.ndarray`
1108
+ States of all pumps.
1109
+ valves_state : `numpy.ndarray`
1110
+ States of all valves.
1111
+ tanks_volume : `numpy.ndarray`
1112
+ Water volume in all tanks.
1113
+ bulk_species_node_concentrations : `numpy.ndarray`
1114
+ Bulk species concentrations at all nodes.
1115
+
1116
+ Expect a three-dimensional array: First dimension denotes time,
1117
+ second dimension corresponds to species ID,
1118
+ and third dimension contains the concentration.
1119
+ bulk_species_link_concentrations : `numpy.ndarray`
1120
+ Bulk species concentrations at all links/pipes.
1121
+
1122
+ Expect a three-dimensional array: First dimension denotes time,
1123
+ second dimension corresponds to species ID,
1124
+ and third dimension contains the concentration.
1125
+ surface_species_concentrations : `numpy.ndarray`
1126
+ Surface species concentrations at all links/pipes.
1127
+
1128
+ Expect a three-dimensional array: First dimension denotes time,
1129
+ second dimension corresponds to species ID,
1130
+ and third dimension contains the concentration.
1131
+
1132
+ Returns
1133
+ -------
1134
+ `numpy.ndarray`
1135
+ Sensor readings.
1136
+ """
1137
+ data = []
1138
+
1139
+ if pressures is not None:
1140
+ data.append(pressures[:, self.__pressure_idx])
1141
+ else:
1142
+ if len(self.__pressure_sensors) != 0:
1143
+ raise ValueError("Pressure readings requested but no pressure data is given")
1144
+
1145
+ if flows is not None:
1146
+ data.append(flows[:, self.__flow_idx])
1147
+ else:
1148
+ if len(self.__flow_sensors) != 0:
1149
+ raise ValueError("Flow readings requested but no flow data is given")
1150
+
1151
+ if demands is not None:
1152
+ data.append(demands[:, self.__demand_idx])
1153
+ else:
1154
+ if len(self.__demand_sensors) != 0:
1155
+ raise ValueError("Demand readings requested but no demand data is given")
1156
+
1157
+ if nodes_quality is not None:
1158
+ data.append(nodes_quality[:, self.__quality_node_idx])
1159
+ else:
1160
+ if len(self.__quality_node_sensors) != 0:
1161
+ raise ValueError("Node water quality readings requested " +
1162
+ "but no water quality data at nodes is given")
1163
+
1164
+ if links_quality is not None:
1165
+ data.append(links_quality[:, self.__quality_link_idx])
1166
+ else:
1167
+ if len(self.__quality_link_sensors) != 0:
1168
+ raise ValueError("Link/Pipe water quality readings requested " +
1169
+ "but no water quality data at links/pipes is given")
1170
+
1171
+ if valves_state is not None:
1172
+ data.append(valves_state[:, self.__valve_state_idx])
1173
+ else:
1174
+ if len(self.__valve_state_sensors) != 0:
1175
+ raise ValueError("Valve states readings requested " +
1176
+ "but no valve state data is given")
1177
+
1178
+ if pumps_state is not None:
1179
+ data.append(pumps_state[:, self.__pump_state_idx])
1180
+ else:
1181
+ if len(self.__pump_state_sensors) != 0:
1182
+ raise ValueError("Pump states readings requested " +
1183
+ "but no pump state data is given")
1184
+
1185
+ if tanks_volume is not None:
1186
+ data.append(tanks_volume[:, self.__tank_volume_idx])
1187
+ else:
1188
+ if len(self.__tank_volume_sensors) != 0:
1189
+ raise ValueError("Water volumes in tanks is requested but no " +
1190
+ "tank water volume data is given")
1191
+
1192
+ if surface_species_concentrations is not None:
1193
+ for species_idx, links_idx in self.__surface_species_idx:
1194
+ data.append(surface_species_concentrations[:, species_idx, links_idx].
1195
+ reshape(-1, len(links_idx)))
1196
+ else:
1197
+ if len(self.__surface_species_sensors) != 0:
1198
+ raise ValueError("Surface species concentratinons requested but no " +
1199
+ "surface species concentration data is given")
1200
+
1201
+ if bulk_species_node_concentrations is not None:
1202
+ for species_idx, nodes_idx in self.__bulk_species_node_idx:
1203
+ data.append(bulk_species_node_concentrations[:, species_idx, nodes_idx].
1204
+ reshape(-1, len(nodes_idx)))
1205
+ else:
1206
+ if len(self.__bulk_species_node_sensors) != 0:
1207
+ raise ValueError("Bulk species concentratinons requested but no " +
1208
+ "bulk species node concentration data is given")
1209
+
1210
+ if bulk_species_link_concentrations is not None:
1211
+ for species_idx, links_idx in self.__bulk_species_link_idx:
1212
+ data.append(bulk_species_link_concentrations[:, species_idx, links_idx].
1213
+ reshape(-1, len(links_idx)))
1214
+ else:
1215
+ if len(self.__bulk_species_link_sensors) != 0:
1216
+ raise ValueError("Bulk species concentratinons requested but no " +
1217
+ "bulk species link/pipe concentration data is given")
1218
+
1219
+ return np.concatenate(data, axis=1)
1220
+
1221
+ def get_index_of_reading(self, pressure_sensor: str = None, flow_sensor: str = None,
1222
+ demand_sensor: str = None, node_quality_sensor: str = None,
1223
+ link_quality_sensor: str = None, valve_state_sensor: str = None,
1224
+ pump_state_sensor: str = None, tank_volume_sensor: str = None,
1225
+ bulk_species_node_sensor: tuple[str, str] = None,
1226
+ bulk_species_link_sensor: tuple[str, str] = None,
1227
+ surface_species_sensor: tuple[str, str] = None) -> int:
1228
+ """
1229
+ Gets the index of a particular sensor in the final sensor readings array.
1230
+
1231
+ Note that only one sensor ID is converted to an index. In case of multiple sensor IDs,
1232
+ call this function for each sensor ID separately.
1233
+
1234
+ .. note::
1235
+
1236
+ This function only returns the correct results if the sensor configuraton is NOT frozen!
1237
+
1238
+ Parameters
1239
+ ----------
1240
+ pressure_sensor : `str`
1241
+ ID of the pressure sensor.
1242
+ flow_sensor : `str`
1243
+ ID of the flow sensor.
1244
+ demand_sensor : `str`
1245
+ ID of the demand sensor.
1246
+ node_quality_sensor : `str`
1247
+ ID of the quality sensor (at a node).
1248
+ link_quality_sensor : `str`
1249
+ ID of the quality sensor (at a link/pipe).
1250
+ valve_state_sensor : `str`
1251
+ ID of the state sensor (at a valve).
1252
+ pump_state_sensor : `str`
1253
+ ID of the state sensor (at a pump).
1254
+ tank_volume_sensor : `str`
1255
+ ID of the water volume sensor (at a tank)
1256
+ bulk_species_node_sensor : `tuple[str, str]`
1257
+ Tuple of bulk species ID and sensor node ID.
1258
+ bulk_species_link_sensor : `tuple[str, str]`
1259
+ Tuple of bulk species ID and sensor link/pipe ID.
1260
+ surface_species_sensor : `tuple[str, str]`
1261
+ Tuple of surface species ID and sensor link/pipe ID.
1262
+ """
1263
+ if pressure_sensor is not None:
1264
+ return self.__sensors_id_to_idx["pressure"][pressure_sensor]
1265
+ elif flow_sensor is not None:
1266
+ return self.__sensors_id_to_idx["flow"][flow_sensor]
1267
+ elif demand_sensor is not None:
1268
+ return self.__sensors_id_to_idx["demand"][demand_sensor]
1269
+ elif node_quality_sensor is not None:
1270
+ return self.__sensors_id_to_idx["quality_node"][node_quality_sensor]
1271
+ elif link_quality_sensor is not None:
1272
+ return self.__sensors_id_to_idx["quality_link"][link_quality_sensor]
1273
+ elif valve_state_sensor is not None:
1274
+ return self.__sensors_id_to_idx["valve_state"][valve_state_sensor]
1275
+ elif pump_state_sensor is not None:
1276
+ return self.__sensors_id_to_idx["pump_state"][pump_state_sensor]
1277
+ elif tank_volume_sensor is not None:
1278
+ return self.__sensors_id_to_idx["tank_volume"][tank_volume_sensor]
1279
+ elif surface_species_sensor is not None:
1280
+ species_id, sensor_id = surface_species_sensor
1281
+ return self.__sensors_id_to_idx["surface_species"][species_id][sensor_id]
1282
+ elif bulk_species_node_sensor is not None:
1283
+ species_id, sensor_id = bulk_species_node_sensor
1284
+ return self.__sensors_id_to_idx["bulk_species_node"][species_id][sensor_id]
1285
+ elif bulk_species_link_sensor is not None:
1286
+ species_id, sensor_id = bulk_species_link_sensor
1287
+ return self.__sensors_id_to_idx["bulk_species_link"][species_id][sensor_id]
1288
+ else:
1289
+ raise ValueError("No sensor given")