epyt-flow 0.9.0__py3-none-any.whl → 0.11.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.
- epyt_flow/VERSION +1 -1
- epyt_flow/data/networks.py +27 -14
- epyt_flow/gym/control_gyms.py +8 -0
- epyt_flow/gym/scenario_control_env.py +17 -4
- epyt_flow/metrics.py +5 -0
- epyt_flow/models/event_detector.py +5 -0
- epyt_flow/models/sensor_interpolation_detector.py +5 -0
- epyt_flow/serialization.py +5 -0
- epyt_flow/simulation/__init__.py +0 -1
- epyt_flow/simulation/events/actuator_events.py +7 -1
- epyt_flow/simulation/events/sensor_reading_attack.py +16 -3
- epyt_flow/simulation/events/sensor_reading_event.py +18 -3
- epyt_flow/simulation/scada/__init__.py +3 -1
- epyt_flow/simulation/scada/advanced_control.py +6 -2
- epyt_flow/simulation/scada/complex_control.py +625 -0
- epyt_flow/simulation/scada/custom_control.py +134 -0
- epyt_flow/simulation/scada/scada_data.py +547 -8
- epyt_flow/simulation/scada/simple_control.py +317 -0
- epyt_flow/simulation/scenario_config.py +87 -26
- epyt_flow/simulation/scenario_simulator.py +865 -51
- epyt_flow/simulation/sensor_config.py +34 -2
- epyt_flow/topology.py +16 -0
- epyt_flow/uncertainty/model_uncertainty.py +80 -62
- epyt_flow/uncertainty/sensor_noise.py +15 -4
- epyt_flow/uncertainty/uncertainties.py +71 -18
- epyt_flow/uncertainty/utils.py +40 -13
- epyt_flow/utils.py +15 -1
- epyt_flow/visualization/__init__.py +2 -0
- epyt_flow/{simulation → visualization}/scenario_visualizer.py +429 -586
- epyt_flow/visualization/visualization_utils.py +611 -0
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.11.0.dist-info}/LICENSE +1 -1
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.11.0.dist-info}/METADATA +18 -6
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.11.0.dist-info}/RECORD +35 -30
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.11.0.dist-info}/WHEEL +1 -1
- {epyt_flow-0.9.0.dist-info → epyt_flow-0.11.0.dist-info}/top_level.txt +0 -0
|
@@ -22,6 +22,7 @@ from ..sensor_config import SensorConfig, is_flowunit_simetric, massunit_to_str,
|
|
|
22
22
|
from ..events import SensorFault, SensorReadingAttack, SensorReadingEvent
|
|
23
23
|
from ...uncertainty import SensorNoise
|
|
24
24
|
from ...serialization import serializable, Serializable, SCADA_DATA_ID
|
|
25
|
+
from ...topology import NetworkTopology
|
|
25
26
|
from ...utils import plot_timeseries_data
|
|
26
27
|
|
|
27
28
|
|
|
@@ -127,6 +128,8 @@ class ScadaData(Serializable):
|
|
|
127
128
|
will be stored -- this usually leads to a significant reduction in memory consumption.
|
|
128
129
|
|
|
129
130
|
The default is False.
|
|
131
|
+
network_topo : :class:`~epyt_flow.topology.NetworkTopology`
|
|
132
|
+
Topology of the water distribution network.
|
|
130
133
|
"""
|
|
131
134
|
def __init__(self, sensor_config: SensorConfig, sensor_readings_time: np.ndarray,
|
|
132
135
|
pressure_data_raw: Union[np.ndarray, bsr_array] = None,
|
|
@@ -148,7 +151,17 @@ class ScadaData(Serializable):
|
|
|
148
151
|
sensor_reading_attacks: list[SensorReadingAttack] = [],
|
|
149
152
|
sensor_reading_events: list[SensorReadingEvent] = [],
|
|
150
153
|
sensor_noise: SensorNoise = None, frozen_sensor_config: bool = False,
|
|
154
|
+
network_topo: NetworkTopology = None,
|
|
151
155
|
**kwds):
|
|
156
|
+
if network_topo is not None:
|
|
157
|
+
if not isinstance(network_topo, NetworkTopology):
|
|
158
|
+
raise TypeError("'network_topo' must be an instance of " +
|
|
159
|
+
"'epyt_flow.topology.NetworkTopology' but not " +
|
|
160
|
+
f"of '{type(network_topo)}'")
|
|
161
|
+
else:
|
|
162
|
+
warnings.warn("You are loading a SCADA data instance that was created with an " +
|
|
163
|
+
"outdated version of EPyT-Flow. Future releases will require " +
|
|
164
|
+
"'network_topo' != None. Please upgrade!")
|
|
152
165
|
if not isinstance(sensor_config, SensorConfig):
|
|
153
166
|
raise TypeError("'sensor_config' must be an instance of " +
|
|
154
167
|
"'epyt_flow.simulation.SensorConfig' but not of " +
|
|
@@ -345,6 +358,7 @@ class ScadaData(Serializable):
|
|
|
345
358
|
if pumps_efficiency_data_raw.shape[0] != n_time_steps:
|
|
346
359
|
__raise_shape_mismatch("pumps_efficiency_data_raw")
|
|
347
360
|
|
|
361
|
+
self.__network_topo = network_topo
|
|
348
362
|
self.__sensor_config = sensor_config
|
|
349
363
|
self.__sensor_noise = sensor_noise
|
|
350
364
|
self.__sensor_reading_events = sensor_faults + sensor_reading_attacks + \
|
|
@@ -1030,7 +1044,8 @@ class ScadaData(Serializable):
|
|
|
1030
1044
|
surface_species_mass_unit=new_surface_species_mass_unit,
|
|
1031
1045
|
surface_species_area_unit=new_surface_species_area_unit)
|
|
1032
1046
|
|
|
1033
|
-
return ScadaData(
|
|
1047
|
+
return ScadaData(network_topo=self.network_topo,
|
|
1048
|
+
sensor_config=sensor_config,
|
|
1034
1049
|
sensor_readings_time=self.sensor_readings_time,
|
|
1035
1050
|
sensor_reading_events=self.sensor_reading_events,
|
|
1036
1051
|
sensor_noise=self.sensor_noise,
|
|
@@ -1049,6 +1064,18 @@ class ScadaData(Serializable):
|
|
|
1049
1064
|
bulk_species_link_concentration_raw=bulk_species_link_concentrations,
|
|
1050
1065
|
surface_species_concentration_raw=surface_species_concentrations)
|
|
1051
1066
|
|
|
1067
|
+
@property
|
|
1068
|
+
def network_topo(self) -> NetworkTopology:
|
|
1069
|
+
"""
|
|
1070
|
+
Returns the topology of the water distribution network.
|
|
1071
|
+
|
|
1072
|
+
Returns
|
|
1073
|
+
-------
|
|
1074
|
+
:class:`epyt_flow.topology.NetworkTopology`
|
|
1075
|
+
Topology of the network.
|
|
1076
|
+
"""
|
|
1077
|
+
return deepcopy(self.__network_topo)
|
|
1078
|
+
|
|
1052
1079
|
@property
|
|
1053
1080
|
def frozen_sensor_config(self) -> bool:
|
|
1054
1081
|
"""
|
|
@@ -1314,7 +1341,7 @@ class ScadaData(Serializable):
|
|
|
1314
1341
|
"""
|
|
1315
1342
|
return deepcopy(self.__pumps_efficiency_data_raw)
|
|
1316
1343
|
|
|
1317
|
-
def __map_sensor_to_idx(self, sensor_type: int, sensor_id: str) -> int:
|
|
1344
|
+
def __map_sensor_to_idx(self, sensor_type: int, sensor_id: Union[str, tuple[str, str]]) -> int:
|
|
1318
1345
|
if sensor_type == SENSOR_TYPE_NODE_PRESSURE:
|
|
1319
1346
|
return self.__sensor_config.get_index_of_reading(pressure_sensor=sensor_id)
|
|
1320
1347
|
elif sensor_type == SENSOR_TYPE_NODE_QUALITY:
|
|
@@ -1351,13 +1378,31 @@ class ScadaData(Serializable):
|
|
|
1351
1378
|
|
|
1352
1379
|
self.__apply_sensor_reading_events = []
|
|
1353
1380
|
for sensor_event in self.__sensor_reading_events:
|
|
1354
|
-
|
|
1355
|
-
|
|
1381
|
+
sensor_id = sensor_event.sensor_id
|
|
1382
|
+
if sensor_event.sensor_type == SENSOR_TYPE_NODE_BULK_SPECIES:
|
|
1383
|
+
for species_id, node_sensors_id in self.__sensor_config.bulk_species_node_sensors.items():
|
|
1384
|
+
if sensor_id in node_sensors_id:
|
|
1385
|
+
idx = self.__map_sensor_to_idx(sensor_event.sensor_type, (species_id, sensor_id))
|
|
1386
|
+
self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
|
|
1387
|
+
elif sensor_event.sensor_type == SENSOR_TYPE_LINK_BULK_SPECIES:
|
|
1388
|
+
for species_id, link_sensors_id in self.__sensor_config.bulk_species_link_sensors.items():
|
|
1389
|
+
if sensor_id in link_sensors_id:
|
|
1390
|
+
idx = self.__map_sensor_to_idx(sensor_event.sensor_type, (species_id, sensor_id))
|
|
1391
|
+
self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
|
|
1392
|
+
elif sensor_event.sensor_type == SENSOR_TYPE_SURFACE_SPECIES:
|
|
1393
|
+
for species_id, link_sensors_id in self.__sensor_config.surface_species_sensors.items():
|
|
1394
|
+
if sensor_id in link_sensors_id:
|
|
1395
|
+
idx = self.__map_sensor_to_idx(sensor_event.sensor_type, (species_id, sensor_id))
|
|
1396
|
+
self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
|
|
1397
|
+
else:
|
|
1398
|
+
idx = self.__map_sensor_to_idx(sensor_event.sensor_type, sensor_event.sensor_id)
|
|
1399
|
+
self.__apply_sensor_reading_events.append((idx, sensor_event.apply))
|
|
1356
1400
|
|
|
1357
1401
|
self.__sensor_readings = None
|
|
1358
1402
|
|
|
1359
1403
|
def get_attributes(self) -> dict:
|
|
1360
|
-
attr = {"
|
|
1404
|
+
attr = {"network_topo": self.__network_topo,
|
|
1405
|
+
"sensor_config": self.__sensor_config,
|
|
1361
1406
|
"frozen_sensor_config": self.__frozen_sensor_config,
|
|
1362
1407
|
"sensor_noise": self.__sensor_noise,
|
|
1363
1408
|
"sensor_reading_events": self.__sensor_reading_events,
|
|
@@ -1508,7 +1553,8 @@ class ScadaData(Serializable):
|
|
|
1508
1553
|
raise TypeError(f"Can not compare 'ScadaData' instance to '{type(other)}' instance")
|
|
1509
1554
|
|
|
1510
1555
|
try:
|
|
1511
|
-
return self.
|
|
1556
|
+
return self.__network_topo == other.network_topo \
|
|
1557
|
+
and self.__sensor_config == other.sensor_config \
|
|
1512
1558
|
and self.__frozen_sensor_config == other.frozen_sensor_config \
|
|
1513
1559
|
and self.__sensor_noise == other.sensor_noise \
|
|
1514
1560
|
and all(a == b for a, b in
|
|
@@ -1536,7 +1582,7 @@ class ScadaData(Serializable):
|
|
|
1536
1582
|
return False
|
|
1537
1583
|
|
|
1538
1584
|
def __str__(self) -> str:
|
|
1539
|
-
return f"sensor_config: {self.__sensor_config} " + \
|
|
1585
|
+
return f"network_topo: {self.__network_topo} sensor_config: {self.__sensor_config} " + \
|
|
1540
1586
|
f"frozen_sensor_config: {self.__frozen_sensor_config} " + \
|
|
1541
1587
|
f"sensor_noise: {self.__sensor_noise} " + \
|
|
1542
1588
|
f"sensor_reading_events: {self.__sensor_reading_events} " + \
|
|
@@ -1734,7 +1780,7 @@ class ScadaData(Serializable):
|
|
|
1734
1780
|
if self.__pumps_efficiency_data_raw is not None:
|
|
1735
1781
|
pumps_efficiency_data_raw = self.__pumps_efficiency_data_raw[start_idx:end_idx, :]
|
|
1736
1782
|
|
|
1737
|
-
return ScadaData(sensor_config=self.sensor_config,
|
|
1783
|
+
return ScadaData(network_topo=self.network_topo, sensor_config=self.sensor_config,
|
|
1738
1784
|
sensor_readings_time=self.sensor_readings_time[start_idx:end_idx],
|
|
1739
1785
|
frozen_sensor_config=self.frozen_sensor_config,
|
|
1740
1786
|
sensor_noise=self.sensor_noise,
|
|
@@ -1772,6 +1818,8 @@ class ScadaData(Serializable):
|
|
|
1772
1818
|
if not isinstance(other, ScadaData):
|
|
1773
1819
|
raise TypeError("'other' must be an instance of 'ScadaData' " +
|
|
1774
1820
|
f"but not of '{type(other)}'")
|
|
1821
|
+
if self.__network_topo != other.network_topo:
|
|
1822
|
+
raise ValueError("Network topology must be the same in both instances")
|
|
1775
1823
|
if self.__frozen_sensor_config != other.frozen_sensor_config:
|
|
1776
1824
|
raise ValueError("Sensor configurations of both instances must be " +
|
|
1777
1825
|
"either frozen or not frozen")
|
|
@@ -1874,6 +1922,8 @@ class ScadaData(Serializable):
|
|
|
1874
1922
|
"""
|
|
1875
1923
|
if not isinstance(other, ScadaData):
|
|
1876
1924
|
raise TypeError(f"'other' must be an instance of 'ScadaData' but not of {type(other)}")
|
|
1925
|
+
if self.__network_topo != other.network_topo:
|
|
1926
|
+
raise ValueError("Network topology must be the same")
|
|
1877
1927
|
if self.__sensor_config != other.sensor_config:
|
|
1878
1928
|
raise ValueError("Sensor configurations must be the same!")
|
|
1879
1929
|
if self.__frozen_sensor_config != other.frozen_sensor_config:
|
|
@@ -1950,11 +2000,91 @@ class ScadaData(Serializable):
|
|
|
1950
2000
|
(self.__pumps_efficiency_data_raw, other.pumps_efficiency_data_raw),
|
|
1951
2001
|
axis=0)
|
|
1952
2002
|
|
|
2003
|
+
def topo_adj_matrix(self) -> bsr_array:
|
|
2004
|
+
"""
|
|
2005
|
+
Returns the adjacency matrix of the network.
|
|
2006
|
+
|
|
2007
|
+
Nodes are ordered according to EPANET.
|
|
2008
|
+
|
|
2009
|
+
Returns
|
|
2010
|
+
-------
|
|
2011
|
+
`scipy.bsr_array <https://docs.scipy.org/doc/scipy/reference/generated/scipy.sparse.bsr_array.html>`_
|
|
2012
|
+
Adjacency matrix as a sparse array of shape [num_nodes, num_nodes].
|
|
2013
|
+
"""
|
|
2014
|
+
return self.__network_topo.get_adj_matrix()
|
|
2015
|
+
|
|
2016
|
+
def map_link_id_to_edge_idx(self, link_id: str) -> tuple[int, int]:
|
|
2017
|
+
"""
|
|
2018
|
+
Maps a given link to the corresponding two indices in the edge indices as computed by
|
|
2019
|
+
:func:`epyt_flow.simulation.scada.scada_data.ScadaData.topo_edge_indices`.
|
|
2020
|
+
|
|
2021
|
+
Returns
|
|
2022
|
+
-------
|
|
2023
|
+
`tuple[int, int]`
|
|
2024
|
+
Indices.
|
|
2025
|
+
"""
|
|
2026
|
+
if not isinstance(link_id, str):
|
|
2027
|
+
raise TypeError(f"'link_id' must be an instance of 'str' but not of '{type(link_id)}'")
|
|
2028
|
+
if link_id not in self.__sensor_config.links:
|
|
2029
|
+
raise ValueError(f"Unknown link '{link_id}'")
|
|
2030
|
+
|
|
2031
|
+
idx = 0
|
|
2032
|
+
for l_id, [node_a_id, node_b_id] in self.__network_topo.get_all_links():
|
|
2033
|
+
if l_id == link_id:
|
|
2034
|
+
return (idx, idx+1)
|
|
2035
|
+
|
|
2036
|
+
idx += 2
|
|
2037
|
+
|
|
2038
|
+
def get_topo_edge_indices(self) -> np.ndarray:
|
|
2039
|
+
"""
|
|
2040
|
+
Returns the edge indices -- i.e. a 2 dimensional array where the first dimension denotes
|
|
2041
|
+
the source node indices and the second dimension denotes the target node indices
|
|
2042
|
+
for all links in the network.
|
|
2043
|
+
Nodes are ordered according to EPANET.
|
|
2044
|
+
|
|
2045
|
+
Note that the network is consideres as a directed graph -- i.e. one link corresponds to
|
|
2046
|
+
two edges in opposite directions!
|
|
2047
|
+
|
|
2048
|
+
Returns
|
|
2049
|
+
-------
|
|
2050
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
2051
|
+
Edge indices of shape [2, num_links * 2].
|
|
2052
|
+
"""
|
|
2053
|
+
edge_indices = [[], []]
|
|
2054
|
+
|
|
2055
|
+
nodes_id = self.__network_topo.get_all_nodes()
|
|
2056
|
+
links = self.__network_topo.get_all_links()
|
|
2057
|
+
|
|
2058
|
+
for _, [node_a_id, node_b_id] in self.__network_topo.get_all_links():
|
|
2059
|
+
node_a_idx = nodes_id.index(node_a_id)
|
|
2060
|
+
node_b_idx = nodes_id.index(node_b_id)
|
|
2061
|
+
|
|
2062
|
+
edge_indices[0] += [node_a_idx, node_b_idx]
|
|
2063
|
+
edge_indices[1] += [node_b_idx, node_a_idx]
|
|
2064
|
+
|
|
2065
|
+
return np.array(edge_indices)
|
|
2066
|
+
|
|
1953
2067
|
def get_data(self) -> np.ndarray:
|
|
1954
2068
|
"""
|
|
1955
2069
|
Computes the final sensor readings -- note that those might be subject to
|
|
1956
2070
|
given sensor faults and sensor noise/uncertainty.
|
|
1957
2071
|
|
|
2072
|
+
Columns (i.e. sensor readings) are ordered as follows:
|
|
2073
|
+
|
|
2074
|
+
1. Pressures
|
|
2075
|
+
2. Flows
|
|
2076
|
+
3. Demands
|
|
2077
|
+
4. Nodes quality
|
|
2078
|
+
5. Links quality
|
|
2079
|
+
6. Valve state
|
|
2080
|
+
7. Pumps state
|
|
2081
|
+
8. Pumps efficiency
|
|
2082
|
+
9. Pumps energy consumption
|
|
2083
|
+
10. Tanks volume
|
|
2084
|
+
11. Surface species concentrations
|
|
2085
|
+
12. Bulk species nodes concentrations
|
|
2086
|
+
13. Bulk species links concentrations
|
|
2087
|
+
|
|
1958
2088
|
Returns
|
|
1959
2089
|
-------
|
|
1960
2090
|
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
@@ -2033,6 +2163,116 @@ class ScadaData(Serializable):
|
|
|
2033
2163
|
|
|
2034
2164
|
return sensor_readings
|
|
2035
2165
|
|
|
2166
|
+
def get_data_node_features(self, default_missing_value: float = 0.
|
|
2167
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2168
|
+
"""
|
|
2169
|
+
Returns the sensor readings as node features together with a boolean mask indicating the
|
|
2170
|
+
presence of a sensor -- i.e. pressure, demand, quality, bulk species concentration
|
|
2171
|
+
at each node.
|
|
2172
|
+
|
|
2173
|
+
Note that only quantities with at least one sensor are considered.
|
|
2174
|
+
|
|
2175
|
+
Parameters
|
|
2176
|
+
----------
|
|
2177
|
+
default_missing_value : `float`, optional
|
|
2178
|
+
Default value (i.e. missing value) for nodes where no sensor is installed.
|
|
2179
|
+
|
|
2180
|
+
The default is 0.
|
|
2181
|
+
|
|
2182
|
+
Returns
|
|
2183
|
+
-------
|
|
2184
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2185
|
+
Node features of shape [num_nodes, num_time_steps, num_node_features], and
|
|
2186
|
+
mask of shape [num_nodes, num_node_features].
|
|
2187
|
+
"""
|
|
2188
|
+
node_features = []
|
|
2189
|
+
node_features_mask = []
|
|
2190
|
+
|
|
2191
|
+
if len(self.__sensor_config.pressure_sensors) != 0:
|
|
2192
|
+
features, node_mask = self.get_data_pressures_as_node_features(default_missing_value)
|
|
2193
|
+
features = features.T
|
|
2194
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2195
|
+
node_features.append(features)
|
|
2196
|
+
node_features_mask.append(node_mask.reshape(-1, 1))
|
|
2197
|
+
|
|
2198
|
+
if len(self.__sensor_config.demand_sensors) != 0:
|
|
2199
|
+
features, node_mask = self.get_data_demands_as_node_features(default_missing_value)
|
|
2200
|
+
features = features.T
|
|
2201
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2202
|
+
node_features.append(features)
|
|
2203
|
+
node_features_mask.append(node_mask.reshape(-1, 1))
|
|
2204
|
+
|
|
2205
|
+
if len(self.__sensor_config.quality_node_sensors) != 0:
|
|
2206
|
+
features, node_mask = self.get_data_nodes_quality_as_node_features(default_missing_value)
|
|
2207
|
+
features = features.T
|
|
2208
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2209
|
+
node_features.append(features)
|
|
2210
|
+
node_features_mask.append(node_mask.reshape(-1, 1))
|
|
2211
|
+
|
|
2212
|
+
if len(self.__sensor_config.bulk_species_node_sensors) != 0:
|
|
2213
|
+
features, node_mask = self.\
|
|
2214
|
+
get_data_bulk_species_concentrations_as_node_features(default_missing_value)
|
|
2215
|
+
features = np.swapaxes(features, 0, 1)
|
|
2216
|
+
node_features.append(features)
|
|
2217
|
+
node_features_mask.append(node_mask)
|
|
2218
|
+
|
|
2219
|
+
return np.concatenate(node_features, axis=2), np.concatenate(node_features_mask, axis=1)
|
|
2220
|
+
|
|
2221
|
+
def get_data_edge_features(self, default_missing_value: float = 0.
|
|
2222
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2223
|
+
"""
|
|
2224
|
+
Returns the sensor readings as edge features together with a boolean mask indicating the
|
|
2225
|
+
presence of a sensor -- i.e. flow, quality, surface species concentration,
|
|
2226
|
+
bulk species concentration at each link.
|
|
2227
|
+
|
|
2228
|
+
Note that only quantities with at least one sensor are considered.
|
|
2229
|
+
|
|
2230
|
+
Parameters
|
|
2231
|
+
----------
|
|
2232
|
+
default_missing_value : `float`, optional
|
|
2233
|
+
Default value (i.e. missing value) for links where no sensor is installed.
|
|
2234
|
+
|
|
2235
|
+
The default is 0.
|
|
2236
|
+
|
|
2237
|
+
Returns
|
|
2238
|
+
-------
|
|
2239
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2240
|
+
Edge features of shape [num_links, num_time_steps, num_edge_features] and
|
|
2241
|
+
mask of shape [num_links, num_edge_features].
|
|
2242
|
+
"""
|
|
2243
|
+
edge_features = []
|
|
2244
|
+
edge_features_mask = []
|
|
2245
|
+
|
|
2246
|
+
if len(self.__sensor_config.flow_sensors) != 0:
|
|
2247
|
+
features, link_mask = self.get_data_flows_as_edge_features(default_missing_value)
|
|
2248
|
+
features = features.T
|
|
2249
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2250
|
+
edge_features.append(features)
|
|
2251
|
+
edge_features_mask.append(link_mask.reshape(-1, 1))
|
|
2252
|
+
|
|
2253
|
+
if len(self.__sensor_config.quality_link_sensors) != 0:
|
|
2254
|
+
features, link_mask = self.get_data_links_quality_as_edge_features(default_missing_value)
|
|
2255
|
+
features = features.T
|
|
2256
|
+
features = features.reshape(features.shape[0], features.shape[1], 1)
|
|
2257
|
+
edge_features.append(features)
|
|
2258
|
+
edge_features_mask.append(link_mask.reshape(-1, 1))
|
|
2259
|
+
|
|
2260
|
+
if len(self.__sensor_config.surface_species_sensors) != 0:
|
|
2261
|
+
features, link_mask = self.\
|
|
2262
|
+
get_data_surface_species_concentrations_as_edge_features(default_missing_value)
|
|
2263
|
+
features = np.swapaxes(features, 0, 1)
|
|
2264
|
+
edge_features.append(features)
|
|
2265
|
+
edge_features_mask.append(link_mask)
|
|
2266
|
+
|
|
2267
|
+
if len(self.__sensor_config.bulk_species_link_sensors) != 0:
|
|
2268
|
+
features, link_mask = self.\
|
|
2269
|
+
get_data_bulk_species_concentrations_as_edge_features(default_missing_value)
|
|
2270
|
+
features = np.swapaxes(features, 0, 1)
|
|
2271
|
+
edge_features.append(features)
|
|
2272
|
+
edge_features_mask.append(link_mask)
|
|
2273
|
+
|
|
2274
|
+
return np.concatenate(edge_features, axis=2), np.concatenate(edge_features_mask, axis=1)
|
|
2275
|
+
|
|
2036
2276
|
def __get_x_axis_label(self) -> str:
|
|
2037
2277
|
if len(self.__sensor_readings_time) > 1:
|
|
2038
2278
|
time_step = self.__sensor_readings_time[1] - self.__sensor_readings_time[0]
|
|
@@ -2084,6 +2324,38 @@ class ScadaData(Serializable):
|
|
|
2084
2324
|
for s_id in sensor_locations]
|
|
2085
2325
|
return self.__sensor_readings[:, idx]
|
|
2086
2326
|
|
|
2327
|
+
def get_data_pressures_as_node_features(self,
|
|
2328
|
+
default_missing_value: float = 0.
|
|
2329
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2330
|
+
"""
|
|
2331
|
+
Returns the pressures as node features together with a boolean mask indicating the
|
|
2332
|
+
presence of a sensor.
|
|
2333
|
+
|
|
2334
|
+
Parameters
|
|
2335
|
+
----------
|
|
2336
|
+
default_missing_value : `float`, optional
|
|
2337
|
+
Default value (i.e. missing value) for nodes where no pressure sensor is installed.
|
|
2338
|
+
|
|
2339
|
+
The default is 0.
|
|
2340
|
+
|
|
2341
|
+
Returns
|
|
2342
|
+
-------
|
|
2343
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2344
|
+
Pressures as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
|
|
2345
|
+
"""
|
|
2346
|
+
mask = np.zeros(len(self.__sensor_config.nodes))
|
|
2347
|
+
node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
|
|
2348
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2349
|
+
nodes_id = self.__network_topo.get_all_nodes()
|
|
2350
|
+
|
|
2351
|
+
pressure_readings = self.get_data_pressures()
|
|
2352
|
+
for pressures_idx, node_id in enumerate(self.__sensor_config.pressure_sensors):
|
|
2353
|
+
idx = nodes_id.index(node_id)
|
|
2354
|
+
node_features[:, idx] = pressure_readings[:, pressures_idx]
|
|
2355
|
+
mask[idx] = 1
|
|
2356
|
+
|
|
2357
|
+
return node_features, mask
|
|
2358
|
+
|
|
2087
2359
|
def plot_pressures(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2088
2360
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2089
2361
|
) -> matplotlib.axes.Axes:
|
|
@@ -2171,6 +2443,43 @@ class ScadaData(Serializable):
|
|
|
2171
2443
|
for s_id in sensor_locations]
|
|
2172
2444
|
return self.__sensor_readings[:, idx]
|
|
2173
2445
|
|
|
2446
|
+
def get_data_flows_as_edge_features(self, default_missing_value: float = 0.
|
|
2447
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2448
|
+
"""
|
|
2449
|
+
Returns the flows as edge features together with a boolean mask indicating the
|
|
2450
|
+
presence of a sensor.
|
|
2451
|
+
|
|
2452
|
+
Note that the second link has the opposite flow direction of the flow at the first link --
|
|
2453
|
+
recall that we have an undirected graph, i.e. two edges per link.
|
|
2454
|
+
|
|
2455
|
+
Parameters
|
|
2456
|
+
----------
|
|
2457
|
+
default_missing_value : `float`, optional
|
|
2458
|
+
Default value (i.e. missing value) for links where no flow sensor is installed.
|
|
2459
|
+
|
|
2460
|
+
The default is 0.
|
|
2461
|
+
|
|
2462
|
+
Returns
|
|
2463
|
+
-------
|
|
2464
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2465
|
+
Flows as edge features of shape [num_time_steps, num_links * 2] and mask of shape [num_links * 2].
|
|
2466
|
+
"""
|
|
2467
|
+
mask = np.zeros(2 * len(self.__sensor_config.links))
|
|
2468
|
+
edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
|
|
2469
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2470
|
+
|
|
2471
|
+
flow_readings = self.get_data_flows()
|
|
2472
|
+
for flows_idx, link_id in enumerate(self.__sensor_config.flow_sensors):
|
|
2473
|
+
idx1, idx2 = self.map_link_id_to_edge_idx(link_id)
|
|
2474
|
+
|
|
2475
|
+
mask[idx1] = 1
|
|
2476
|
+
mask[idx2] = 1
|
|
2477
|
+
|
|
2478
|
+
edge_features[:, idx1] = flow_readings[:, flows_idx]
|
|
2479
|
+
edge_features[:, idx2] = -1 * flow_readings[:, flows_idx]
|
|
2480
|
+
|
|
2481
|
+
return edge_features, mask
|
|
2482
|
+
|
|
2174
2483
|
def plot_flows(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2175
2484
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2176
2485
|
) -> matplotlib.axes.Axes:
|
|
@@ -2257,6 +2566,38 @@ class ScadaData(Serializable):
|
|
|
2257
2566
|
for s_id in sensor_locations]
|
|
2258
2567
|
return self.__sensor_readings[:, idx]
|
|
2259
2568
|
|
|
2569
|
+
def get_data_demands_as_node_features(self,
|
|
2570
|
+
default_missing_value: float = 0.
|
|
2571
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2572
|
+
"""
|
|
2573
|
+
Returns the demands as node features together with a boolean mask indicating the
|
|
2574
|
+
presence of a sensor.
|
|
2575
|
+
|
|
2576
|
+
Parameters
|
|
2577
|
+
----------
|
|
2578
|
+
default_missing_value : `float`, optional
|
|
2579
|
+
Default value (i.e. missing value) for nodes where no demand sensor is installed.
|
|
2580
|
+
|
|
2581
|
+
The default is 0.
|
|
2582
|
+
|
|
2583
|
+
Returns
|
|
2584
|
+
-------
|
|
2585
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2586
|
+
Demands as node features of shape [num_time_steps, num_nodes], and mask of shape [num_nodes].
|
|
2587
|
+
"""
|
|
2588
|
+
mask = np.zeros(len(self.__sensor_config.nodes))
|
|
2589
|
+
node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
|
|
2590
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2591
|
+
nodes_id = self.__network_topo.get_all_nodes()
|
|
2592
|
+
|
|
2593
|
+
demand_readings = self.get_data_demands()
|
|
2594
|
+
for demands_idx, node_id in enumerate(self.__sensor_config.demand_sensors):
|
|
2595
|
+
idx = nodes_id.index(node_id)
|
|
2596
|
+
node_features[:, idx] = demand_readings[:, demands_idx]
|
|
2597
|
+
mask[idx] = 1
|
|
2598
|
+
|
|
2599
|
+
return node_features, mask
|
|
2600
|
+
|
|
2260
2601
|
def plot_demands(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2261
2602
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2262
2603
|
) -> matplotlib.axes.Axes:
|
|
@@ -2344,6 +2685,39 @@ class ScadaData(Serializable):
|
|
|
2344
2685
|
for s_id in sensor_locations]
|
|
2345
2686
|
return self.__sensor_readings[:, idx]
|
|
2346
2687
|
|
|
2688
|
+
def get_data_nodes_quality_as_node_features(self,
|
|
2689
|
+
default_missing_value: float = 0
|
|
2690
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2691
|
+
"""
|
|
2692
|
+
Returns the nodes' quality as node features together with a boolean mask indicating the
|
|
2693
|
+
presence of a sensor.
|
|
2694
|
+
|
|
2695
|
+
Parameters
|
|
2696
|
+
----------
|
|
2697
|
+
default_missing_value : `float`, optional
|
|
2698
|
+
Default value (i.e. missing value) for nodes where no quality sensor is installed.
|
|
2699
|
+
|
|
2700
|
+
The default is 0.
|
|
2701
|
+
|
|
2702
|
+
Returns
|
|
2703
|
+
-------
|
|
2704
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2705
|
+
Nodes' quality as node features of shape [num_time_steps, num_nodes], and mask of
|
|
2706
|
+
shape [num_nodes].
|
|
2707
|
+
"""
|
|
2708
|
+
mask = np.zeros(len(self.__sensor_config.nodes))
|
|
2709
|
+
node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
|
|
2710
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2711
|
+
nodes_id = self.__network_topo.get_all_nodes()
|
|
2712
|
+
|
|
2713
|
+
node_quality_readings = self.get_data_nodes_quality()
|
|
2714
|
+
for quality_idx, node_id in enumerate(self.__sensor_config.quality_node_sensors):
|
|
2715
|
+
idx = nodes_id.index(node_id)
|
|
2716
|
+
node_features[:, idx] = node_quality_readings[:, quality_idx]
|
|
2717
|
+
mask[idx] = 1
|
|
2718
|
+
|
|
2719
|
+
return node_features, mask
|
|
2720
|
+
|
|
2347
2721
|
def plot_nodes_quality(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2348
2722
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2349
2723
|
) -> matplotlib.axes.Axes:
|
|
@@ -2433,6 +2807,38 @@ class ScadaData(Serializable):
|
|
|
2433
2807
|
for s_id in sensor_locations]
|
|
2434
2808
|
return self.__sensor_readings[:, idx]
|
|
2435
2809
|
|
|
2810
|
+
def get_data_links_quality_as_edge_features(self,
|
|
2811
|
+
default_missing_value: float = 0
|
|
2812
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
2813
|
+
"""
|
|
2814
|
+
Returns the links' quality as edge features together with a boolean mask indicating the
|
|
2815
|
+
presence of a sensor.
|
|
2816
|
+
|
|
2817
|
+
Parameters
|
|
2818
|
+
----------
|
|
2819
|
+
default_missing_value : `float`, optional
|
|
2820
|
+
Default value (i.e. missing value) for links where no quality sensor is installed.
|
|
2821
|
+
|
|
2822
|
+
The default is 0.
|
|
2823
|
+
|
|
2824
|
+
Returns
|
|
2825
|
+
-------
|
|
2826
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
2827
|
+
Links' quality as edge features of shape [num_time_steps, num_links * 2], and mask of
|
|
2828
|
+
shape [num_links * 2].
|
|
2829
|
+
"""
|
|
2830
|
+
mask = np.zeros(2 * len(self.__sensor_config.links))
|
|
2831
|
+
edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
|
|
2832
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
2833
|
+
|
|
2834
|
+
links_quality_readings = self.get_data_links_quality()
|
|
2835
|
+
for quality_idx, link_id in enumerate(self.__sensor_config.quality_link_sensors):
|
|
2836
|
+
for idx in self.map_link_id_to_edge_idx(link_id):
|
|
2837
|
+
edge_features[:, idx] = links_quality_readings[:, quality_idx]
|
|
2838
|
+
mask[idx] = 1
|
|
2839
|
+
|
|
2840
|
+
return edge_features, mask
|
|
2841
|
+
|
|
2436
2842
|
def plot_links_quality(self, sensor_locations: list[str] = None, show: bool = True,
|
|
2437
2843
|
save_to_file: str = None, ax: matplotlib.axes.Axes = None
|
|
2438
2844
|
) -> matplotlib.axes.Axes:
|
|
@@ -2971,6 +3377,50 @@ class ScadaData(Serializable):
|
|
|
2971
3377
|
for link_id in surface_species_sensor_locations[species_id]]
|
|
2972
3378
|
return self.__sensor_readings[:, idx]
|
|
2973
3379
|
|
|
3380
|
+
def get_data_surface_species_concentrations_as_edge_features(self,
|
|
3381
|
+
default_missing_value: float = 0.
|
|
3382
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
3383
|
+
"""
|
|
3384
|
+
Returns the concentrations of surface species as edge features together with a
|
|
3385
|
+
boolean mask indicating the presence of a sensor.
|
|
3386
|
+
|
|
3387
|
+
Note that only surface species with at least one sensor are considered.
|
|
3388
|
+
|
|
3389
|
+
Parameters
|
|
3390
|
+
----------
|
|
3391
|
+
default_missing_value : `float`, optional
|
|
3392
|
+
Default value (i.e. missing value) for links where no surface species
|
|
3393
|
+
sensor is installed.
|
|
3394
|
+
|
|
3395
|
+
The default is 0.
|
|
3396
|
+
|
|
3397
|
+
Returns
|
|
3398
|
+
-------
|
|
3399
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
3400
|
+
Concentrations of surface species as edge features of shape
|
|
3401
|
+
[num_time_steps, num_links * 2, num_species], and mask of
|
|
3402
|
+
shape [num_links * 2, num_species].
|
|
3403
|
+
"""
|
|
3404
|
+
masks = []
|
|
3405
|
+
results = []
|
|
3406
|
+
|
|
3407
|
+
surface_species_sensor_locations = self.__sensor_config.surface_species_sensors
|
|
3408
|
+
for species_id, links_id in surface_species_sensor_locations.items():
|
|
3409
|
+
mask = np.zeros(2 * len(self.__sensor_config.links))
|
|
3410
|
+
edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
|
|
3411
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
3412
|
+
|
|
3413
|
+
sensor_readings = self.get_data_surface_species_concentration({species_id: links_id})
|
|
3414
|
+
for sensor_readings_idx, link_id in enumerate(links_id):
|
|
3415
|
+
for idx in self.map_link_id_to_edge_idx(link_id):
|
|
3416
|
+
edge_features[:, idx] = sensor_readings[:, sensor_readings_idx]
|
|
3417
|
+
mask[idx] = 1
|
|
3418
|
+
|
|
3419
|
+
results.append(edge_features.reshape(edge_features.shape[0], edge_features.shape[1], 1))
|
|
3420
|
+
masks.append(mask.reshape(-1, 1))
|
|
3421
|
+
|
|
3422
|
+
return np.concatenate(results, axis=2), np.concatenate(masks, axis=1)
|
|
3423
|
+
|
|
2974
3424
|
def plot_surface_species_concentration(self, surface_species_sensor_locations: dict = None,
|
|
2975
3425
|
show: bool = True, save_to_file: str = None,
|
|
2976
3426
|
ax: matplotlib.axes.Axes = None
|
|
@@ -3087,6 +3537,51 @@ class ScadaData(Serializable):
|
|
|
3087
3537
|
for node_id in bulk_species_sensor_locations[species_id]]
|
|
3088
3538
|
return self.__sensor_readings[:, idx]
|
|
3089
3539
|
|
|
3540
|
+
def get_data_bulk_species_concentrations_as_node_features(self,
|
|
3541
|
+
default_missing_value: float = 0.
|
|
3542
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
3543
|
+
"""
|
|
3544
|
+
Returns the concentrations of bulk species as node features together with a boolean mask
|
|
3545
|
+
indicating the presence of a sensor.
|
|
3546
|
+
|
|
3547
|
+
Note that only bulk species with at least one sensor are considered.
|
|
3548
|
+
|
|
3549
|
+
Parameters
|
|
3550
|
+
----------
|
|
3551
|
+
default_missing_value : `float`, optional
|
|
3552
|
+
Default value (i.e. missing value) for nodes where no bulk species
|
|
3553
|
+
sensor is installed.
|
|
3554
|
+
|
|
3555
|
+
The default is 0.
|
|
3556
|
+
|
|
3557
|
+
Returns
|
|
3558
|
+
-------
|
|
3559
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
3560
|
+
Concentrations of bulk species as node features of shape
|
|
3561
|
+
[num_time_steps, num_nodes, num_species], and mask of shape [num_nodes, num_species].
|
|
3562
|
+
"""
|
|
3563
|
+
masks = []
|
|
3564
|
+
results = []
|
|
3565
|
+
|
|
3566
|
+
all_nodes_id = self.__network_topo.get_all_nodes()
|
|
3567
|
+
bulk_species_sensor_locations = self.__sensor_config.bulk_species_node_sensors
|
|
3568
|
+
|
|
3569
|
+
for species_id, nodes_id in bulk_species_sensor_locations.items():
|
|
3570
|
+
mask = np.zeros(len(self.__sensor_config.nodes))
|
|
3571
|
+
node_features = np.array([[default_missing_value] * len(self.__sensor_config.nodes)
|
|
3572
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
3573
|
+
|
|
3574
|
+
sensor_readings = self.get_data_bulk_species_node_concentration({species_id: nodes_id})
|
|
3575
|
+
for sensor_readings_idx, node_id in enumerate(nodes_id):
|
|
3576
|
+
idx = all_nodes_id.index(node_id)
|
|
3577
|
+
node_features[:, idx] = sensor_readings[:, sensor_readings_idx]
|
|
3578
|
+
mask[idx] = 1
|
|
3579
|
+
|
|
3580
|
+
results.append(node_features.reshape(node_features.shape[0], node_features.shape[1],1))
|
|
3581
|
+
masks.append(mask.reshape(-1, 1))
|
|
3582
|
+
|
|
3583
|
+
return np.concatenate(results, axis=2), np.concatenate(masks, axis=1)
|
|
3584
|
+
|
|
3090
3585
|
def plot_bulk_species_node_concentration(self, bulk_species_node_sensors: dict = None,
|
|
3091
3586
|
show: bool = True, save_to_file: str = None,
|
|
3092
3587
|
ax: matplotlib.axes.Axes = None
|
|
@@ -3202,6 +3697,50 @@ class ScadaData(Serializable):
|
|
|
3202
3697
|
for node_id in bulk_species_sensor_locations[species_id]]
|
|
3203
3698
|
return self.__sensor_readings[:, idx]
|
|
3204
3699
|
|
|
3700
|
+
def get_data_bulk_species_concentrations_as_edge_features(self,
|
|
3701
|
+
default_missing_value: float = 0.
|
|
3702
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
3703
|
+
"""
|
|
3704
|
+
Returns the concentrations of bulk species as edge features together with a boolean mask
|
|
3705
|
+
indicating the presence of a sensor.
|
|
3706
|
+
|
|
3707
|
+
Note that only bulk species with at least one sensor are considered.
|
|
3708
|
+
|
|
3709
|
+
Parameters
|
|
3710
|
+
----------
|
|
3711
|
+
default_missing_value : `float`, optional
|
|
3712
|
+
Default value (i.e. missing value) for links where no bulk species
|
|
3713
|
+
sensor is installed.
|
|
3714
|
+
|
|
3715
|
+
The default is 0.
|
|
3716
|
+
|
|
3717
|
+
Returns
|
|
3718
|
+
-------
|
|
3719
|
+
tuple[`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_, `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_]
|
|
3720
|
+
Concentrations of bulk species as edge features of shape
|
|
3721
|
+
[num_time_steps, num_links * 2, num_species], and mask of
|
|
3722
|
+
shape [num_links * 2, num_species].
|
|
3723
|
+
"""
|
|
3724
|
+
masks = []
|
|
3725
|
+
results = []
|
|
3726
|
+
|
|
3727
|
+
bulk_species_sensor_locations = self.__sensor_config.bulk_species_link_sensors
|
|
3728
|
+
for species_id, links_id in bulk_species_sensor_locations.items():
|
|
3729
|
+
mask = np.zeros(2 * len(self.__sensor_config.links))
|
|
3730
|
+
edge_features = np.array([[default_missing_value] * 2 * len(self.__sensor_config.links)
|
|
3731
|
+
for _ in range(len(self.__sensor_readings_time))])
|
|
3732
|
+
|
|
3733
|
+
sensor_readings = self.get_data_bulk_species_link_concentration({species_id: links_id})
|
|
3734
|
+
for sensor_readings_idx, link_id in enumerate(links_id):
|
|
3735
|
+
for idx in self.map_link_id_to_edge_idx(link_id):
|
|
3736
|
+
edge_features[:, idx] = sensor_readings[:, sensor_readings_idx]
|
|
3737
|
+
mask[idx] = 1
|
|
3738
|
+
|
|
3739
|
+
results.append(edge_features.reshape(edge_features.shape[0], edge_features.shape[1], 1))
|
|
3740
|
+
masks.append(mask.reshape(-1, 1))
|
|
3741
|
+
|
|
3742
|
+
return np.concatenate(results, axis=2), np.concatenate(masks, axis=1)
|
|
3743
|
+
|
|
3205
3744
|
def plot_bulk_species_link_concentration(self, bulk_species_link_sensors: dict = None,
|
|
3206
3745
|
show: bool = True, save_to_file: str = None,
|
|
3207
3746
|
ax: matplotlib.axes.Axes = None
|