epyt-flow 0.10.0__py3-none-any.whl → 0.12.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/benchmarks/gecco_water_quality.py +2 -2
- epyt_flow/data/benchmarks/leakdb.py +40 -5
- epyt_flow/data/benchmarks/water_usage.py +4 -3
- epyt_flow/data/networks.py +27 -14
- epyt_flow/gym/__init__.py +0 -3
- epyt_flow/gym/scenario_control_env.py +11 -13
- epyt_flow/rest_api/scenario/control_handlers.py +118 -0
- epyt_flow/rest_api/scenario/event_handlers.py +114 -1
- epyt_flow/rest_api/scenario/handlers.py +33 -0
- epyt_flow/rest_api/server.py +14 -2
- epyt_flow/serialization.py +1 -0
- epyt_flow/simulation/__init__.py +0 -1
- epyt_flow/simulation/backend/__init__.py +1 -0
- epyt_flow/simulation/backend/my_epyt.py +1056 -0
- epyt_flow/simulation/events/actuator_events.py +7 -1
- epyt_flow/simulation/events/quality_events.py +3 -1
- epyt_flow/simulation/scada/scada_data.py +716 -5
- epyt_flow/simulation/scenario_config.py +1 -40
- epyt_flow/simulation/scenario_simulator.py +645 -119
- epyt_flow/simulation/sensor_config.py +18 -2
- epyt_flow/topology.py +24 -7
- 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 +45 -1
- epyt_flow/visualization/__init__.py +2 -0
- epyt_flow/visualization/scenario_visualizer.py +1240 -0
- epyt_flow/visualization/visualization_utils.py +738 -0
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/METADATA +15 -4
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/RECORD +35 -36
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/WHEEL +1 -1
- epyt_flow/gym/control_gyms.py +0 -47
- epyt_flow/metrics.py +0 -466
- epyt_flow/models/__init__.py +0 -2
- epyt_flow/models/event_detector.py +0 -31
- epyt_flow/models/sensor_interpolation_detector.py +0 -118
- epyt_flow/simulation/scada/advanced_control.py +0 -138
- epyt_flow/simulation/scenario_visualizer.py +0 -1307
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info/licenses}/LICENSE +0 -0
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.12.0.dist-info}/top_level.txt +0 -0
|
@@ -1992,8 +1992,24 @@ class SensorConfig(JsonSerializable):
|
|
|
1992
1992
|
bulk_species_link_concentrations: np.ndarray,
|
|
1993
1993
|
surface_species_concentrations: np.ndarray) -> np.ndarray:
|
|
1994
1994
|
"""
|
|
1995
|
-
Applies the sensor configuration to a set of raw simulation results --
|
|
1996
|
-
|
|
1995
|
+
Applies the sensor configuration to a set of raw simulation results -- i.e. computes
|
|
1996
|
+
the sensor readings as an array.
|
|
1997
|
+
|
|
1998
|
+
Columns (i.e. sensor readings) are ordered as follows:
|
|
1999
|
+
|
|
2000
|
+
1. Pressures
|
|
2001
|
+
2. Flows
|
|
2002
|
+
3. Demands
|
|
2003
|
+
4. Nodes quality
|
|
2004
|
+
5. Links quality
|
|
2005
|
+
6. Valve state
|
|
2006
|
+
7. Pumps state
|
|
2007
|
+
8. Pumps efficiency
|
|
2008
|
+
9. Pumps energy consumption
|
|
2009
|
+
10. Tanks volume
|
|
2010
|
+
11. Surface species concentrations
|
|
2011
|
+
12. Bulk species nodes concentrations
|
|
2012
|
+
13. Bulk species links concentrations
|
|
1997
2013
|
|
|
1998
2014
|
Parameters
|
|
1999
2015
|
----------
|
epyt_flow/topology.py
CHANGED
|
@@ -78,7 +78,7 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
78
78
|
links: list[tuple[str, tuple[str, str], dict]],
|
|
79
79
|
pumps: dict,
|
|
80
80
|
valves: dict,
|
|
81
|
-
units: int
|
|
81
|
+
units: int,
|
|
82
82
|
**kwds):
|
|
83
83
|
super().__init__(name=f_inp, **kwds)
|
|
84
84
|
|
|
@@ -88,11 +88,6 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
88
88
|
self.__valves = valves
|
|
89
89
|
self.__units = units
|
|
90
90
|
|
|
91
|
-
if units is None:
|
|
92
|
-
warnings.warn("Loading a file that was created with an outdated version of EPyT-Flow" +
|
|
93
|
-
" -- support of such old files will be removed in the next release!",
|
|
94
|
-
DeprecationWarning)
|
|
95
|
-
|
|
96
91
|
for node_id, node_info in nodes:
|
|
97
92
|
node_elevation = node_info["elevation"]
|
|
98
93
|
node_type = node_info["type"]
|
|
@@ -561,7 +556,8 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
561
556
|
|
|
562
557
|
# Nodes
|
|
563
558
|
node_data = {"id": [], "type": [], "elevation": [], "geometry": []}
|
|
564
|
-
tank_data = {"id": [], "
|
|
559
|
+
tank_data = {"id": [], "volume": [], "max_level": [], "min_level": [], "mixing_fraction": [],
|
|
560
|
+
"elevation": [], "diameter": [], "geometry": []}
|
|
565
561
|
reservoir_data = {"id": [], "elevation": [], "geometry": []}
|
|
566
562
|
for node_id in self.get_all_nodes():
|
|
567
563
|
node_info = self.get_node_info(node_id)
|
|
@@ -575,6 +571,11 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
575
571
|
tank_data["id"].append(node_id)
|
|
576
572
|
tank_data["elevation"].append(node_info["elevation"])
|
|
577
573
|
tank_data["diameter"].append(node_info["diameter"])
|
|
574
|
+
tank_data["volume"].append(node_info["volume"])
|
|
575
|
+
tank_data["max_level"].append(node_info["max_level"])
|
|
576
|
+
tank_data["min_level"].append(node_info["min_level"])
|
|
577
|
+
tank_data["mixing_fraction"].append(node_info["mixing_fraction"])
|
|
578
|
+
#tank_data["mixing_model"].append(node_info["mixing_model"])
|
|
578
579
|
tank_data["geometry"].append(Point(node_info["coord"]))
|
|
579
580
|
elif node_info["type"] == "RESERVOIR":
|
|
580
581
|
reservoir_data["id"].append(node_id)
|
|
@@ -653,6 +654,22 @@ class NetworkTopology(nx.Graph, JsonSerializable):
|
|
|
653
654
|
|
|
654
655
|
return bsr_array((np.ones(len(row)), (row, col)), shape=(n_nodes, n_nodes))
|
|
655
656
|
|
|
657
|
+
def get_adj_list(self) -> dict[str, list[str]]:
|
|
658
|
+
"""
|
|
659
|
+
Returns the connectivity of the nodes (node IDs) as an adjacency list.
|
|
660
|
+
|
|
661
|
+
Returns
|
|
662
|
+
-------
|
|
663
|
+
`dict[str, list[str]]`
|
|
664
|
+
Adjacency list as a dictionary.
|
|
665
|
+
"""
|
|
666
|
+
adj_list = {}
|
|
667
|
+
|
|
668
|
+
for node_id in self.get_all_nodes():
|
|
669
|
+
adj_list[node_id] = self.get_neighbors(node_id)
|
|
670
|
+
|
|
671
|
+
return adj_list
|
|
672
|
+
|
|
656
673
|
def get_neighbors(self, node_id: str) -> list[str]:
|
|
657
674
|
"""
|
|
658
675
|
Gets all neighboring nodes of a given node.
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Module provides a class for implementing model uncertainty.
|
|
3
3
|
"""
|
|
4
|
+
from typing import Optional
|
|
4
5
|
from copy import deepcopy
|
|
5
6
|
import warnings
|
|
6
7
|
import epyt
|
|
@@ -114,67 +115,31 @@ class ModelUncertainty(JsonSerializable):
|
|
|
114
115
|
None, in the case of no uncertainty.
|
|
115
116
|
|
|
116
117
|
The default is None.
|
|
118
|
+
seed : `int`, optional
|
|
119
|
+
Seed for the random number generator.
|
|
120
|
+
|
|
121
|
+
Thed default is None.
|
|
117
122
|
"""
|
|
118
|
-
def __init__(self,
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
local_base_demand_uncertainty: dict[str, Uncertainty] = None,
|
|
138
|
-
local_demand_pattern_uncertainty: dict[str, Uncertainty] = None,
|
|
139
|
-
local_elevation_uncertainty: dict[str, Uncertainty] = None,
|
|
140
|
-
local_constants_uncertainty: dict[str, Uncertainty] = None,
|
|
141
|
-
local_parameters_uncertainty: dict[str, int, Uncertainty] = None,
|
|
142
|
-
local_patterns_uncertainty: dict[str, Uncertainty] = None,
|
|
143
|
-
local_msx_patterns_uncertainty: dict[str, Uncertainty] = None,
|
|
123
|
+
def __init__(self, global_pipe_length_uncertainty: Optional[Uncertainty] = None,
|
|
124
|
+
global_pipe_roughness_uncertainty: Optional[Uncertainty] = None,
|
|
125
|
+
global_pipe_diameter_uncertainty: Optional[Uncertainty] = None,
|
|
126
|
+
global_base_demand_uncertainty: Optional[Uncertainty] = None,
|
|
127
|
+
global_demand_pattern_uncertainty: Optional[Uncertainty] = None,
|
|
128
|
+
global_elevation_uncertainty: Optional[Uncertainty] = None,
|
|
129
|
+
global_constants_uncertainty: Optional[Uncertainty] = None,
|
|
130
|
+
global_parameters_uncertainty: Optional[Uncertainty] = None,
|
|
131
|
+
local_pipe_length_uncertainty: Optional[dict[str, Uncertainty]] = None,
|
|
132
|
+
local_pipe_roughness_uncertainty: Optional[dict[str, Uncertainty]] = None,
|
|
133
|
+
local_pipe_diameter_uncertainty: Optional[dict[str, Uncertainty]] = None,
|
|
134
|
+
local_base_demand_uncertainty: Optional[dict[str, Uncertainty]] = None,
|
|
135
|
+
local_demand_pattern_uncertainty: Optional[dict[str, Uncertainty]] = None,
|
|
136
|
+
local_elevation_uncertainty: Optional[dict[str, Uncertainty]] = None,
|
|
137
|
+
local_constants_uncertainty: Optional[dict[str, Uncertainty]] = None,
|
|
138
|
+
local_parameters_uncertainty: Optional[dict[str, int, Uncertainty]] = None,
|
|
139
|
+
local_patterns_uncertainty: Optional[dict[str, Uncertainty]] = None,
|
|
140
|
+
local_msx_patterns_uncertainty: Optional[dict[str, Uncertainty]] = None,
|
|
141
|
+
seed: Optional[int] = None,
|
|
144
142
|
**kwds):
|
|
145
|
-
if pipe_length_uncertainty is not None:
|
|
146
|
-
global_pipe_diameter_uncertainty = pipe_length_uncertainty
|
|
147
|
-
warnings.warn("'pipe_length_uncertainty' is deprecated and " +
|
|
148
|
-
"will be removed in future releases")
|
|
149
|
-
if pipe_roughness_uncertainty is not None:
|
|
150
|
-
global_pipe_roughness_uncertainty = pipe_roughness_uncertainty
|
|
151
|
-
warnings.warn("'pipe_roughness_uncertainty' is deprecated and " +
|
|
152
|
-
"will be removed in future releases")
|
|
153
|
-
if pipe_diameter_uncertainty is not None:
|
|
154
|
-
global_pipe_diameter_uncertainty = pipe_diameter_uncertainty
|
|
155
|
-
warnings.warn("'pipe_diameter_uncertainty' is deprecated and " +
|
|
156
|
-
"will be removed in future releases")
|
|
157
|
-
if base_demand_uncertainty is not None:
|
|
158
|
-
global_base_demand_uncertainty = base_demand_uncertainty
|
|
159
|
-
warnings.warn("'base_demand_uncertainty' is deprecated and " +
|
|
160
|
-
"will be removed in future releases")
|
|
161
|
-
if demand_pattern_uncertainty is not None:
|
|
162
|
-
global_demand_pattern_uncertainty = demand_pattern_uncertainty
|
|
163
|
-
warnings.warn("'demand_pattern_uncertainty' is deprecated and " +
|
|
164
|
-
"will be removed in future releases")
|
|
165
|
-
if elevation_uncertainty is not None:
|
|
166
|
-
global_elevation_uncertainty = elevation_uncertainty
|
|
167
|
-
warnings.warn("'elevation_uncertainty' is deprecated and " +
|
|
168
|
-
"will be removed in future releases")
|
|
169
|
-
if constants_uncertainty is not None:
|
|
170
|
-
global_constants_uncertainty = constants_uncertainty
|
|
171
|
-
warnings.warn("'constants_uncertainty' is deprecated and " +
|
|
172
|
-
"will be removed in future releases")
|
|
173
|
-
if parameters_uncertainty is not None:
|
|
174
|
-
global_parameters_uncertainty = parameters_uncertainty
|
|
175
|
-
warnings.warn("'parameters_uncertainty' is deprecated and " +
|
|
176
|
-
"will be removed in future releases")
|
|
177
|
-
|
|
178
143
|
if global_pipe_length_uncertainty is not None:
|
|
179
144
|
if not isinstance(global_pipe_length_uncertainty, Uncertainty):
|
|
180
145
|
raise TypeError("'global_pipe_length_uncertainty' must be an instance of " +
|
|
@@ -337,9 +302,22 @@ class ModelUncertainty(JsonSerializable):
|
|
|
337
302
|
self.__local_parameters = local_parameters_uncertainty
|
|
338
303
|
self.__local_patterns = local_patterns_uncertainty
|
|
339
304
|
self.__local_msx_patterns = local_msx_patterns_uncertainty
|
|
305
|
+
self.__seed = seed
|
|
340
306
|
|
|
341
307
|
super().__init__(**kwds)
|
|
342
308
|
|
|
309
|
+
@property
|
|
310
|
+
def seed(self) -> int:
|
|
311
|
+
"""
|
|
312
|
+
Returns the seed used for the random number generator.
|
|
313
|
+
|
|
314
|
+
Returns
|
|
315
|
+
-------
|
|
316
|
+
`int`
|
|
317
|
+
Seed for the random number generator.
|
|
318
|
+
"""
|
|
319
|
+
return self.__seed
|
|
320
|
+
|
|
343
321
|
@property
|
|
344
322
|
def global_pipe_length(self) -> Uncertainty:
|
|
345
323
|
"""
|
|
@@ -574,7 +552,8 @@ class ModelUncertainty(JsonSerializable):
|
|
|
574
552
|
"local_constants_uncertainty": self.__local_constants,
|
|
575
553
|
"local_parameters_uncertainty": self.__local_parameters,
|
|
576
554
|
"local_patterns_uncertainty": self.__local_patterns,
|
|
577
|
-
"local_msx_patterns_uncertainty": self.__local_msx_patterns
|
|
555
|
+
"local_msx_patterns_uncertainty": self.__local_msx_patterns,
|
|
556
|
+
"seed": self.__seed}
|
|
578
557
|
|
|
579
558
|
return super().get_attributes() | attribs
|
|
580
559
|
|
|
@@ -600,7 +579,8 @@ class ModelUncertainty(JsonSerializable):
|
|
|
600
579
|
and self.__local_parameters == other.local_parameters \
|
|
601
580
|
and self.__local_constants == other.local_constants \
|
|
602
581
|
and self.__local_patterns == other.local_patterns \
|
|
603
|
-
and self.__local_msx_patterns == other.local_msx_patterns
|
|
582
|
+
and self.__local_msx_patterns == other.local_msx_patterns \
|
|
583
|
+
and self.__seed == other.seed
|
|
604
584
|
|
|
605
585
|
def __str__(self) -> str:
|
|
606
586
|
return f"global_pipe_length: {self.__global_pipe_length} " +\
|
|
@@ -620,7 +600,7 @@ class ModelUncertainty(JsonSerializable):
|
|
|
620
600
|
f"local_constants: {self.__local_constants} " + \
|
|
621
601
|
f"local_parameters: {self.__local_parameters} " + \
|
|
622
602
|
f"local_patterns: {self.__local_patterns} " + \
|
|
623
|
-
f"local_msx_patterns: {self.__local_msx_patterns}"
|
|
603
|
+
f"local_msx_patterns: {self.__local_msx_patterns} + seed: {self.__seed}"
|
|
624
604
|
|
|
625
605
|
def apply(self, epanet_api: epyt.epanet) -> None:
|
|
626
606
|
"""
|
|
@@ -631,12 +611,18 @@ class ModelUncertainty(JsonSerializable):
|
|
|
631
611
|
epanet_api : `epyt.epanet <https://epanet-python-toolkit-epyt.readthedocs.io/en/stable/api.html#epyt.epanet.epanet>`_
|
|
632
612
|
Interface to EPANET and EPANET-MSX.
|
|
633
613
|
"""
|
|
614
|
+
np_rand_gen = np.random.default_rng(seed=self.__seed)
|
|
615
|
+
|
|
634
616
|
if self.__global_pipe_length is not None:
|
|
617
|
+
self.__global_pipe_length.set_random_generator(np_rand_gen)
|
|
618
|
+
|
|
635
619
|
link_length = epanet_api.getLinkLength()
|
|
636
620
|
link_length = self.__global_pipe_length.apply_batch(link_length)
|
|
637
621
|
epanet_api.setLinkLength(link_length)
|
|
638
622
|
|
|
639
623
|
if self.__local_pipe_length is not None:
|
|
624
|
+
self.__local_pipe_length.set_random_generator(np_rand_gen)
|
|
625
|
+
|
|
640
626
|
for pipe_id, uncertainty in self.__local_pipe_length.items():
|
|
641
627
|
link_idx = epanet_api.getLinkIndex(pipe_id)
|
|
642
628
|
link_length = epanet_api.getLinkLength(link_idx)
|
|
@@ -644,11 +630,15 @@ class ModelUncertainty(JsonSerializable):
|
|
|
644
630
|
epanet_api.setLinkLength(link_idx, link_length)
|
|
645
631
|
|
|
646
632
|
if self.__global_pipe_diameter is not None:
|
|
633
|
+
self.__global_pipe_diameter.set_random_generator(np_rand_gen)
|
|
634
|
+
|
|
647
635
|
link_diameters = epanet_api.getLinkDiameter()
|
|
648
636
|
link_diameters = self.__global_pipe_diameter.apply_batch(link_diameters)
|
|
649
637
|
epanet_api.setLinkDiameter(link_diameters)
|
|
650
638
|
|
|
651
639
|
if self.__local_pipe_diameter is not None:
|
|
640
|
+
self.__local_pipe_diameter.set_random_generator(np_rand_gen)
|
|
641
|
+
|
|
652
642
|
for pipe_id, uncertainty in self.__local_pipe_diameter.items():
|
|
653
643
|
link_idx = epanet_api.getLinkIndex(pipe_id)
|
|
654
644
|
link_diameter = epanet_api.getLinkDiameter(link_idx)
|
|
@@ -656,11 +646,15 @@ class ModelUncertainty(JsonSerializable):
|
|
|
656
646
|
epanet_api.setLinkDiameter(link_idx, link_diameter)
|
|
657
647
|
|
|
658
648
|
if self.__global_pipe_roughness is not None:
|
|
649
|
+
self.__global_pipe_roughness.set_random_generator(np_rand_gen)
|
|
650
|
+
|
|
659
651
|
coeffs = epanet_api.getLinkRoughnessCoeff()
|
|
660
652
|
coeffs = self.__global_pipe_roughness.apply_batch(coeffs)
|
|
661
653
|
epanet_api.setLinkRoughnessCoeff(coeffs)
|
|
662
654
|
|
|
663
655
|
if self.__local_pipe_roughness is not None:
|
|
656
|
+
self.__local_pipe_roughness.set_random_generator(np_rand_gen)
|
|
657
|
+
|
|
664
658
|
for pipe_id, uncertainty in self.__local_pipe_roughness.items():
|
|
665
659
|
link_idx = epanet_api.getLinkIndex(pipe_id)
|
|
666
660
|
link_roughness_coeff = epanet_api.getLinkRoughnessCoeff(link_idx)
|
|
@@ -668,6 +662,8 @@ class ModelUncertainty(JsonSerializable):
|
|
|
668
662
|
epanet_api.setLinkRoughnessCoeff(link_idx, link_roughness_coeff)
|
|
669
663
|
|
|
670
664
|
if self.__global_base_demand is not None:
|
|
665
|
+
self.__global_base_demand.set_random_generator(np_rand_gen)
|
|
666
|
+
|
|
671
667
|
all_nodes_idx = epanet_api.getNodeIndex()
|
|
672
668
|
for node_idx in all_nodes_idx:
|
|
673
669
|
n_demand_categories = epanet_api.getNodeDemandCategoriesNumber(node_idx)
|
|
@@ -677,6 +673,8 @@ class ModelUncertainty(JsonSerializable):
|
|
|
677
673
|
epanet_api.setNodeBaseDemands(node_idx, demand_category + 1, base_demand)
|
|
678
674
|
|
|
679
675
|
if self.__local_base_demand is not None:
|
|
676
|
+
self.__local_base_demand.set_random_generator(np_rand_gen)
|
|
677
|
+
|
|
680
678
|
for node_id, uncertainty in self.__local_base_demand.items():
|
|
681
679
|
node_idx = epanet_api.getNodeIndex(node_id)
|
|
682
680
|
n_demand_categories = epanet_api.getNodeDemandCategoriesNumber(node_idx)
|
|
@@ -686,6 +684,8 @@ class ModelUncertainty(JsonSerializable):
|
|
|
686
684
|
epanet_api.setNodeBaseDemands(node_idx, demand_category + 1, base_demand)
|
|
687
685
|
|
|
688
686
|
if self.__global_demand_pattern is not None:
|
|
687
|
+
self.__global_demand_pattern.set_random_generator(np_rand_gen)
|
|
688
|
+
|
|
689
689
|
demand_patterns_idx = epanet_api.getNodeDemandPatternIndex()
|
|
690
690
|
demand_patterns_id = np.unique([demand_patterns_idx[k]
|
|
691
691
|
for k in demand_patterns_idx.keys()])
|
|
@@ -699,6 +699,8 @@ class ModelUncertainty(JsonSerializable):
|
|
|
699
699
|
epanet_api.setPatternValue(pattern_id, t+1, v_)
|
|
700
700
|
|
|
701
701
|
if self.__local_demand_pattern is not None:
|
|
702
|
+
self.__local_demand_pattern.set_random_generator(np_rand_gen)
|
|
703
|
+
|
|
702
704
|
patterns_id = epanet_api.getPatternNameID()
|
|
703
705
|
paterns_idx = epanet_api.getPatternIndex()
|
|
704
706
|
|
|
@@ -711,11 +713,15 @@ class ModelUncertainty(JsonSerializable):
|
|
|
711
713
|
epanet_api.setPatternValue(pattern_idx, t+1, v_)
|
|
712
714
|
|
|
713
715
|
if self.__global_elevation is not None:
|
|
716
|
+
self.__global_elevation.set_random_generator(np_rand_gen)
|
|
717
|
+
|
|
714
718
|
elevations = epanet_api.getNodeElevations()
|
|
715
719
|
elevations = self.__global_elevation.apply_batch(elevations)
|
|
716
720
|
epanet_api.setNodeElevations(elevations)
|
|
717
721
|
|
|
718
722
|
if self.__local_elevation is not None:
|
|
723
|
+
self.__local_elevation.set_random_generator(np_rand_gen)
|
|
724
|
+
|
|
719
725
|
for node_id, uncertainty in self.__local_elevation.items():
|
|
720
726
|
node_idx = epanet_api.getNodeIndex(node_id)
|
|
721
727
|
elevation = epanet_api.getNodeElevations(node_idx)
|
|
@@ -723,6 +729,8 @@ class ModelUncertainty(JsonSerializable):
|
|
|
723
729
|
epanet_api.setNodeElevations(node_idx, elevation)
|
|
724
730
|
|
|
725
731
|
if self.__local_patterns is not None:
|
|
732
|
+
self.__local_patterns.set_random_generator(np_rand_gen)
|
|
733
|
+
|
|
726
734
|
for pattern_id, uncertainty in self.__local_patterns.items():
|
|
727
735
|
pattern_idx = epanet_api.getPatternIndex(pattern_id)
|
|
728
736
|
pattern_length = epanet_api.getPatternLengths(pattern_idx)
|
|
@@ -733,11 +741,15 @@ class ModelUncertainty(JsonSerializable):
|
|
|
733
741
|
|
|
734
742
|
if epanet_api.MSXFile is not None:
|
|
735
743
|
if self.__global_constants is not None:
|
|
744
|
+
self.__global_constants.set_random_generator(np_rand_gen)
|
|
745
|
+
|
|
736
746
|
constants = np.array(epanet_api.getMSXConstantsValue())
|
|
737
747
|
constants = self.__global_constants.apply_batch(constants)
|
|
738
748
|
epanet_api.setMSXConstantsValue(constants)
|
|
739
749
|
|
|
740
750
|
if self.__local_constants:
|
|
751
|
+
self.__local_constants.set_random_generator(np_rand_gen)
|
|
752
|
+
|
|
741
753
|
for constant_id, uncertainty in self.__local_constants.items():
|
|
742
754
|
idx = epanet_api.MSXgetindex(ToolkitConstants.MSX_CONSTANT, constant_id)
|
|
743
755
|
constant = epanet_api.msx.MSXgetconstant(idx)
|
|
@@ -745,6 +757,8 @@ class ModelUncertainty(JsonSerializable):
|
|
|
745
757
|
epanet_api.msx.MSXsetconstant(idx, constant)
|
|
746
758
|
|
|
747
759
|
if self.__global_parameters is not None:
|
|
760
|
+
self.__global_parameters.set_random_generator(np_rand_gen)
|
|
761
|
+
|
|
748
762
|
parameters_pipes = epanet_api.getMSXParametersPipesValue()
|
|
749
763
|
for i, pipe_idx in enumerate(epanet_api.getLinkPipeIndex()):
|
|
750
764
|
if len(parameters_pipes[i]) == 0:
|
|
@@ -764,6 +778,8 @@ class ModelUncertainty(JsonSerializable):
|
|
|
764
778
|
epanet_api.setMSXParametersTanksValue(tank_idx, parameters_tanks_val)
|
|
765
779
|
|
|
766
780
|
if self.__local_parameters is not None:
|
|
781
|
+
self.__local_parameters.set_random_generator(np_rand_gen)
|
|
782
|
+
|
|
767
783
|
for (param_id, item_type, item_id), uncertainty in self.__local_parameters.items():
|
|
768
784
|
idx, = epanet_api.getMSXParametersIndex([param_id])
|
|
769
785
|
|
|
@@ -780,6 +796,8 @@ class ModelUncertainty(JsonSerializable):
|
|
|
780
796
|
epanet_api.msx.MSXsetparameter(item_type, item_idx, idx, parameter)
|
|
781
797
|
|
|
782
798
|
if self.__local_msx_patterns is not None:
|
|
799
|
+
self.__local_msx_patterns.set_random_generator(np_rand_gen)
|
|
800
|
+
|
|
783
801
|
for pattern_id, uncertainty in self.__local_msx_patterns.items():
|
|
784
802
|
pattern_idx, = epanet_api.getMSXPatternsIndex([pattern_id])
|
|
785
803
|
pattern = epanet_api.getMSXConstantsValue([pattern_idx])
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
"""
|
|
2
2
|
Module provides a class for implementing sensor noise (e.g. uncertainty in sensor readings).
|
|
3
3
|
"""
|
|
4
|
+
from typing import Optional, Callable
|
|
4
5
|
from copy import deepcopy
|
|
5
6
|
import warnings
|
|
6
|
-
from typing import Callable
|
|
7
7
|
import numpy
|
|
8
|
+
from numpy.random import default_rng
|
|
8
9
|
|
|
9
10
|
from .uncertainties import Uncertainty
|
|
10
11
|
from ..serialization import serializable, JsonSerializable, SENSOR_NOISE_ID
|
|
@@ -25,11 +26,16 @@ class SensorNoise(JsonSerializable):
|
|
|
25
26
|
Local (i.e. sensor specific) uncertainties.
|
|
26
27
|
If None, no local sensor uncertainties are applied.
|
|
27
28
|
|
|
29
|
+
The default is None.
|
|
30
|
+
seed : `int`, optional
|
|
31
|
+
Seed for the random number generator.
|
|
32
|
+
|
|
28
33
|
The default is None.
|
|
29
34
|
"""
|
|
30
|
-
def __init__(self, uncertainty: Uncertainty = None,
|
|
31
|
-
global_uncertainty: Uncertainty = None,
|
|
32
|
-
local_uncertainties: dict[int, str, Uncertainty] = None,
|
|
35
|
+
def __init__(self, uncertainty: Optional[Uncertainty] = None,
|
|
36
|
+
global_uncertainty: Optional[Uncertainty] = None,
|
|
37
|
+
local_uncertainties: Optional[dict[int, str, Uncertainty]] = None,
|
|
38
|
+
seed: Optional[int] = None,
|
|
33
39
|
**kwds):
|
|
34
40
|
if uncertainty is not None:
|
|
35
41
|
global_uncertainty = uncertainty
|
|
@@ -53,6 +59,7 @@ class SensorNoise(JsonSerializable):
|
|
|
53
59
|
|
|
54
60
|
self.__global_uncertainty = global_uncertainty
|
|
55
61
|
self.__local_uncertainties = local_uncertainties
|
|
62
|
+
self.__np_rand_gen = default_rng(seed)
|
|
56
63
|
|
|
57
64
|
super().__init__(**kwds)
|
|
58
65
|
|
|
@@ -118,6 +125,8 @@ class SensorNoise(JsonSerializable):
|
|
|
118
125
|
if self.__local_uncertainties is None:
|
|
119
126
|
return sensor_readings
|
|
120
127
|
else:
|
|
128
|
+
self.__local_uncertainties.set_random_generator(self.__np_rand_gen)
|
|
129
|
+
|
|
121
130
|
for (sensor_type, sensor_id), uncertainty in map_sensor_to_idx.items():
|
|
122
131
|
idx = map_sensor_to_idx(sensor_type, sensor_id)
|
|
123
132
|
sensor_readings[:, idx] = uncertainty.apply_batch(sensor_readings[:, idx])
|
|
@@ -145,4 +154,6 @@ class SensorNoise(JsonSerializable):
|
|
|
145
154
|
if self.__global_uncertainty is None:
|
|
146
155
|
return sensor_readings
|
|
147
156
|
else:
|
|
157
|
+
self.__global_uncertainty.set_random_generator(self.__np_rand_gen)
|
|
158
|
+
|
|
148
159
|
return self.__global_uncertainty.apply_batch(sensor_readings)
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
Module provides classes for implementing different types of uncertainties.
|
|
3
3
|
"""
|
|
4
4
|
from abc import ABC, abstractmethod
|
|
5
|
+
from copy import deepcopy
|
|
5
6
|
import numpy as np
|
|
7
|
+
from numpy.random import Generator, default_rng
|
|
6
8
|
|
|
7
9
|
from .utils import generate_deep_random_gaussian_noise, create_deep_random_pattern
|
|
8
10
|
from ..serialization import serializable, JsonSerializable, ABSOLUTE_GAUSSIAN_UNCERTAINTY_ID, \
|
|
@@ -34,6 +36,8 @@ class Uncertainty(ABC):
|
|
|
34
36
|
self.__min_value = min_value
|
|
35
37
|
self.__max_value = max_value
|
|
36
38
|
|
|
39
|
+
self._random_generator = default_rng()
|
|
40
|
+
|
|
37
41
|
@property
|
|
38
42
|
def min_value(self) -> float:
|
|
39
43
|
"""
|
|
@@ -58,6 +62,32 @@ class Uncertainty(ABC):
|
|
|
58
62
|
"""
|
|
59
63
|
return self.__max_value
|
|
60
64
|
|
|
65
|
+
@property
|
|
66
|
+
def random_generator(self) -> Generator:
|
|
67
|
+
"""
|
|
68
|
+
Returns the random number generator that is used for generating the uncertainties.
|
|
69
|
+
|
|
70
|
+
Returns
|
|
71
|
+
-------
|
|
72
|
+
`numpy.random.Generator <https://numpy.org/doc/stable/reference/random/generator.html#numpy.random.Generator>`_
|
|
73
|
+
The random number generator.
|
|
74
|
+
"""
|
|
75
|
+
return deepcopy(self._random_generator)
|
|
76
|
+
|
|
77
|
+
def set_random_generator(self, np_rand_generator: Generator) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Sets the random number generator that is going to be used for generating the uncertainties.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
np_rand_generator : `numpy.random.Generator <https://numpy.org/doc/stable/reference/random/generator.html#numpy.random.Generator>`_, optional
|
|
84
|
+
The random number generator.
|
|
85
|
+
|
|
86
|
+
The default is the default BitGenerator (PCG64) as constructed by
|
|
87
|
+
`numpy.random.default_rng() <https://numpy.org/doc/stable/reference/random/generator.html#numpy.random.default_rng>`_.
|
|
88
|
+
"""
|
|
89
|
+
self._random_generator = np_rand_generator
|
|
90
|
+
|
|
61
91
|
def get_attributes(self) -> dict:
|
|
62
92
|
"""
|
|
63
93
|
Gets all attributes to be serialized -- these attributes are passed to the
|
|
@@ -75,7 +105,8 @@ class Uncertainty(ABC):
|
|
|
75
105
|
raise TypeError("Can not compare 'Uncertainty' instance " +
|
|
76
106
|
f"with '{type(other)}' instance")
|
|
77
107
|
|
|
78
|
-
return self.__min_value == other.min_value and self.__max_value == other.max_value
|
|
108
|
+
return self.__min_value == other.min_value and self.__max_value == other.max_value and \
|
|
109
|
+
self._random_generator == other.random_generator
|
|
79
110
|
|
|
80
111
|
def __str__(self) -> str:
|
|
81
112
|
return f"min_value: {self.__min_value} max_value: {self.__max_value}"
|
|
@@ -159,8 +190,8 @@ class GaussianUncertainty(Uncertainty):
|
|
|
159
190
|
def __init__(self, mean: float = None, scale: float = None, **kwds):
|
|
160
191
|
super().__init__(**kwds)
|
|
161
192
|
|
|
162
|
-
self.
|
|
163
|
-
self.
|
|
193
|
+
self._mean = mean
|
|
194
|
+
self._scale = scale
|
|
164
195
|
|
|
165
196
|
@property
|
|
166
197
|
def mean(self) -> float:
|
|
@@ -172,7 +203,7 @@ class GaussianUncertainty(Uncertainty):
|
|
|
172
203
|
`float`
|
|
173
204
|
Mean of the Gaussian noise.
|
|
174
205
|
"""
|
|
175
|
-
return self.
|
|
206
|
+
return self._mean
|
|
176
207
|
|
|
177
208
|
@property
|
|
178
209
|
def scale(self) -> float:
|
|
@@ -184,20 +215,20 @@ class GaussianUncertainty(Uncertainty):
|
|
|
184
215
|
`float`
|
|
185
216
|
Scale (i.e. standard deviation) of the Gaussian noise.
|
|
186
217
|
"""
|
|
187
|
-
return self.
|
|
218
|
+
return self._scale
|
|
188
219
|
|
|
189
220
|
def get_attributes(self) -> dict:
|
|
190
|
-
return super().get_attributes() | {"mean": self.
|
|
221
|
+
return super().get_attributes() | {"mean": self._mean, "scale": self._scale}
|
|
191
222
|
|
|
192
223
|
def __eq__(self, other) -> bool:
|
|
193
224
|
if not isinstance(other, GaussianUncertainty):
|
|
194
225
|
raise TypeError("Can not compare 'GaussianUncertainty' instance " +
|
|
195
226
|
f"with '{type(other)}' instance")
|
|
196
227
|
|
|
197
|
-
return super().__eq__(other) and self.
|
|
228
|
+
return super().__eq__(other) and self._mean == other.mean and self._scale == other.scale
|
|
198
229
|
|
|
199
230
|
def __str__(self) -> str:
|
|
200
|
-
return super().__str__() + f" mean: {self.
|
|
231
|
+
return super().__str__() + f" mean: {self._mean} scale: {self._scale}"
|
|
201
232
|
|
|
202
233
|
|
|
203
234
|
@serializable(ABSOLUTE_GAUSSIAN_UNCERTAINTY_ID, ".epytflow_uncertainty_absolute_gaussian")
|
|
@@ -206,7 +237,10 @@ class AbsoluteGaussianUncertainty(GaussianUncertainty, JsonSerializable):
|
|
|
206
237
|
Class implementing absolute Gaussian uncertainty -- i.e. Gaussian noise is added to the data.
|
|
207
238
|
"""
|
|
208
239
|
def apply(self, data: float) -> float:
|
|
209
|
-
|
|
240
|
+
self._mean = self._random_generator.rand() if self._mean is None else self._mean
|
|
241
|
+
self._scale = self._random_generator.rand() if self._scale is None else self._scale
|
|
242
|
+
|
|
243
|
+
data += self._random_generator.normal(loc=self._mean, scale=self._scale)
|
|
210
244
|
|
|
211
245
|
return self.clip(data)
|
|
212
246
|
|
|
@@ -230,7 +264,9 @@ class RelativeGaussianUncertainty(GaussianUncertainty, JsonSerializable):
|
|
|
230
264
|
super().__init__(mean=0., scale=scale, **kwds)
|
|
231
265
|
|
|
232
266
|
def apply(self, data: float) -> float:
|
|
233
|
-
|
|
267
|
+
self._scale = self._random_generator.rand() if self._scale is None else self._scale
|
|
268
|
+
|
|
269
|
+
data += self._random_generator.normal(loc=0, scale=self._scale)
|
|
234
270
|
|
|
235
271
|
return self.clip(data)
|
|
236
272
|
|
|
@@ -300,7 +336,7 @@ class AbsoluteUniformUncertainty(UniformUncertainty, JsonSerializable):
|
|
|
300
336
|
Class implementing absolute uniform uncertainty -- i.e. uniform noise is added to the data.
|
|
301
337
|
"""
|
|
302
338
|
def apply(self, data: float) -> float:
|
|
303
|
-
data +=
|
|
339
|
+
data += self._random_generator.uniform(low=self.low, high=self.high)
|
|
304
340
|
|
|
305
341
|
return self.clip(data)
|
|
306
342
|
|
|
@@ -311,7 +347,7 @@ class RelativeUniformUncertainty(UniformUncertainty, JsonSerializable):
|
|
|
311
347
|
Class implementing relative uniform uncertainty -- i.e. data is multiplied by uniform noise.
|
|
312
348
|
"""
|
|
313
349
|
def apply(self, data: float) -> float:
|
|
314
|
-
data *=
|
|
350
|
+
data *= self._random_generator.uniform(low=self.low, high=self.high)
|
|
315
351
|
|
|
316
352
|
return self.clip(data)
|
|
317
353
|
|
|
@@ -345,7 +381,7 @@ class PercentageDeviationUncertainty(UniformUncertainty, JsonSerializable):
|
|
|
345
381
|
return super().get_attributes() | {"deviation_percentage": self.high - 1.}
|
|
346
382
|
|
|
347
383
|
def apply(self, data: float) -> float:
|
|
348
|
-
data *=
|
|
384
|
+
data *= self._random_generator.uniform(low=self.low, high=self.high)
|
|
349
385
|
|
|
350
386
|
return self.clip(data)
|
|
351
387
|
|
|
@@ -361,13 +397,18 @@ class DeepUniformUncertainty(Uncertainty):
|
|
|
361
397
|
|
|
362
398
|
def __create_uncertainties(self, n_samples: int = 500):
|
|
363
399
|
self._uncertainties_idx = 0
|
|
364
|
-
rand_low = create_deep_random_pattern(n_samples)
|
|
365
|
-
rand_high = create_deep_random_pattern(n_samples)
|
|
400
|
+
rand_low = create_deep_random_pattern(n_samples, np_rand_gen=self._random_generator)
|
|
401
|
+
rand_high = create_deep_random_pattern(n_samples, np_rand_gen=self._random_generator)
|
|
366
402
|
rand_low = np.minimum(rand_low, rand_high)
|
|
367
403
|
rand_high = np.maximum(rand_low, rand_high)
|
|
368
|
-
self._uncertainties = [
|
|
404
|
+
self._uncertainties = [self._random_generator.uniform(low, high)
|
|
369
405
|
for low, high in zip(rand_low, rand_high)]
|
|
370
406
|
|
|
407
|
+
def set_random_generator(self, np_rand_generator) -> None:
|
|
408
|
+
super().set_random_generator(np_rand_generator)
|
|
409
|
+
|
|
410
|
+
self.__create_uncertainties()
|
|
411
|
+
|
|
371
412
|
@abstractmethod
|
|
372
413
|
def apply(self, data: float) -> float:
|
|
373
414
|
self._uncertainties_idx += 1
|
|
@@ -422,7 +463,13 @@ class DeepGaussianUncertainty(Uncertainty, JsonSerializable):
|
|
|
422
463
|
|
|
423
464
|
def __create_uncertainties(self, n_samples: int = 500) -> None:
|
|
424
465
|
self._uncertainties_idx = 0
|
|
425
|
-
self._uncertainties = generate_deep_random_gaussian_noise(n_samples, self.__mean
|
|
466
|
+
self._uncertainties = generate_deep_random_gaussian_noise(n_samples, self.__mean,
|
|
467
|
+
np_rand_gen=self._random_generator)
|
|
468
|
+
|
|
469
|
+
def set_random_generator(self, np_rand_generator) -> None:
|
|
470
|
+
super().set_random_generator(np_rand_generator)
|
|
471
|
+
|
|
472
|
+
self.__create_uncertainties()
|
|
426
473
|
|
|
427
474
|
@abstractmethod
|
|
428
475
|
def apply(self, data: float) -> float:
|
|
@@ -505,6 +552,11 @@ class DeepUncertainty(Uncertainty):
|
|
|
505
552
|
"""
|
|
506
553
|
return self.__max_noise_value
|
|
507
554
|
|
|
555
|
+
def set_random_generator(self, np_rand_generator) -> None:
|
|
556
|
+
super().set_random_generator(np_rand_generator)
|
|
557
|
+
|
|
558
|
+
self.__create_uncertainties()
|
|
559
|
+
|
|
508
560
|
def get_attributes(self) -> dict:
|
|
509
561
|
return super().get_attributes() | {"min_noise_value": self.__min_noise_value,
|
|
510
562
|
"max_noise_value": self.__max_noise_value}
|
|
@@ -528,7 +580,8 @@ class DeepUncertainty(Uncertainty):
|
|
|
528
580
|
|
|
529
581
|
self._uncertainties_idx = 0
|
|
530
582
|
self._uncertainties = create_deep_random_pattern(n_samples, self.__min_noise_value,
|
|
531
|
-
self.__max_noise_value, init_value
|
|
583
|
+
self.__max_noise_value, init_value,
|
|
584
|
+
np_rand_gen=self._random_generator)
|
|
532
585
|
|
|
533
586
|
@abstractmethod
|
|
534
587
|
def apply(self, data: float) -> float:
|