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,1589 @@
1
+ """
2
+ Module provides a class for storing and processing SCADA data.
3
+ """
4
+ import warnings
5
+ from typing import Callable
6
+ from copy import deepcopy
7
+ import numpy as np
8
+
9
+ from ..sensor_config import SensorConfig, SENSOR_TYPE_LINK_FLOW, SENSOR_TYPE_LINK_QUALITY, \
10
+ SENSOR_TYPE_NODE_DEMAND, SENSOR_TYPE_NODE_PRESSURE, SENSOR_TYPE_NODE_QUALITY, \
11
+ SENSOR_TYPE_PUMP_STATE, SENSOR_TYPE_TANK_VOLUME, SENSOR_TYPE_VALVE_STATE, \
12
+ SENSOR_TYPE_NODE_BULK_SPECIES, SENSOR_TYPE_LINK_BULK_SPECIES, SENSOR_TYPE_SURFACE_SPECIES
13
+ from ..events import SensorFault, SensorReadingAttack, SensorReadingEvent
14
+ from ...uncertainty import SensorNoise
15
+ from ...serialization import serializable, Serializable, SCADA_DATA_ID
16
+
17
+
18
+ @serializable(SCADA_DATA_ID, ".epytflow_scada_data")
19
+ class ScadaData(Serializable):
20
+ """
21
+ Class for storing and processing SCADA data.
22
+
23
+ Parameters
24
+ ----------
25
+ sensor_config : :class:`~epyt_flow.simulation.sensor_config.SensorConfig`
26
+ Specifications of all sensors.
27
+ sensor_readings_time : `numpy.ndarray`
28
+ Time (seconds since simulation start) for each sensor reading row
29
+ in `sensor_readings_data_raw`.
30
+
31
+ This parameter is expected to be a 1d array with the same size as
32
+ the number of rows in `sensor_readings_data_raw`.
33
+ pressure_data_raw : `numpy.ndarray`, optional
34
+ Raw pressure values of all nodes as a two-dimensional array --
35
+ first dimension encodes time, second dimension pressure at nodes.
36
+
37
+ The default is None,
38
+ flow_data_raw : `numpy.ndarray`, optional
39
+ Raw flow values of all links/pipes --
40
+ first dimension encodes time, second dimension pressure at links/pipes.
41
+
42
+ The default is None.
43
+ demand_data_raw : `numpy.ndarray`, optional
44
+ Raw demand values of all nodes --
45
+ first dimension encodes time, second dimension demand at nodes.
46
+
47
+ The default is None.
48
+ node_quality_data_raw : `numpy.ndarray`, optional
49
+ Raw quality values of all nodes --
50
+ first dimension encodes time, second dimension quality at nodes.
51
+
52
+ The default is None.
53
+ link_quality_data_raw : `numpy.ndarray`, optional
54
+ Raw quality values of all links/pipes --
55
+ first dimension encodes time, second dimension quality at links/pipes.
56
+
57
+ The default is None.
58
+ pumps_state_data_raw : `numpy.ndarray`, optional
59
+ States of all pumps --
60
+ first dimension encodes time, second dimension states of pumps.
61
+
62
+ The default is None.
63
+ valves_state_data_raw : `numpy.ndarray`, optional
64
+ States of all valves --
65
+ first dimension encodes time, second dimension states of valves.
66
+
67
+ The default is None.
68
+ tanks_volume_data_raw : `numpy.ndarray`, optional
69
+ Water volumes in all tanks --
70
+ first dimension encodes time, second dimension water volume in tanks.
71
+
72
+ The default is None.
73
+ surface_species_concentration_raw : `numpy.ndarray`, optional
74
+ Raw concentrations of surface species as a tree dimensional array --
75
+ first dimension encodes time, second dimension denotes the different surface species,
76
+ third dimension denotes species concentrations at links/pipes.
77
+
78
+ The default is None.
79
+ bulk_species_node_concentration_raw : `numpy.ndarray`, optional
80
+ Raw concentrations of bulk species at nodes as a tree dimensional array --
81
+ first dimension encodes time, second dimension denotes the different bulk species,
82
+ third dimension denotes species concentrations at nodes.
83
+
84
+ The default is None.
85
+ bulk_species_link_concentration_raw : `numpy.ndarray`, optional
86
+ Raw concentrations of bulk species at links as a tree dimensional array --
87
+ first dimension encodes time, second dimension denotes the different bulk species,
88
+ third dimension denotes species concentrations at nodes.
89
+
90
+ The default is None.
91
+ pump_energy_usage_data : `numpy.ndarray`, optional
92
+ Energy usage data of each pump.
93
+
94
+ The default is None.
95
+ pump_efficiency_data : `numpy.ndarray`, optional
96
+ Pump efficiency data of each pump.
97
+
98
+ The default is None.
99
+ sensor_faults : list[:class:`~epyt_flow.simulation.events.sensor_faults.SensorFault`], optional
100
+ List of sensor faults to be applied to the sensor readings.
101
+
102
+ The default is an empty list.
103
+ sensor_reading_attacks : list[:class:`~epyt_flow.simulation.events.sensor_reading_attack.SensorReadingAttack`], optional
104
+ List of sensor reading attacks to be applied to the sensor readings.
105
+
106
+ The default is an empty list.
107
+ sensor_reading_events : list[`:class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`], optional
108
+ List of additional sensor reading events that are to be applied to the sensor readings.
109
+
110
+ The default is an empty list.
111
+ sensor_noise : :class:`~epyt_flow.uncertainty.sensor_noise.SensorNoise`, optional
112
+ Specification of the sensor noise/uncertainty to be added to the sensor readings.
113
+
114
+ The default is None.
115
+ frozen_sensor_config : `bool`, optional
116
+ If True, the sensor config can not be changed and only the required sensor nodes/links
117
+ will be stored -- this usually leads to a significant reduction in memory consumption.
118
+
119
+ The default is False.
120
+ """
121
+
122
+ def __init__(self, sensor_config: SensorConfig, sensor_readings_time: np.ndarray,
123
+ pressure_data_raw: np.ndarray = None, flow_data_raw: np.ndarray = None,
124
+ demand_data_raw: np.ndarray = None, node_quality_data_raw: np.ndarray = None,
125
+ link_quality_data_raw: np.ndarray = None, pumps_state_data_raw: np.ndarray = None,
126
+ valves_state_data_raw: np.ndarray = None, tanks_volume_data_raw: np.ndarray = None,
127
+ surface_species_concentration_raw: np.ndarray = None,
128
+ bulk_species_node_concentration_raw: np.ndarray = None,
129
+ bulk_species_link_concentration_raw: np.ndarray = None,
130
+ pump_energy_usage_data: np.ndarray = None,
131
+ pump_efficiency_data: np.ndarray = None,
132
+ sensor_faults: list[SensorFault] = [],
133
+ sensor_reading_attacks: list[SensorReadingAttack] = [],
134
+ sensor_reading_events: list[SensorReadingEvent] = [],
135
+ sensor_noise: SensorNoise = None, frozen_sensor_config: bool = False, **kwds):
136
+ if not isinstance(sensor_config, SensorConfig):
137
+ raise TypeError("'sensor_config' must be an instance of " +
138
+ "'epyt_flow.simulation.SensorConfig' but not of " +
139
+ f"'{type(sensor_config)}'")
140
+ if not isinstance(sensor_readings_time, np.ndarray):
141
+ raise TypeError("'sensor_readings_time' must be an instance of 'numpy.ndarray' " +
142
+ f"but not of '{type(sensor_readings_time)}'")
143
+ if pressure_data_raw is not None:
144
+ if not isinstance(pressure_data_raw, np.ndarray):
145
+ raise TypeError("'pressure_data_raw' must be an instance of 'numpy.ndarray'" +
146
+ f" but not of '{type(pressure_data_raw)}'")
147
+ if flow_data_raw is not None:
148
+ if not isinstance(flow_data_raw, np.ndarray):
149
+ raise TypeError("'flow_data_raw' must be an instance of 'numpy.ndarray' " +
150
+ f"but not of '{type(flow_data_raw)}'")
151
+ if demand_data_raw is not None:
152
+ if not isinstance(demand_data_raw, np.ndarray):
153
+ raise TypeError("'demand_data_raw' must be an instance of 'numpy.ndarray' " +
154
+ f"but not of '{type(demand_data_raw)}'")
155
+ if node_quality_data_raw is not None:
156
+ if not isinstance(node_quality_data_raw, np.ndarray):
157
+ raise TypeError("'node_quality_data_raw' must be an instance of 'numpy.ndarray'" +
158
+ f" but not of '{type(node_quality_data_raw)}'")
159
+ if link_quality_data_raw is not None:
160
+ if not isinstance(link_quality_data_raw, np.ndarray):
161
+ raise TypeError("'link_quality_data_raw' must be an instance of 'numpy.ndarray'" +
162
+ f" but not of '{type(link_quality_data_raw)}'")
163
+ if pumps_state_data_raw is not None:
164
+ if not isinstance(pumps_state_data_raw, np.ndarray):
165
+ raise TypeError("'pumps_state_data_raw' must be an instance of 'numpy.ndarray' " +
166
+ f"but no of '{type(pumps_state_data_raw)}'")
167
+ if valves_state_data_raw is not None:
168
+ if not isinstance(valves_state_data_raw, np.ndarray):
169
+ raise TypeError("'valves_state_data_raw' must be an instance of 'numpy.ndarray' " +
170
+ f"but no of '{type(valves_state_data_raw)}'")
171
+ if tanks_volume_data_raw is not None:
172
+ if not isinstance(tanks_volume_data_raw, np.ndarray):
173
+ raise TypeError("'tanks_volume_data_raw' must be an instance of 'numpy.ndarray'" +
174
+ f" but not of '{type(tanks_volume_data_raw)}'")
175
+ if sensor_faults is None or not isinstance(sensor_faults, list):
176
+ raise TypeError("'sensor_faults' must be a list of " +
177
+ "'epyt_flow.simulation.events.SensorFault' instances but " +
178
+ f"'{type(sensor_faults)}'")
179
+ if surface_species_concentration_raw is not None:
180
+ if not isinstance(surface_species_concentration_raw, np.ndarray):
181
+ raise TypeError("'surface_species_concentration_raw' must be an instance of " +
182
+ "'numpy.ndarray' but not of " +
183
+ f"'{type(surface_species_concentration_raw)}'")
184
+ if bulk_species_node_concentration_raw is not None:
185
+ if not isinstance(bulk_species_node_concentration_raw, np.ndarray):
186
+ raise TypeError("'bulk_species_node_concentration_raw' must be an instance of " +
187
+ "'numpy.ndarray' but not of " +
188
+ f"'{type(bulk_species_node_concentration_raw)}'")
189
+ if bulk_species_link_concentration_raw is not None:
190
+ if not isinstance(bulk_species_link_concentration_raw, np.ndarray):
191
+ raise TypeError("'bulk_species_link_concentration_raw' must be an instance of " +
192
+ "'numpy.ndarray' but not of " +
193
+ f"'{type(bulk_species_link_concentration_raw)}'")
194
+ if pump_energy_usage_data is not None:
195
+ if not isinstance(pump_energy_usage_data, np.ndarray):
196
+ raise TypeError("'pump_energy_usage_data' must be an instance of 'numpy.ndarray' " +
197
+ f"but not of '{type(pump_energy_usage_data)}'")
198
+ if pump_efficiency_data is not None:
199
+ if not isinstance(pump_efficiency_data, np.ndarray):
200
+ raise TypeError("'pump_efficiency_data' must be an instance of 'numpy.ndarray' " +
201
+ f"but not of '{type(pump_efficiency_data)}'")
202
+ if len(sensor_faults) != 0:
203
+ if any(not isinstance(f, SensorFault) for f in sensor_faults):
204
+ raise TypeError("'sensor_faults' must be a list of " +
205
+ "'epyt_flow.simulation.event.SensorFault' instances")
206
+ if len(sensor_reading_attacks) != 0:
207
+ if any(not isinstance(f, SensorReadingAttack) for f in sensor_reading_attacks):
208
+ raise TypeError("'sensor_reading_attacks' must be a list of " +
209
+ "'epyt_flow.simulation.event.SensorReadingAttack' instances")
210
+ if len(sensor_reading_events) != 0:
211
+ if any(not isinstance(f, SensorReadingEvent) for f in sensor_reading_events):
212
+ raise TypeError("'sensor_reading_events' must be a list of " +
213
+ "'epyt_flow.simulation.event.SensorReadingEvent' instances")
214
+ if sensor_noise is not None and not isinstance(sensor_noise, SensorNoise):
215
+ raise TypeError("'sensor_noise' must be an instance of " +
216
+ "'epyt_flow.uncertainty.SensorNoise' but not of " +
217
+ f"'{type(sensor_noise)}'")
218
+ if not isinstance(frozen_sensor_config, bool):
219
+ raise TypeError("'frozen_sensor_config' must be an instance of 'bool' " +
220
+ f"but not of '{type(frozen_sensor_config)}'")
221
+
222
+ def __raise_shape_mismatch(var_name: str) -> None:
223
+ raise ValueError(f"Shape mismatch in '{var_name}' -- " +
224
+ "i.e number of time steps in 'sensor_readings_time' " +
225
+ "must match number of raw measurements.")
226
+
227
+ n_time_steps = sensor_readings_time.shape[0]
228
+ if pressure_data_raw is not None:
229
+ if pressure_data_raw.shape[0] != n_time_steps:
230
+ __raise_shape_mismatch("pressure_data_raw")
231
+ if flow_data_raw is not None:
232
+ if flow_data_raw.shape[0] != n_time_steps:
233
+ __raise_shape_mismatch("flow_data_raw")
234
+ if demand_data_raw is not None:
235
+ if demand_data_raw.shape[0] != n_time_steps:
236
+ __raise_shape_mismatch("demand_data_raw")
237
+ if node_quality_data_raw is not None:
238
+ if node_quality_data_raw.shape[0] != n_time_steps:
239
+ __raise_shape_mismatch("node_quality_data_raw")
240
+ if link_quality_data_raw is not None:
241
+ if link_quality_data_raw.shape[0] != n_time_steps:
242
+ __raise_shape_mismatch("link_quality_data_raw")
243
+ if valves_state_data_raw is not None:
244
+ if valves_state_data_raw.shape[0] != n_time_steps:
245
+ __raise_shape_mismatch("valves_state_data_raw")
246
+ if pumps_state_data_raw is not None:
247
+ if pumps_state_data_raw.shape[0] != n_time_steps:
248
+ __raise_shape_mismatch("pumps_state_data_raw")
249
+ if tanks_volume_data_raw is not None:
250
+ if tanks_volume_data_raw.shape[0] != n_time_steps:
251
+ __raise_shape_mismatch("tanks_volume_data_raw")
252
+ if valves_state_data_raw is not None:
253
+ if not valves_state_data_raw.shape[0] == n_time_steps:
254
+ __raise_shape_mismatch("valves_state_data_raw")
255
+ if pumps_state_data_raw is not None:
256
+ if not pumps_state_data_raw.shape[0] == n_time_steps:
257
+ __raise_shape_mismatch("pumps_state_data_raw")
258
+ if tanks_volume_data_raw is not None:
259
+ if not tanks_volume_data_raw.shape[0] == n_time_steps:
260
+ __raise_shape_mismatch("tanks_volume_data_raw")
261
+ if bulk_species_node_concentration_raw is not None:
262
+ if bulk_species_node_concentration_raw.shape[0] != n_time_steps:
263
+ __raise_shape_mismatch("bulk_species_node_concentration_raw")
264
+ if bulk_species_link_concentration_raw is not None:
265
+ if bulk_species_link_concentration_raw.shape[0] != n_time_steps:
266
+ __raise_shape_mismatch("bulk_species_link_concentration_raw")
267
+ if surface_species_concentration_raw is not None:
268
+ if surface_species_concentration_raw.shape[0] != n_time_steps:
269
+ __raise_shape_mismatch("surface_species_concentration_raw")
270
+ if pump_energy_usage_data is not None:
271
+ if pump_energy_usage_data.shape[0] != n_time_steps:
272
+ __raise_shape_mismatch("pump_energy_usage_data")
273
+ if pump_efficiency_data is not None:
274
+ if pump_efficiency_data.shape[0] != n_time_steps:
275
+ __raise_shape_mismatch("pump_efficiency_data")
276
+
277
+ self.__sensor_config = sensor_config
278
+ self.__sensor_noise = sensor_noise
279
+ self.__sensor_reading_events = sensor_faults + sensor_reading_attacks + \
280
+ sensor_reading_events
281
+
282
+ self.__sensor_readings = None
283
+ self.__frozen_sensor_config = frozen_sensor_config
284
+ self.__sensor_readings_time = sensor_readings_time
285
+ self.__pump_energy_usage_data = pump_energy_usage_data
286
+ self.__pump_efficiency_data = pump_efficiency_data
287
+
288
+ if self.__frozen_sensor_config is False:
289
+ self.__pressure_data_raw = pressure_data_raw
290
+ self.__flow_data_raw = flow_data_raw
291
+ self.__demand_data_raw = demand_data_raw
292
+ self.__node_quality_data_raw = node_quality_data_raw
293
+ self.__link_quality_data_raw = link_quality_data_raw
294
+ self.__pumps_state_data_raw = pumps_state_data_raw
295
+ self.__valves_state_data_raw = valves_state_data_raw
296
+ self.__tanks_volume_data_raw = tanks_volume_data_raw
297
+ self.__surface_species_concentration_raw = surface_species_concentration_raw
298
+ self.__bulk_species_node_concentration_raw = bulk_species_node_concentration_raw
299
+ self.__bulk_species_link_concentration_raw = bulk_species_link_concentration_raw
300
+ else:
301
+ sensor_config = self.__sensor_config
302
+
303
+ node_to_idx = sensor_config.node_id_to_idx
304
+ link_to_idx = sensor_config.link_id_to_idx
305
+ pump_to_idx = sensor_config.pump_id_to_idx
306
+ valve_to_idx = sensor_config.valve_id_to_idx
307
+ tank_to_idx = sensor_config.tank_id_to_idx
308
+
309
+ # EPANET quantities
310
+ def __reduce_data(data: np.ndarray, sensors: list[str],
311
+ item_to_idx: Callable[[str], int]) -> np.ndarray:
312
+ idx = [item_to_idx(item_id) for item_id in sensors]
313
+
314
+ if data is None or len(idx) == 0:
315
+ return None
316
+ else:
317
+ return data[:, idx]
318
+
319
+ self.__pressure_data_raw = __reduce_data(data=pressure_data_raw,
320
+ item_to_idx=node_to_idx,
321
+ sensors=sensor_config.pressure_sensors)
322
+ self.__flow_data_raw = __reduce_data(data=flow_data_raw,
323
+ item_to_idx=link_to_idx,
324
+ sensors=sensor_config.flow_sensors)
325
+ self.__demand_data_raw = __reduce_data(data=demand_data_raw,
326
+ item_to_idx=node_to_idx,
327
+ sensors=sensor_config.demand_sensors)
328
+ self.__node_quality_data_raw = __reduce_data(data=node_quality_data_raw,
329
+ item_to_idx=node_to_idx,
330
+ sensors=sensor_config.quality_node_sensors)
331
+ self.__link_quality_data_raw = __reduce_data(data=link_quality_data_raw,
332
+ item_to_idx=link_to_idx,
333
+ sensors=sensor_config.quality_link_sensors)
334
+ self.__pumps_state_data_raw = __reduce_data(data=pumps_state_data_raw,
335
+ item_to_idx=pump_to_idx,
336
+ sensors=sensor_config.pump_state_sensors)
337
+ self.__valves_state_data_raw = __reduce_data(data=valves_state_data_raw,
338
+ item_to_idx=valve_to_idx,
339
+ sensors=sensor_config.valve_state_sensors)
340
+ self.__tanks_volume_data_raw = __reduce_data(data=tanks_volume_data_raw,
341
+ item_to_idx=tank_to_idx,
342
+ sensors=sensor_config.tank_volume_sensors)
343
+
344
+ # EPANET-MSX quantities
345
+ def __reduce_msx_data(data: np.ndarray, sensors: list[tuple[list[int], list[int]]]
346
+ ) -> np.ndarray:
347
+ if data is None or len(sensors) == 0:
348
+ return None
349
+ else:
350
+ r = []
351
+ for species_idx, item_idx in sensors:
352
+ r.append(data[:, species_idx, item_idx].reshape(-1, len(item_idx)))
353
+
354
+ return np.concatenate(r, axis=1)
355
+
356
+ node_bulk_species_idx = [(sensor_config.bulkspecies_id_to_idx(s),
357
+ [sensor_config.node_id_to_idx(node_id)
358
+ for node_id in sensor_config.bulk_species_node_sensors[s]
359
+ ]) for s in sensor_config.bulk_species_node_sensors.keys()]
360
+ self.__bulk_species_node_concentration_raw = \
361
+ __reduce_msx_data(data=bulk_species_node_concentration_raw,
362
+ sensors=node_bulk_species_idx)
363
+
364
+ bulk_species_link_idx = [(sensor_config.bulkspecies_id_to_idx(s),
365
+ [sensor_config.link_id_to_idx(link_id)
366
+ for link_id in sensor_config.bulk_species_link_sensors[s]
367
+ ]) for s in sensor_config.bulk_species_link_sensors.keys()]
368
+ self.__bulk_species_link_concentration_raw = \
369
+ __reduce_msx_data(data=bulk_species_link_concentration_raw,
370
+ sensors=bulk_species_link_idx)
371
+
372
+ surface_species_idx = [(sensor_config.surfacespecies_id_to_idx(s),
373
+ [sensor_config.link_id_to_idx(link_id)
374
+ for link_id in sensor_config.surface_species_sensors[s]
375
+ ]) for s in sensor_config.surface_species_sensors.keys()]
376
+ self.__surface_species_concentration_raw = \
377
+ __reduce_msx_data(data=surface_species_concentration_raw,
378
+ sensors=surface_species_idx)
379
+
380
+ self.__init()
381
+
382
+ super().__init__(**kwds)
383
+
384
+ @property
385
+ def frozen_sensor_config(self) -> bool:
386
+ """
387
+ Checks if the sensor configuration is frozen or not.
388
+
389
+ Returns
390
+ -------
391
+ `bool`
392
+ True if the sensor configuration is frozen, False otherwise.
393
+ """
394
+ return self.__frozen_sensor_config
395
+
396
+ @property
397
+ def sensor_config(self) -> SensorConfig:
398
+ """
399
+ Gets the sensor configuration.
400
+
401
+ Returns
402
+ -------
403
+ :class:`~epyt_flow.simulation.sensor_config.SensorConfig`
404
+ Sensor configuration.
405
+ """
406
+ return deepcopy(self.__sensor_config)
407
+
408
+ @sensor_config.setter
409
+ def sensor_config(self, sensor_config: SensorConfig) -> None:
410
+ if self.__frozen_sensor_config is True:
411
+ raise RuntimeError("Sensor config can not be changed because it is frozen")
412
+
413
+ self.change_sensor_config(sensor_config)
414
+
415
+ @property
416
+ def sensor_noise(self) -> SensorNoise:
417
+ """
418
+ Gets the sensor noise.
419
+
420
+ Returns
421
+ -------
422
+ :class:`~epyt_flow.uncertainty.sensor_noise.SensorNoise`
423
+ Sensor noise.
424
+ """
425
+ return deepcopy(self.__sensor_noise)
426
+
427
+ @sensor_noise.setter
428
+ def sensor_noise(self, sensor_noise: SensorNoise) -> None:
429
+ self.change_sensor_noise(sensor_noise)
430
+
431
+ @property
432
+ def sensor_faults(self) -> list[SensorFault]:
433
+ """
434
+ Gets all sensor faults.
435
+
436
+ Returns
437
+ -------
438
+ list[:class:`~epyt_flow.simulation.events.sensor_faults.SensorFault`]
439
+ All sensor faults.
440
+ """
441
+ return deepcopy(list(filter(lambda e: isinstance(e, SensorFault),
442
+ self.__sensor_reading_events)))
443
+
444
+ @sensor_faults.setter
445
+ def sensor_faults(self, sensor_faults: list[SensorFault]) -> None:
446
+ self.change_sensor_faults(sensor_faults)
447
+
448
+ @property
449
+ def sensor_reading_attacks(self) -> list[SensorReadingAttack]:
450
+ """
451
+ Gets all sensor reading attacks.
452
+
453
+ Returns
454
+ -------
455
+ list[:class:`~epyt_flow.simulation.events.sensor_reading_attack.SensorReadingAttack`]
456
+ All sensor reading attacks.
457
+ """
458
+ return deepcopy(list(filter(lambda e: isinstance(e, SensorReadingAttack),
459
+ self.__sensor_reading_events)))
460
+
461
+ @sensor_reading_attacks.setter
462
+ def sensor_reading_attacks(self, sensor_reading_attacks: list[SensorReadingAttack]) -> None:
463
+ self.change_sensor_reading_attacks(sensor_reading_attacks)
464
+
465
+ @property
466
+ def sensor_reading_events(self) -> list[SensorReadingEvent]:
467
+ """
468
+ Gets all sensor reading events.
469
+
470
+ Returns
471
+ -------
472
+ list[:class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`]
473
+ All sensor faults.
474
+ """
475
+ return deepcopy(self.__sensor_reading_events)
476
+
477
+ @sensor_reading_events.setter
478
+ def sensor_reading_events(self, sensor_reading_events: list[SensorReadingEvent]) -> None:
479
+ self.change_sensor_reading_events(sensor_reading_events)
480
+
481
+ @property
482
+ def pressure_data_raw(self) -> np.ndarray:
483
+ """
484
+ Gets the raw pressure readings.
485
+
486
+ Returns
487
+ -------
488
+ `numpy.ndarray`
489
+ Raw pressure readings.
490
+ """
491
+ return deepcopy(self.__pressure_data_raw)
492
+
493
+ @property
494
+ def flow_data_raw(self) -> np.ndarray:
495
+ """
496
+ Gets the raw flow readings.
497
+
498
+ Returns
499
+ -------
500
+ `numpy.ndarray`
501
+ Raw flow readings.
502
+ """
503
+ return deepcopy(self.__flow_data_raw)
504
+
505
+ @property
506
+ def demand_data_raw(self) -> np.ndarray:
507
+ """
508
+ Gets the raw demand readings.
509
+
510
+ Returns
511
+ -------
512
+ `numpy.ndarray`
513
+ Raw demand readings.
514
+ """
515
+ return deepcopy(self.__demand_data_raw)
516
+
517
+ @property
518
+ def node_quality_data_raw(self) -> np.ndarray:
519
+ """
520
+ Gets the raw node quality readings.
521
+
522
+ Returns
523
+ -------
524
+ `numpy.ndarray`
525
+ Raw node quality readings.
526
+ """
527
+ return deepcopy(self.__node_quality_data_raw)
528
+
529
+ @property
530
+ def link_quality_data_raw(self) -> np.ndarray:
531
+ """
532
+ Gets the raw link quality readings.
533
+
534
+ Returns
535
+ -------
536
+ `numpy.ndarray`
537
+ Raw link quality readings.
538
+ """
539
+ return deepcopy(self.__link_quality_data_raw)
540
+
541
+ @property
542
+ def sensor_readings_time(self) -> np.ndarray:
543
+ """
544
+ Gets the sensor readings time stamps.
545
+
546
+ Returns
547
+ -------
548
+ `numpy.ndarray`
549
+ Sensor readings time stamps.
550
+ """
551
+ return deepcopy(self.__sensor_readings_time)
552
+
553
+ @property
554
+ def pumps_state_data_raw(self) -> np.ndarray:
555
+ """
556
+ Gets the raw pump state readings.
557
+
558
+ Returns
559
+ -------
560
+ `numpy.ndarray`
561
+ Raw pump state readings.
562
+ """
563
+ return deepcopy(self.__pumps_state_data_raw)
564
+
565
+ @property
566
+ def valves_state_data_raw(self) -> np.ndarray:
567
+ """
568
+ Gets the raw valve state readings.
569
+
570
+ Returns
571
+ -------
572
+ `numpy.ndarray`
573
+ Raw valve state readings.
574
+ """
575
+ return deepcopy(self.__valves_state_data_raw)
576
+
577
+ @property
578
+ def tanks_volume_data_raw(self) -> np.ndarray:
579
+ """
580
+ Gets the raw tank volume readings.
581
+
582
+ Returns
583
+ -------
584
+ `numpy.ndarray`
585
+ Raw tank volume readings.
586
+ """
587
+ return deepcopy(self.__tanks_volume_data_raw)
588
+
589
+ @property
590
+ def surface_species_concentration_raw(self) -> np.ndarray:
591
+ """
592
+ Gets the raw surface species concentrations at links/pipes.
593
+
594
+ Returns
595
+ -------
596
+ `numpy.ndarray`
597
+ Raw species concentrations.
598
+ """
599
+ return deepcopy(self.__surface_species_concentration_raw)
600
+
601
+ @property
602
+ def bulk_species_node_concentration_raw(self) -> np.ndarray:
603
+ """
604
+ Gets the raw bulk species concentrations at nodes.
605
+
606
+ Returns
607
+ -------
608
+ `numpy.ndarray`
609
+ Raw species concentrations.
610
+ """
611
+ return deepcopy(self.__bulk_species_node_concentration_raw)
612
+
613
+ @property
614
+ def bulk_species_link_concentration_raw(self) -> np.ndarray:
615
+ """
616
+ Gets the raw bulk species concentrations at links/pipes.
617
+
618
+ Returns
619
+ -------
620
+ `numpy.ndarray`
621
+ Raw species concentrations.
622
+ """
623
+ return deepcopy(self.__bulk_species_link_concentration_raw)
624
+
625
+ @property
626
+ def pump_energy_usage_data(self) -> np.ndarray:
627
+ """
628
+ Gets the energy usage of each pump.
629
+
630
+ .. note::
631
+ This attribute is NOT included in
632
+ :func:`~epyt_flow.simulation.scada.scada_data.ScadaData.get_data` --
633
+ calling this function is the only way of accessing the energy usage of each pump.
634
+
635
+ Returns
636
+ -------
637
+ `numpy.ndarray`
638
+ Energy usage of each pump.
639
+ """
640
+ return deepcopy(self.__pump_energy_usage_data)
641
+
642
+ @property
643
+ def pump_efficiency_data(self) -> np.ndarray:
644
+ """
645
+ Gets the pumps' efficiency.
646
+
647
+ .. note::
648
+ This attribute is NOT included in
649
+ :func:`~epyt_flow.simulation.scada.scada_data.ScadaData.get_data` --
650
+ calling this function is the only way of accessing the pumps' efficiency.
651
+
652
+ Returns
653
+ -------
654
+ `numpy.ndarray`
655
+ Pumps' efficiency.
656
+ """
657
+ return deepcopy(self.__pump_efficiency_data)
658
+
659
+ def __init(self):
660
+ self.__apply_sensor_noise = lambda x: x
661
+ if self.__sensor_noise is not None:
662
+ self.__apply_sensor_noise = self.__sensor_noise.apply
663
+
664
+ self.__apply_sensor_reading_events = []
665
+ for sensor_event in self.__sensor_reading_events:
666
+ idx = None
667
+ if sensor_event.sensor_type == SENSOR_TYPE_NODE_PRESSURE:
668
+ idx = self.__sensor_config.get_index_of_reading(
669
+ pressure_sensor=sensor_event.sensor_id)
670
+ elif sensor_event.sensor_type == SENSOR_TYPE_NODE_QUALITY:
671
+ idx = self.__sensor_config.get_index_of_reading(
672
+ node_quality_sensor=sensor_event.sensor_id)
673
+ elif sensor_event.sensor_type == SENSOR_TYPE_NODE_DEMAND:
674
+ idx = self.__sensor_config.get_index_of_reading(
675
+ demand_sensor=sensor_event.sensor_id)
676
+ elif sensor_event.sensor_type == SENSOR_TYPE_LINK_FLOW:
677
+ idx = self.__sensor_config.get_index_of_reading(
678
+ flow_sensor=sensor_event.sensor_id)
679
+ elif sensor_event.sensor_type == SENSOR_TYPE_LINK_QUALITY:
680
+ idx = self.__sensor_config.get_index_of_reading(
681
+ link_quality_sensor=sensor_event.sensor_id)
682
+ elif sensor_event.sensor_type == SENSOR_TYPE_VALVE_STATE:
683
+ idx = self.__sensor_config.get_index_of_reading(
684
+ valve_state_sensor=sensor_event.sensor_id)
685
+ elif sensor_event.sensor_type == SENSOR_TYPE_PUMP_STATE:
686
+ idx = self.__sensor_config.get_index_of_reading(
687
+ pump_state_sensor=sensor_event.sensor_id)
688
+ elif sensor_event.sensor_type == SENSOR_TYPE_TANK_VOLUME:
689
+ idx = self.__sensor_config.get_index_of_reading(
690
+ tank_volume_sensor=sensor_event.sensor_id)
691
+ elif sensor_event.sensor_type == SENSOR_TYPE_NODE_BULK_SPECIES:
692
+ idx = self.__sensor_config.get_index_of_reading(
693
+ bulk_species_node_sensor=sensor_event.sensor_id)
694
+ elif sensor_event.sensor_type == SENSOR_TYPE_LINK_BULK_SPECIES:
695
+ idx = self.__sensor_config.get_index_of_reading(
696
+ bulk_species_link_sensor=sensor_event.sensor_id)
697
+ elif sensor_event.sensor_type == SENSOR_TYPE_SURFACE_SPECIES:
698
+ idx = self.__sensor_config.get_index_of_reading(
699
+ surface_species_sensor=sensor_event.sensor_id)
700
+
701
+ self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
702
+
703
+ self.__sensor_readings = None
704
+
705
+ def get_attributes(self) -> dict:
706
+ attr = {"sensor_config": self.__sensor_config,
707
+ "frozen_sensor_config": self.__frozen_sensor_config,
708
+ "sensor_noise": self.__sensor_noise,
709
+ "sensor_reading_events": self.__sensor_reading_events,
710
+ "pressure_data_raw": self.__pressure_data_raw,
711
+ "flow_data_raw": self.__flow_data_raw,
712
+ "demand_data_raw": self.__demand_data_raw,
713
+ "node_quality_data_raw": self.__node_quality_data_raw,
714
+ "link_quality_data_raw": self.__link_quality_data_raw,
715
+ "sensor_readings_time": self.__sensor_readings_time,
716
+ "pumps_state_data_raw": self.__pumps_state_data_raw,
717
+ "valves_state_data_raw": self.__valves_state_data_raw,
718
+ "tanks_volume_data_raw": self.__tanks_volume_data_raw,
719
+ "surface_species_concentration_raw": self.__surface_species_concentration_raw,
720
+ "bulk_species_node_concentration_raw": self.__bulk_species_node_concentration_raw,
721
+ "bulk_species_link_concentration_raw": self.__bulk_species_link_concentration_raw,
722
+ "pump_energy_usage_data": self.__pump_energy_usage_data,
723
+ "pump_efficiency_data": self.__pump_efficiency_data}
724
+
725
+ return super().get_attributes() | attr
726
+
727
+ def __eq__(self, other) -> bool:
728
+ if not isinstance(other, ScadaData):
729
+ raise TypeError(f"Can not compare 'ScadaData' instance to '{type(other)}' instance")
730
+
731
+ try:
732
+ return self.__sensor_config == other.sensor_config \
733
+ and self.__frozen_sensor_config == other.frozen_sensor_config \
734
+ and self.__sensor_noise == other.sensor_noise \
735
+ and all(a == b for a, b in
736
+ zip(self.__sensor_reading_events, other.sensor_reading_events)) \
737
+ and np.all(self.__pressure_data_raw == other.pressure_data_raw) \
738
+ and np.all(self.__flow_data_raw == other.flow_data_raw) \
739
+ and np.all(self.__demand_data_raw == self.demand_data_raw) \
740
+ and np.all(self.__node_quality_data_raw == other.node_quality_data_raw) \
741
+ and np.all(self.__link_quality_data_raw == other.link_quality_data_raw) \
742
+ and np.all(self.__sensor_readings_time == other.sensor_readings_time) \
743
+ and np.all(self.__pumps_state_data_raw == other.pumps_state_data_raw) \
744
+ and np.all(self.__valves_state_data_raw == other.valves_state_data_raw) \
745
+ and np.all(self.__tanks_volume_data_raw == other.tanks_volume_data_raw) \
746
+ and np.all(self.__surface_species_concentration_raw ==
747
+ other.surface_species_concentration_raw) \
748
+ and np.all(self.__bulk_species_node_concentration_raw ==
749
+ other.bulk_species_node_concentration_raw) \
750
+ and np.all(self.__bulk_species_link_concentration_raw ==
751
+ other.bulk_species_link_concentration_raw) \
752
+ and np.all(self.__pump_energy_usage_data == other.pump_energy_usage_data) \
753
+ and np.all(self.__pump_efficiency_data == other.pump_efficiency_data)
754
+ except Exception as ex:
755
+ warnings.warn(ex.__str__())
756
+ return False
757
+
758
+ def __str__(self) -> str:
759
+ return f"sensor_config: {self.__sensor_config} " + \
760
+ f"frozen_sensor_config: {self.__frozen_sensor_config} " + \
761
+ f"sensor_noise: {self.__sensor_noise} " + \
762
+ f"sensor_reading_events: {self.__sensor_reading_events} " + \
763
+ f"pressure_data_raw: {self.__pressure_data_raw} " + \
764
+ f"flow_data_raw: {self.__flow_data_raw} demand_data_raw: {self.__demand_data_raw} " + \
765
+ f"node_quality_data_raw: {self.__node_quality_data_raw} " + \
766
+ f"link_quality_data_raw: {self.__link_quality_data_raw} " + \
767
+ f"sensor_readings_time: {self.__sensor_readings_time} " + \
768
+ f"pumps_state_data_raw: {self.__pumps_state_data_raw} " + \
769
+ f"valves_state_data_raw: {self.__valves_state_data_raw} " + \
770
+ f"tanks_volume_data_raw: {self.__tanks_volume_data_raw} " + \
771
+ f"surface_species_concentration_raw: {self.__surface_species_concentration_raw} " + \
772
+ f"bulk_species_node_concentration_raw: {self.__bulk_species_node_concentration_raw}" +\
773
+ f" bulk_species_link_concentration_raw: {self.__bulk_species_link_concentration_raw}" +\
774
+ f" pump_efficiency_data: {self.__pump_efficiency_data} " + \
775
+ f"pump_energy_usage_data: {self.__pump_energy_usage_data}"
776
+
777
+ def change_sensor_config(self, sensor_config: SensorConfig) -> None:
778
+ """
779
+ Changes the sensor configuration.
780
+
781
+ Parameters
782
+ ----------
783
+ sensor_config : :class:`~epyt_flow.simulation.sensor_config.SensorConfig`
784
+ New sensor configuration.
785
+ """
786
+ if self.__frozen_sensor_config is True:
787
+ raise RuntimeError("Sensor configuration can not be changed because it is frozen")
788
+ if not isinstance(sensor_config, SensorConfig):
789
+ raise TypeError("'sensor_config' must be an instance of " +
790
+ "'epyt_flow.simulation.SensorConfig' but not of " +
791
+ f"'{type(sensor_config)}'")
792
+
793
+ self.__sensor_config = sensor_config
794
+ self.__init()
795
+
796
+ def change_sensor_noise(self, sensor_noise: SensorNoise) -> None:
797
+ """
798
+ Changes the sensor noise/uncertainty.
799
+
800
+ Parameters
801
+ ----------
802
+ sensor_noise : :class:`~epyt_flow.uncertainty.sensor_noise.SensorNoise`
803
+ New sensor noise/uncertainty specification.
804
+ """
805
+ if not isinstance(sensor_noise, SensorNoise):
806
+ raise TypeError("'sensor_noise' must be an instance of " +
807
+ "'epyt_flow.uncertainty.SensorNoise' but not of " +
808
+ f"'{type(sensor_noise)}'")
809
+
810
+ self.__sensor_noise = sensor_noise
811
+ self.__init()
812
+
813
+ def change_sensor_faults(self, sensor_faults: list[SensorFault]) -> None:
814
+ """
815
+ Changes the sensor faults -- overrides all previous sensor faults!
816
+
817
+ sensor_faults : list[:class:`~epyt_flow.simulation.events.sensor_faults.SensorFault`]
818
+ List of new sensor faults.
819
+ """
820
+ if len(sensor_faults) != 0:
821
+ if any(not isinstance(e, SensorFault) for e in sensor_faults):
822
+ raise TypeError("'sensor_faults' must be a list of " +
823
+ "'epyt_flow.simulation.events.SensorFault' instances")
824
+
825
+ self.__sensor_reading_events = list(filter(lambda e: not isinstance(e, SensorFault),
826
+ self.__sensor_reading_events))
827
+ self.__sensor_reading_events += sensor_faults
828
+ self.__init()
829
+
830
+ def change_sensor_reading_attacks(self,
831
+ sensor_reading_attacks: list[SensorReadingAttack]) -> None:
832
+ """
833
+ Changes the sensor reading attacks -- overrides all previous sensor reading attacks!
834
+
835
+ sensor_reading_attacks : list[:class:`~epyt_flow.simulation.events.sensor_reading_attack.SensorReadingAttack`]
836
+ List of new sensor reading attacks.
837
+ """
838
+ if len(sensor_reading_attacks) != 0:
839
+ if any(not isinstance(e, SensorReadingAttack) for e in sensor_reading_attacks):
840
+ raise TypeError("'sensor_reading_attacks' must be a list of " +
841
+ "'epyt_flow.simulation.events.SensorReadingAttack' instances")
842
+
843
+ self.__sensor_reading_events = list(filter(lambda e: not isinstance(e, SensorReadingAttack),
844
+ self.__sensor_reading_events))
845
+ self.__sensor_reading_events += sensor_reading_attacks
846
+ self.__init()
847
+
848
+ def change_sensor_reading_events(self, sensor_reading_events: list[SensorReadingEvent]) -> None:
849
+ """
850
+ Changes the sensor reading events -- overrides all previous sensor reading events
851
+ (incl. sensor faults)!
852
+
853
+ sensor_reading_events : list[:class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`]
854
+ List of new sensor reading events.
855
+ """
856
+ if len(sensor_reading_events) != 0:
857
+ if any(not isinstance(e, SensorReadingEvent) for e in sensor_reading_events):
858
+ raise TypeError("'sensor_reading_events' must be a list of " +
859
+ "'epyt_flow.simulation.events.SensorReadingEvent' instances")
860
+
861
+ self.__sensor_reading_events = sensor_reading_events
862
+ self.__init()
863
+
864
+ def join(self, other) -> None:
865
+ """
866
+ Joins two :class:`~epyt_flow.simulation.scada_data.scada_data.ScadaData` instances based
867
+ on the sensor reading times. Consequently, **both instances must be equal in their
868
+ sensor reading times**.
869
+ Attributes (i.e. types of sensor readings) that are NOT present in THIS instance
870
+ but in `others` will be added to this instance -- all other attributes are ignored.
871
+ The sensor configuration is updated according to the sensor readings in `other`.
872
+
873
+ Parameters
874
+ ----------
875
+ other : :class:`~epyt_flow.simulation.scada_data.scada_data.ScadaData`
876
+ Other scada data to be concatenated to this data.
877
+ """
878
+ if not isinstance(other, ScadaData):
879
+ raise TypeError("'other' must be an instance of 'ScadaData' " +
880
+ f"but not of '{type(other)}'")
881
+ if self.__frozen_sensor_config != other.frozen_sensor_config:
882
+ raise ValueError("Sensor configurations of both instances must be " +
883
+ "either frozen or not frozen")
884
+ if not np.all(self.__sensor_readings_time == other.sensor_readings_time):
885
+ raise ValueError("Both 'ScadaData' instances must be equal in their " +
886
+ "sensor readings times")
887
+ if any(e1 != e2 for e1, e2 in zip(self.__sensor_reading_events,
888
+ other.sensor_reading_events)):
889
+ raise ValueError("'other' must have the same sensor reading events as this instance!")
890
+ if self.__sensor_config.nodes != other.sensor_config.nodes:
891
+ raise ValueError("Inconsistency in nodes found")
892
+ if self.__sensor_config.links != other.sensor_config.links:
893
+ raise ValueError("Inconsistency in links/pipes found")
894
+ if self.__sensor_config.valves != other.sensor_config.valves:
895
+ raise ValueError("Inconsistency in valves found")
896
+ if self.__sensor_config.pumps != other.sensor_config.pumps:
897
+ raise ValueError("Inconsistency in pumps found")
898
+ if self.__sensor_config.tanks != other.sensor_config.tanks:
899
+ raise ValueError("Inconsistency in tanks found")
900
+ if self.__sensor_config.bulk_species != other.sensor_config.bulk_species:
901
+ raise ValueError("Inconsistency in bulk species found")
902
+ if self.__sensor_config.surface_species != other.sensor_config.surface_species:
903
+ raise ValueError("Inconsistency in surface species found")
904
+
905
+ self.__sensor_readings = None
906
+
907
+ if self.__pressure_data_raw is None and other.pressure_data_raw is not None:
908
+ self.__pressure_data_raw = other.pressure_data_raw
909
+ self.__sensor_config.pressure_sensors = other.sensor_config.pressure_sensors
910
+
911
+ if self.__flow_data_raw is None and other.flow_data_raw is not None:
912
+ self.__flow_data_raw = other.flow_data_raw
913
+ self.__sensor_config.flow_sensors = other.sensor_config.flow_sensors
914
+
915
+ if self.__demand_data_raw is None and other.demand_data_raw is not None:
916
+ self.__demand_data_raw = other.demand_data_raw
917
+ self.__sensor_config.demand_sensors = other.sensor_config.demand_sensors
918
+
919
+ if self.__node_quality_data_raw is None and other.node_quality_data_raw is not None:
920
+ self.__node_quality_data_raw = other.node_quality_data_raw
921
+ self.__sensor_config.quality_node_sensors = other.sensor_config.quality_node_sensors
922
+
923
+ if self.__link_quality_data_raw is None and other.link_quality_data_raw is not None:
924
+ self.__link_quality_data_raw = other.link_quality_data_raw
925
+ self.__sensor_config.quality_node_sensors = other.sensor_config.quality_node_sensors
926
+
927
+ if self.__valves_state_data_raw is None and other.valves_state_data_raw is not None:
928
+ self.__valves_state_data_raw = other.valves_state_data_raw
929
+ self.__sensor_config.valve_state_sensors = other.sensor_config.valve_state_sensors
930
+
931
+ if self.__pumps_state_data_raw is None and other.pumps_state_data_raw is not None:
932
+ self.__pumps_state_data_raw = other.pumps_state_data_raw
933
+ self.__sensor_config.pump_state_sensors = other.sensor_config.pump_state_sensors
934
+
935
+ if self.__tanks_volume_data_raw is None and other.tanks_volume_data_raw is not None:
936
+ self.__tanks_volume_data_raw = other.tanks_volume_data_raw
937
+ self.__sensor_config.tank_volume_sensors = other.sensor_config.tank_volume_sensors
938
+
939
+ if self.__bulk_species_node_concentration_raw is None and \
940
+ other.bulk_species_node_concentration_raw is not None:
941
+ self.__bulk_species_node_concentration_raw = other.bulk_species_node_concentration_raw
942
+ self.__sensor_config.bulk_species_node_sensors = \
943
+ other.sensor_config.bulk_species_node_sensors
944
+
945
+ if self.__bulk_species_link_concentration_raw is None and \
946
+ other.bulk_species_link_concentration_raw is not None:
947
+ self.__bulk_species_link_concentration_raw = other.bulk_species_link_concentration_raw
948
+ self.__sensor_config.bulk_species_link_sensors = \
949
+ other.sensor_config.bulk_species_link_sensors
950
+
951
+ if self.__surface_species_concentration_raw is None and \
952
+ other.surface_species_concentration_raw is not None:
953
+ self.__surface_species_concentration_raw = other.surface_species_concentration_raw
954
+ self.__sensor_config.surface_species_sensors = \
955
+ other.sensor_config.surface_species_sensors
956
+
957
+ if self.__pump_energy_usage_data is None and other.pump_energy_usage_data is not None:
958
+ self.__pump_energy_usage_data = other.pump_energy_usage_data
959
+
960
+ if self.__pump_efficiency_data is None and other.pump_efficiency_data is not None:
961
+ self.__pump_efficiency_data = other.pump_efficiency_data
962
+
963
+ self.__init()
964
+
965
+ def concatenate(self, other) -> None:
966
+ """
967
+ Concatenates two :class:`~epyt_flow.simulation.scada_data.scada_data.ScadaData` instances
968
+ -- i.e. add SCADA data from another given
969
+ :class:`~epyt_flow.simulation.scada_data.scada_data.ScadaData` instance to this one.
970
+
971
+ Note that the two :class:`~epyt_flow.simulation.scada_data.scada_data.ScadaData` instances
972
+ must be the same in all other attributs (e.g. sensor configuration, etc.).
973
+
974
+ Parameters
975
+ ----------
976
+ other : :class:`~epyt_flow.simulation.scada_data.scada_data.ScadaData`
977
+ Other scada data to be concatenated to this data.
978
+ """
979
+ if not isinstance(other, ScadaData):
980
+ raise TypeError(f"'other' must be an instance of 'ScadaData' but not of {type(other)}")
981
+ if self.__sensor_config != other.sensor_config:
982
+ raise ValueError("Sensor configurations must be the same!")
983
+ if self.__frozen_sensor_config != other.frozen_sensor_config:
984
+ raise ValueError("Sensor configurations of both instances must be " +
985
+ "either frozen or not frozen")
986
+ if len(self.__sensor_reading_events) != len(other.sensor_reading_events):
987
+ raise ValueError("'other' must have the same sensor reading events as this instance!")
988
+ if any(e1 != e2 for e1, e2 in zip(self.__sensor_reading_events,
989
+ other.sensor_reading_events)):
990
+ raise ValueError("'other' must have the same sensor reading events as this instance!")
991
+
992
+ self.__sensor_readings = None
993
+
994
+ self.__sensor_readings_time = np.concatenate(
995
+ (self.__sensor_readings_time, other.sensor_readings_time), axis=0)
996
+
997
+ if self.__pressure_data_raw is not None:
998
+ self.__pressure_data_raw = np.concatenate(
999
+ (self.__pressure_data_raw, other.pressure_data_raw), axis=0)
1000
+
1001
+ if self.__flow_data_raw is not None:
1002
+ self.__flow_data_raw = np.concatenate(
1003
+ (self.__flow_data_raw, other.flow_data_raw), axis=0)
1004
+
1005
+ if self.__demand_data_raw is not None:
1006
+ self.__demand_data_raw = np.concatenate(
1007
+ (self.__demand_data_raw, other.demand_data_raw), axis=0)
1008
+
1009
+ if self.__node_quality_data_raw is not None:
1010
+ self.__node_quality_data_raw = np.concatenate(
1011
+ (self.__node_quality_data_raw, other.node_quality_data_raw), axis=0)
1012
+
1013
+ if self.__link_quality_data_raw is not None:
1014
+ self.__link_quality_data_raw = np.concatenate(
1015
+ (self.__link_quality_data_raw, other.link_quality_data_raw), axis=0)
1016
+
1017
+ if self.__pumps_state_data_raw is not None:
1018
+ self.__pumps_state_data_raw = np.concatenate(
1019
+ (self.__pumps_state_data_raw, other.pumps_state_data_raw), axis=0)
1020
+
1021
+ if self.__valves_state_data_raw is not None:
1022
+ self.__valves_state_data_raw = np.concatenate(
1023
+ (self.__valves_state_data_raw, other.valves_state_data_raw), axis=0)
1024
+
1025
+ if self.__tanks_volume_data_raw is not None:
1026
+ self.__tanks_volume_data_raw = np.concatenate(
1027
+ (self.__tanks_volume_data_raw, other.tanks_volume_data_raw), axis=0)
1028
+
1029
+ if self.__surface_species_concentration_raw is not None:
1030
+ self.__surface_species_concentration_raw = np.concatenate(
1031
+ (self.__surface_species_concentration_raw,
1032
+ other.surface_species_concentration_raw),
1033
+ axis=0)
1034
+
1035
+ if self.__bulk_species_node_concentration_raw is not None:
1036
+ self.__bulk_species_node_concentration_raw = np.concatenate(
1037
+ (self.__bulk_species_node_concentration_raw,
1038
+ other.bulk_species_node_concentration_raw),
1039
+ axis=0)
1040
+
1041
+ if self.__bulk_species_link_concentration_raw is not None:
1042
+ self.__bulk_species_link_concentration_raw = np.concatenate(
1043
+ (self.__bulk_species_link_concentration_raw,
1044
+ other.bulk_species_link_concentration_raw),
1045
+ axis=0)
1046
+
1047
+ if self.__pump_energy_usage_data is not None:
1048
+ self.__pump_energy_usage_data = np.concatenate(
1049
+ (self.__pump_energy_usage_data, other.pump_energy_usage_data),
1050
+ axis=0)
1051
+
1052
+ if self.__pump_efficiency_data is not None:
1053
+ self.__pump_efficiency_data = np.concatenate(
1054
+ (self.__pump_efficiency_data, other.pump_efficiency_data),
1055
+ axis=0)
1056
+
1057
+ def get_data(self) -> np.ndarray:
1058
+ """
1059
+ Computes the final sensor readings -- note that those might be subject to
1060
+ given sensor faults and sensor noise/uncertainty.
1061
+
1062
+ Returns
1063
+ -------
1064
+ `numpy.ndarray`
1065
+ Final sensor readings.
1066
+ """
1067
+ # Comute clean sensor readings
1068
+ if self.__frozen_sensor_config is False:
1069
+ args = {"pressures": self.__pressure_data_raw,
1070
+ "flows": self.__flow_data_raw,
1071
+ "demands": self.__demand_data_raw,
1072
+ "nodes_quality": self.__node_quality_data_raw,
1073
+ "links_quality": self.__link_quality_data_raw,
1074
+ "pumps_state": self.__pumps_state_data_raw,
1075
+ "valves_state": self.__valves_state_data_raw,
1076
+ "tanks_volume": self.__tanks_volume_data_raw,
1077
+ "bulk_species_node_concentrations": self.__bulk_species_node_concentration_raw,
1078
+ "bulk_species_link_concentrations": self.__bulk_species_link_concentration_raw,
1079
+ "surface_species_concentrations": self.__surface_species_concentration_raw}
1080
+ sensor_readings = self.__sensor_config.compute_readings(**args)
1081
+ else:
1082
+ data = []
1083
+
1084
+ if self.__pressure_data_raw is not None:
1085
+ data.append(self.__pressure_data_raw)
1086
+ if self.__flow_data_raw is not None:
1087
+ data.append(self.__flow_data_raw)
1088
+ if self.__demand_data_raw is not None:
1089
+ data.append(self.__demand_data_raw)
1090
+ if self.__node_quality_data_raw is not None:
1091
+ data.append(self.__node_quality_data_raw)
1092
+ if self.__link_quality_data_raw is not None:
1093
+ data.append(self.__link_quality_data_raw)
1094
+ if self.__valves_state_data_raw is not None:
1095
+ data.append(self.__valves_state_data_raw)
1096
+ if self.__pumps_state_data_raw is not None:
1097
+ data.append(self.__pumps_state_data_raw)
1098
+ if self.__tanks_volume_data_raw is not None:
1099
+ data.append(self.__tanks_volume_data_raw)
1100
+ if self.__surface_species_concentration_raw is not None:
1101
+ data.append(self.__surface_species_concentration_raw)
1102
+ if self.__bulk_species_node_concentration_raw is not None:
1103
+ data.append(self.__bulk_species_node_concentration_raw)
1104
+ if self.__bulk_species_link_concentration_raw is not None:
1105
+ data.append(self.__bulk_species_link_concentration_raw)
1106
+
1107
+ sensor_readings = np.concatenate(data, axis=1)
1108
+
1109
+ # Apply sensor uncertainties
1110
+ state_sensors_idx = [] # Pump states and valve states are NOT affected!
1111
+ for link_id in self.sensor_config.pump_state_sensors:
1112
+ state_sensors_idx.append(
1113
+ self.__sensor_config.get_index_of_reading(pump_state_sensor=link_id))
1114
+ for link_id in self.sensor_config.valve_state_sensors:
1115
+ state_sensors_idx.append(
1116
+ self.__sensor_config.get_index_of_reading(valve_state_sensor=link_id))
1117
+
1118
+ mask = np.ones(sensor_readings.shape[1], dtype=bool)
1119
+ mask[state_sensors_idx] = False
1120
+
1121
+ sensor_readings[:, mask] = self.__apply_sensor_noise(sensor_readings[:, mask])
1122
+
1123
+ # Apply sensor faults
1124
+ for idx, f in self.__apply_sensor_reading_events:
1125
+ sensor_readings[:, idx] = f(sensor_readings[:, idx], self.__sensor_readings_time)
1126
+
1127
+ self.__sensor_readings = deepcopy(sensor_readings)
1128
+
1129
+ return sensor_readings
1130
+
1131
+ def get_data_pressures(self, sensor_locations: list[str] = None) -> np.ndarray:
1132
+ """
1133
+ Gets the final pressure sensor readings -- note that those might be subject to
1134
+ given sensor faults and sensor noise/uncertainty.
1135
+
1136
+ Parameters
1137
+ ----------
1138
+ sensor_locations : `list[str]`, optional
1139
+ Existing pressure sensor locations for which the sensor readings are requested.
1140
+ If None, the readings from all pressure sensors are returned.
1141
+
1142
+ The default is None.
1143
+
1144
+ Returns
1145
+ -------
1146
+ `numpy.ndarray`
1147
+ Pressure sensor readings.
1148
+ """
1149
+ if self.__sensor_config.pressure_sensors == []:
1150
+ raise ValueError("No pressure sensors set")
1151
+ if sensor_locations is not None:
1152
+ if not isinstance(sensor_locations, list):
1153
+ raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
1154
+ f"but not of '{type(sensor_locations)}'")
1155
+ if any(s_id not in self.__sensor_config.pressure_sensors for s_id in sensor_locations):
1156
+ raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
1157
+ "sensors in 'sensor_locations' must be set in the current " +
1158
+ "pressure sensor configuration")
1159
+ else:
1160
+ sensor_locations = self.__sensor_config.pressure_sensors
1161
+
1162
+ if self.__sensor_readings is None:
1163
+ self.get_data()
1164
+
1165
+ idx = [self.__sensor_config.get_index_of_reading(pressure_sensor=s_id)
1166
+ for s_id in sensor_locations]
1167
+ return self.__sensor_readings[:, idx]
1168
+
1169
+ def get_data_flows(self, sensor_locations: list[str] = None) -> np.ndarray:
1170
+ """
1171
+ Gets the final flow sensor readings -- note that those might be subject to
1172
+ given sensor faults and sensor noise/uncertainty.
1173
+
1174
+ Parameters
1175
+ ----------
1176
+ sensor_locations : `list[str]`, optional
1177
+ Existing flow sensor locations for which the sensor readings are requested.
1178
+ If None, the readings from all flow sensors are returned.
1179
+
1180
+ The default is None.
1181
+
1182
+ Returns
1183
+ -------
1184
+ `numpy.ndarray`
1185
+ Flow sensor readings.
1186
+ """
1187
+ if self.__sensor_config.flow_sensors == []:
1188
+ raise ValueError("No flow sensors set")
1189
+ if sensor_locations is not None:
1190
+ if not isinstance(sensor_locations, list):
1191
+ raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
1192
+ f"but not of '{type(sensor_locations)}'")
1193
+ if any(s_id not in self.__sensor_config.flow_sensors for s_id in sensor_locations):
1194
+ raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
1195
+ "sensors in 'sensor_locations' must be set in the current " +
1196
+ "flow sensor configuration")
1197
+ else:
1198
+ sensor_locations = self.__sensor_config.flow_sensors
1199
+
1200
+ if self.__sensor_readings is None:
1201
+ self.get_data()
1202
+
1203
+ idx = [self.__sensor_config.get_index_of_reading(flow_sensor=s_id)
1204
+ for s_id in sensor_locations]
1205
+ return self.__sensor_readings[:, idx]
1206
+
1207
+ def get_data_demands(self, sensor_locations: list[str] = None) -> np.ndarray:
1208
+ """
1209
+ Gets the final demand sensor readings -- note that those might be subject to
1210
+ given sensor faults and sensor noise/uncertainty.
1211
+
1212
+ Parameters
1213
+ ----------
1214
+ sensor_locations : `list[str]`, optional
1215
+ Existing demand sensor locations for which the sensor readings are requested.
1216
+ If None, the readings from all demand sensors are returned.
1217
+
1218
+ The default is None.
1219
+
1220
+ Returns
1221
+ -------
1222
+ `numpy.ndarray`
1223
+ Demand sensor readings.
1224
+ """
1225
+ if self.__sensor_config.demand_sensors == []:
1226
+ raise ValueError("No demand sensors set")
1227
+ if sensor_locations is not None:
1228
+ if not isinstance(sensor_locations, list):
1229
+ raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
1230
+ f"but not of '{type(sensor_locations)}'")
1231
+ if any(s_id not in self.__sensor_config.demand_sensors for s_id in sensor_locations):
1232
+ raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
1233
+ "sensors in 'sensor_locations' must be set in the current " +
1234
+ "demand sensor configuration")
1235
+ else:
1236
+ sensor_locations = self.__sensor_config.demand_sensors
1237
+
1238
+ if self.__sensor_readings is None:
1239
+ self.get_data()
1240
+
1241
+ idx = [self.__sensor_config.get_index_of_reading(demand_sensor=s_id)
1242
+ for s_id in sensor_locations]
1243
+ return self.__sensor_readings[:, idx]
1244
+
1245
+ def get_data_nodes_quality(self, sensor_locations: list[str] = None) -> np.ndarray:
1246
+ """
1247
+ Gets the final node quality sensor readings -- note that those might be subject to
1248
+ given sensor faults and sensor noise/uncertainty.
1249
+
1250
+ Parameters
1251
+ ----------
1252
+ sensor_locations : `list[str]`, optional
1253
+ Existing node quality sensor locations for which the sensor readings are requested.
1254
+ If None, the readings from all node quality sensors are returned.
1255
+
1256
+ The default is None.
1257
+
1258
+ Returns
1259
+ -------
1260
+ `numpy.ndarray`
1261
+ Node quality sensor readings.
1262
+ """
1263
+ if self.__sensor_config.quality_node_sensors == []:
1264
+ raise ValueError("No node quality sensors set")
1265
+ if sensor_locations is not None:
1266
+ if not isinstance(sensor_locations, list):
1267
+ raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
1268
+ f"but not of '{type(sensor_locations)}'")
1269
+ if any(s_id not in self.__sensor_config.quality_node_sensors
1270
+ for s_id in sensor_locations):
1271
+ raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
1272
+ "sensors in 'sensor_locations' must be set in the current " +
1273
+ "node quality sensor configuration")
1274
+ else:
1275
+ sensor_locations = self.__sensor_config.quality_node_sensors
1276
+
1277
+ if self.__sensor_readings is None:
1278
+ self.get_data()
1279
+
1280
+ idx = [self.__sensor_config.get_index_of_reading(node_quality_sensor=s_id)
1281
+ for s_id in sensor_locations]
1282
+ return self.__sensor_readings[:, idx]
1283
+
1284
+ def get_data_links_quality(self, sensor_locations: list[str] = None) -> np.ndarray:
1285
+ """
1286
+ Gets the final link quality sensor readings -- note that those might be subject to
1287
+ given sensor faults and sensor noise/uncertainty.
1288
+
1289
+ Parameters
1290
+ ----------
1291
+ sensor_locations : `list[str]`, optional
1292
+ Existing link quality sensor locations for which the sensor readings are requested.
1293
+ If None, the readings from all link quality sensors are returned.
1294
+
1295
+ The default is None.
1296
+
1297
+ Returns
1298
+ -------
1299
+ `numpy.ndarray`
1300
+ Link quality sensor readings.
1301
+ """
1302
+ if self.__sensor_config.quality_link_sensors == []:
1303
+ raise ValueError("No link quality sensors set")
1304
+ if sensor_locations is not None:
1305
+ if not isinstance(sensor_locations, list):
1306
+ raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
1307
+ f"but not of '{type(sensor_locations)}'")
1308
+ if any(s_id not in self.__sensor_config.quality_link_sensors
1309
+ for s_id in sensor_locations):
1310
+ raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
1311
+ "sensors in 'sensor_locations' must be set in the current " +
1312
+ "link quality sensor configuration")
1313
+ else:
1314
+ sensor_locations = self.__sensor_config.quality_link_sensors
1315
+
1316
+ if self.__sensor_readings is None:
1317
+ self.get_data()
1318
+
1319
+ idx = [self.__sensor_config.get_index_of_reading(link_quality_sensor=s_id)
1320
+ for s_id in sensor_locations]
1321
+ return self.__sensor_readings[:, idx]
1322
+
1323
+ def get_data_pumps_state(self, sensor_locations: list[str] = None) -> np.ndarray:
1324
+ """
1325
+ Gets the final pump state sensor readings -- note that those might be subject to
1326
+ given sensor faults and sensor noise/uncertainty.
1327
+
1328
+ Parameters
1329
+ ----------
1330
+ sensor_locations : `list[str]`, optional
1331
+ Existing pump state sensor locations for which the sensor readings are requested.
1332
+ If None, the readings from all pump state sensors are returned.
1333
+
1334
+ The default is None.
1335
+
1336
+ Returns
1337
+ -------
1338
+ `numpy.ndarray`
1339
+ Pump state sensor readings.
1340
+ """
1341
+ if self.__sensor_config.pump_state_sensors == []:
1342
+ raise ValueError("No pump state sensors set")
1343
+ if sensor_locations is not None:
1344
+ if not isinstance(sensor_locations, list):
1345
+ raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
1346
+ f"but not of '{type(sensor_locations)}'")
1347
+ if any(s_id not in self.__sensor_config.pump_state_sensors
1348
+ for s_id in sensor_locations):
1349
+ raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
1350
+ "sensors in 'sensor_locations' must be set in the current " +
1351
+ "pump state sensor configuration")
1352
+ else:
1353
+ sensor_locations = self.__sensor_config.pump_state_sensors
1354
+
1355
+ if self.__sensor_readings is None:
1356
+ self.get_data()
1357
+
1358
+ idx = [self.__sensor_config.get_index_of_reading(pump_state_sensor=s_id)
1359
+ for s_id in sensor_locations]
1360
+ return self.__sensor_readings[:, idx]
1361
+
1362
+ def get_data_valves_state(self, sensor_locations: list[str] = None) -> np.ndarray:
1363
+ """
1364
+ Gets the final valve state sensor readings -- note that those might be subject to
1365
+ given sensor faults and sensor noise/uncertainty.
1366
+
1367
+ Parameters
1368
+ ----------
1369
+ sensor_locations : `list[str]`, optional
1370
+ Existing valve state sensor locations for which the sensor readings are requested.
1371
+ If None, the readings from all valve state sensors are returned.
1372
+
1373
+ The default is None.
1374
+
1375
+ Returns
1376
+ -------
1377
+ `numpy.ndarray`
1378
+ Valve state sensor readings.
1379
+ """
1380
+ if self.__sensor_config.valve_state_sensors == []:
1381
+ raise ValueError("No valve state sensors set")
1382
+ if sensor_locations is not None:
1383
+ if not isinstance(sensor_locations, list):
1384
+ raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
1385
+ f"but not of '{type(sensor_locations)}'")
1386
+ if any(s_id not in self.__sensor_config.valve_state_sensors
1387
+ for s_id in sensor_locations):
1388
+ raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
1389
+ "sensors in 'sensor_locations' must be set in the current " +
1390
+ "valve state sensor configuration")
1391
+ else:
1392
+ sensor_locations = self.__sensor_config.valve_state_sensors
1393
+
1394
+ if self.__sensor_readings is None:
1395
+ self.get_data()
1396
+
1397
+ idx = [self.__sensor_config.get_index_of_reading(valve_state_sensor=s_id)
1398
+ for s_id in sensor_locations]
1399
+ return self.__sensor_readings[:, idx]
1400
+
1401
+ def get_data_tanks_water_volume(self, sensor_locations: list[str] = None) -> np.ndarray:
1402
+ """
1403
+ Gets the final water tanks volume sensor readings -- note that those might be subject to
1404
+ given sensor faults and sensor noise/uncertainty.
1405
+
1406
+ Parameters
1407
+ ----------
1408
+ sensor_locations : `list[str]`, optional
1409
+ Existing flow sensor locations for which the sensor readings are requested.
1410
+ If None, the readings from all water tanks volume sensors are returned.
1411
+
1412
+ The default is None.
1413
+
1414
+ Returns
1415
+ -------
1416
+ `numpy.ndarray`
1417
+ Water tanks volume sensor readings.
1418
+ """
1419
+ if self.__sensor_config.tank_volume_sensors == []:
1420
+ raise ValueError("No tank volume sensors set")
1421
+ if sensor_locations is not None:
1422
+ if not isinstance(sensor_locations, list):
1423
+ raise TypeError("'sensor_locations' must be an instance of 'list[str]' " +
1424
+ f"but not of '{type(sensor_locations)}'")
1425
+ if any(s_id not in self.__sensor_config.tank_volume_sensors
1426
+ for s_id in sensor_locations):
1427
+ raise ValueError("Invalid sensor ID in 'sensor_locations' -- note that all " +
1428
+ "sensors in 'sensor_locations' must be set in the current " +
1429
+ "water tanks volume sensor configuration")
1430
+ else:
1431
+ sensor_locations = self.__sensor_config.tank_volume_sensors
1432
+
1433
+ if self.__sensor_readings is None:
1434
+ self.get_data()
1435
+
1436
+ idx = [self.__sensor_config.get_index_of_reading(tank_volume_sensor=s_id)
1437
+ for s_id in sensor_locations]
1438
+ return self.__sensor_readings[:, idx]
1439
+
1440
+ def get_data_surface_species_concentration(self,
1441
+ surface_species_sensor_locations: dict = None
1442
+ ) -> np.ndarray:
1443
+ """
1444
+ Gets the final surface species concentration sensor readings --
1445
+ note that those might be subject to given sensor faults and sensor noise/uncertainty.
1446
+
1447
+ Parameters
1448
+ ----------
1449
+ surface_species_sensor_locations : `dict`, optional
1450
+ Existing surface species concentration sensors (species ID and link/pipe IDs) for which
1451
+ the sensor readings are requested.
1452
+ If None, the readings from all surface species concentration sensors are returned.
1453
+
1454
+ The default is None.
1455
+
1456
+ Returns
1457
+ -------
1458
+ `numpy.ndarray`
1459
+ Surface species concentration sensor readings.
1460
+ """
1461
+ if self.__sensor_config.surface_species_sensors == {}:
1462
+ raise ValueError("No surface species sensors set")
1463
+ if surface_species_sensor_locations is not None:
1464
+ if not isinstance(surface_species_sensor_locations, dict):
1465
+ raise TypeError("'surface_species_sensor_locations' must be an instance of 'dict'" +
1466
+ f" but not of '{type(surface_species_sensor_locations)}'")
1467
+ for species_id in surface_species_sensor_locations:
1468
+ if species_id not in self.__sensor_config.surface_species_sensors:
1469
+ raise ValueError(f"Species '{species_id}' is not included in the " +
1470
+ "sensor configuration")
1471
+
1472
+ my_surface_species_sensor_locations = \
1473
+ self.__sensor_config.surface_species_sensors[species_id]
1474
+ for sensor_id in surface_species_sensor_locations[species_id]:
1475
+ if sensor_id not in my_surface_species_sensor_locations:
1476
+ raise ValueError(f"Link '{sensor_id}' is not included in the " +
1477
+ f"sensor configuration for species '{species_id}'")
1478
+ else:
1479
+ surface_species_sensor_locations = self.__sensor_config.surface_species_sensors
1480
+
1481
+ if self.__sensor_readings is None:
1482
+ self.get_data()
1483
+
1484
+ idx = [self.__sensor_config.get_index_of_reading(
1485
+ surface_species_sensor=(species_id, link_id))
1486
+ for species_id in surface_species_sensor_locations
1487
+ for link_id in surface_species_sensor_locations[species_id]]
1488
+ return self.__sensor_readings[:, idx]
1489
+
1490
+ def get_data_bulk_species_node_concentration(self,
1491
+ bulk_species_sensor_locations: dict = None
1492
+ ) -> np.ndarray:
1493
+ """
1494
+ Gets the final bulk species node concentration sensor readings --
1495
+ note that those might be subject to given sensor faults and sensor noise/uncertainty.
1496
+
1497
+ Parameters
1498
+ ----------
1499
+ bulk_species_sensor_locations : `dict`, optional
1500
+ Existing bulk species concentration sensors (species ID and node IDs) for which
1501
+ the sensor readings are requested.
1502
+ If None, the readings from all bulk species node concentration sensors are returned.
1503
+
1504
+ The default is None.
1505
+
1506
+ Returns
1507
+ -------
1508
+ `numpy.ndarray`
1509
+ Bulk species concentration sensor readings.
1510
+ """
1511
+ if self.__sensor_config.bulk_species_node_sensors == {}:
1512
+ raise ValueError("No bulk species node sensors set")
1513
+ if bulk_species_sensor_locations is not None:
1514
+ if not isinstance(bulk_species_sensor_locations, dict):
1515
+ raise TypeError("'bulk_species_sensor_locations' must be an instance of 'dict'" +
1516
+ f" but not of '{type(bulk_species_sensor_locations)}'")
1517
+ for species_id in bulk_species_sensor_locations:
1518
+ if species_id not in self.__sensor_config.bulk_species_sensors:
1519
+ raise ValueError(f"Species '{species_id}' is not included in the " +
1520
+ "sensor configuration")
1521
+
1522
+ my_bulk_species_sensor_locations = \
1523
+ self.__sensor_config.bulk_species_node_sensors[species_id]
1524
+ for sensor_id in bulk_species_sensor_locations[species_id]:
1525
+ if sensor_id not in my_bulk_species_sensor_locations:
1526
+ raise ValueError(f"Link '{sensor_id}' is not included in the " +
1527
+ f"sensor configuration for species '{species_id}'")
1528
+ else:
1529
+ bulk_species_sensor_locations = self.__sensor_config.bulk_species_node_sensors
1530
+
1531
+ if self.__sensor_readings is None:
1532
+ self.get_data()
1533
+
1534
+ idx = [self.__sensor_config.get_index_of_reading(
1535
+ bulk_species_node_sensor=(species_id, node_id))
1536
+ for species_id in bulk_species_sensor_locations
1537
+ for node_id in bulk_species_sensor_locations[species_id]]
1538
+ return self.__sensor_readings[:, idx]
1539
+
1540
+ def get_data_bulk_species_link_concentration(self,
1541
+ bulk_species_sensor_locations: dict = None
1542
+ ) -> np.ndarray:
1543
+ """
1544
+ Gets the final bulk species link/pipe concentration sensor readings --
1545
+ note that those might be subject to given sensor faults and sensor noise/uncertainty.
1546
+
1547
+ Parameters
1548
+ ----------
1549
+ bulk_species_sensor_locations : `dict`, optional
1550
+ Existing bulk species concentration sensors (species ID and link/pipe IDs) for which
1551
+ the sensor readings are requested.
1552
+ If None, the readings from all bulk species concentration link/pipe sensors
1553
+ are returned.
1554
+
1555
+ The default is None.
1556
+
1557
+ Returns
1558
+ -------
1559
+ `numpy.ndarray`
1560
+ Bulk species concentration sensor readings.
1561
+ """
1562
+ if self.__sensor_config.bulk_species_link_sensors == {}:
1563
+ raise ValueError("No bulk species link/pipe sensors set")
1564
+ if bulk_species_sensor_locations is not None:
1565
+ if not isinstance(bulk_species_sensor_locations, dict):
1566
+ raise TypeError("'bulk_species_sensor_locations' must be an instance of 'dict'" +
1567
+ f" but not of '{type(bulk_species_sensor_locations)}'")
1568
+ for species_id in bulk_species_sensor_locations:
1569
+ if species_id not in self.__sensor_config.bulk_species_link_sensors:
1570
+ raise ValueError(f"Species '{species_id}' is not included in the " +
1571
+ "sensor configuration")
1572
+
1573
+ my_bulk_species_sensor_locations = \
1574
+ self.__sensor_config.bulk_species_link_sensors[species_id]
1575
+ for sensor_id in bulk_species_sensor_locations[species_id]:
1576
+ if sensor_id not in my_bulk_species_sensor_locations:
1577
+ raise ValueError(f"Link '{sensor_id}' is not included in the " +
1578
+ f"sensor configuration for species '{species_id}'")
1579
+ else:
1580
+ bulk_species_sensor_locations = self.__sensor_config.bulk_species_link_sensors
1581
+
1582
+ if self.__sensor_readings is None:
1583
+ self.get_data()
1584
+
1585
+ idx = [self.__sensor_config.get_index_of_reading(
1586
+ bulk_species_link_sensor=(species_id, node_id))
1587
+ for species_id in bulk_species_sensor_locations
1588
+ for node_id in bulk_species_sensor_locations[species_id]]
1589
+ return self.__sensor_readings[:, idx]