epyt-flow 0.10.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 +8 -3
- 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 +1 -0
- epyt_flow/simulation/__init__.py +0 -1
- epyt_flow/simulation/events/actuator_events.py +7 -1
- epyt_flow/simulation/scada/scada_data.py +527 -5
- epyt_flow/simulation/scenario_config.py +1 -40
- epyt_flow/simulation/scenario_simulator.py +511 -68
- epyt_flow/simulation/sensor_config.py +18 -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.10.0.dist-info → epyt_flow-0.11.0.dist-info}/METADATA +12 -1
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.11.0.dist-info}/RECORD +28 -26
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.11.0.dist-info}/WHEEL +1 -1
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.11.0.dist-info}/LICENSE +0 -0
- {epyt_flow-0.10.0.dist-info → epyt_flow-0.11.0.dist-info}/top_level.txt +0 -0
|
@@ -7,7 +7,7 @@ import pathlib
|
|
|
7
7
|
import time
|
|
8
8
|
from datetime import timedelta
|
|
9
9
|
from datetime import datetime
|
|
10
|
-
from typing import Generator, Union
|
|
10
|
+
from typing import Generator, Union, Optional
|
|
11
11
|
from copy import deepcopy
|
|
12
12
|
import shutil
|
|
13
13
|
import warnings
|
|
@@ -112,7 +112,6 @@ class ScenarioSimulator():
|
|
|
112
112
|
self._model_uncertainty = ModelUncertainty()
|
|
113
113
|
self._sensor_noise = None
|
|
114
114
|
self._sensor_config = None
|
|
115
|
-
self._advanced_controls = []
|
|
116
115
|
self._custom_controls = []
|
|
117
116
|
self._simple_controls = []
|
|
118
117
|
self._complex_controls = []
|
|
@@ -190,9 +189,6 @@ class ScenarioSimulator():
|
|
|
190
189
|
self._sensor_noise = scenario_config.sensor_noise
|
|
191
190
|
self._sensor_config = scenario_config.sensor_config
|
|
192
191
|
|
|
193
|
-
if scenario_config.advanced_controls is not None:
|
|
194
|
-
for control in scenario_config.advanced_controls:
|
|
195
|
-
self.add_advanced_control(control)
|
|
196
192
|
for control in scenario_config.custom_controls:
|
|
197
193
|
self.add_custom_control(control)
|
|
198
194
|
for control in scenario_config.simple_controls:
|
|
@@ -342,22 +338,6 @@ class ScenarioSimulator():
|
|
|
342
338
|
|
|
343
339
|
self._sensor_config = sensor_config
|
|
344
340
|
|
|
345
|
-
@property
|
|
346
|
-
def advanced_controls(self) -> list:
|
|
347
|
-
"""
|
|
348
|
-
Returns all advanced control modules.
|
|
349
|
-
|
|
350
|
-
Returns
|
|
351
|
-
-------
|
|
352
|
-
list[:class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`]
|
|
353
|
-
All advanced control modules.
|
|
354
|
-
"""
|
|
355
|
-
warnings.warn("'AdvancedControlModule' is deprecated and will be removed in a " +
|
|
356
|
-
"future release -- use 'CustomControlModule' instead")
|
|
357
|
-
self._adapt_to_network_changes()
|
|
358
|
-
|
|
359
|
-
return deepcopy(self._advanced_controls)
|
|
360
|
-
|
|
361
341
|
@property
|
|
362
342
|
def custom_controls(self) -> list[CustomControlModule]:
|
|
363
343
|
"""
|
|
@@ -990,7 +970,6 @@ class ScenarioSimulator():
|
|
|
990
970
|
return ScenarioConfig(f_inp_in=self.__f_inp_in, f_msx_in=self.__f_msx_in,
|
|
991
971
|
general_params=general_params, sensor_config=self.sensor_config,
|
|
992
972
|
memory_consumption_estimate=self.estimate_memory_consumption(),
|
|
993
|
-
advanced_controls=None if len(self._advanced_controls) == 0 else self.advanced_controls,
|
|
994
973
|
custom_controls=self.custom_controls,
|
|
995
974
|
simple_controls=self.simple_controls,
|
|
996
975
|
complex_controls=self.complex_controls,
|
|
@@ -1041,6 +1020,7 @@ class ScenarioSimulator():
|
|
|
1041
1020
|
nodes_type = [self.epanet_api.TYPENODE[i] for i in self.epanet_api.getNodeTypeIndex()]
|
|
1042
1021
|
nodes_coord = [self.epanet_api.api.ENgetcoord(node_idx)
|
|
1043
1022
|
for node_idx in self.epanet_api.getNodeIndex()]
|
|
1023
|
+
nodes_comments = self.epanet_api.getNodeComment()
|
|
1044
1024
|
node_tank_names = self.epanet_api.getNodeTankNameID()
|
|
1045
1025
|
|
|
1046
1026
|
links_id = self.epanet_api.getLinkNameID()
|
|
@@ -1060,10 +1040,12 @@ class ScenarioSimulator():
|
|
|
1060
1040
|
|
|
1061
1041
|
# Build graph describing the topology
|
|
1062
1042
|
nodes = []
|
|
1063
|
-
for node_id, node_elevation, node_type,
|
|
1064
|
-
|
|
1043
|
+
for node_id, node_elevation, node_type, \
|
|
1044
|
+
node_coord, node_comment in zip(nodes_id, nodes_elevation, nodes_type, nodes_coord,
|
|
1045
|
+
nodes_comments):
|
|
1065
1046
|
node_info = {"elevation": node_elevation,
|
|
1066
1047
|
"coord": node_coord,
|
|
1048
|
+
"comment": node_comment,
|
|
1067
1049
|
"type": node_type}
|
|
1068
1050
|
if node_type == "TANK":
|
|
1069
1051
|
node_tank_idx = node_tank_names.index(node_id) + 1
|
|
@@ -1072,9 +1054,10 @@ class ScenarioSimulator():
|
|
|
1072
1054
|
nodes.append((node_id, node_info))
|
|
1073
1055
|
|
|
1074
1056
|
links = []
|
|
1075
|
-
for link_id, link_type, link, diameter, length, roughness_coeff, bulk_coeff,
|
|
1076
|
-
|
|
1077
|
-
|
|
1057
|
+
for link_id, link_type, link, diameter, length, roughness_coeff, bulk_coeff, \
|
|
1058
|
+
wall_coeff, loss_coeff in zip(links_id, links_type, links_data, links_diameter,
|
|
1059
|
+
links_length, links_roughness_coeff, links_bulk_coeff,
|
|
1060
|
+
links_wall_coeff, links_loss_coeff):
|
|
1078
1061
|
links.append((link_id, list(link),
|
|
1079
1062
|
{"type": link_type, "diameter": diameter, "length": length,
|
|
1080
1063
|
"roughness_coeff": roughness_coeff,
|
|
@@ -1110,7 +1093,7 @@ class ScenarioSimulator():
|
|
|
1110
1093
|
|
|
1111
1094
|
The default is None.
|
|
1112
1095
|
"""
|
|
1113
|
-
from
|
|
1096
|
+
from ..visualization import ScenarioVisualizer
|
|
1114
1097
|
ScenarioVisualizer(self).show_plot(export_to_file)
|
|
1115
1098
|
|
|
1116
1099
|
def randomize_demands(self) -> None:
|
|
@@ -1183,7 +1166,10 @@ class ScenarioSimulator():
|
|
|
1183
1166
|
raise ValueError(f"Inconsistent pattern shape '{pattern.shape}' " +
|
|
1184
1167
|
"detected. Expected a one dimensional array!")
|
|
1185
1168
|
|
|
1186
|
-
self.epanet_api.addPattern(pattern_id, pattern)
|
|
1169
|
+
pattern_idx = self.epanet_api.addPattern(pattern_id, pattern)
|
|
1170
|
+
if pattern_idx == 0:
|
|
1171
|
+
raise RuntimeError("Failed to add pattern! " +
|
|
1172
|
+
"Maybe pattern name contains invalid characters or is too long?")
|
|
1187
1173
|
|
|
1188
1174
|
def get_node_demand_pattern(self, node_id: str) -> np.ndarray:
|
|
1189
1175
|
"""
|
|
@@ -1257,25 +1243,6 @@ class ScenarioSimulator():
|
|
|
1257
1243
|
self.epanet_api.setNodeJunctionData(node_idx, self.epanet_api.getNodeElevations(node_idx),
|
|
1258
1244
|
base_demand, demand_pattern_id)
|
|
1259
1245
|
|
|
1260
|
-
def add_advanced_control(self, control) -> None:
|
|
1261
|
-
"""
|
|
1262
|
-
Adds an advanced control module to the scenario simulation.
|
|
1263
|
-
|
|
1264
|
-
Parameters
|
|
1265
|
-
----------
|
|
1266
|
-
control : :class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`
|
|
1267
|
-
Advanced control module.
|
|
1268
|
-
"""
|
|
1269
|
-
self._adapt_to_network_changes()
|
|
1270
|
-
|
|
1271
|
-
from .scada.advanced_control import AdvancedControlModule
|
|
1272
|
-
if not isinstance(control, AdvancedControlModule):
|
|
1273
|
-
raise TypeError("'control' must be an instance of " +
|
|
1274
|
-
"'epyt_flow.simulation.scada.AdvancedControlModule' not of " +
|
|
1275
|
-
f"'{type(control)}'")
|
|
1276
|
-
|
|
1277
|
-
self._advanced_controls.append(control)
|
|
1278
|
-
|
|
1279
1246
|
def add_custom_control(self, control: CustomControlModule) -> None:
|
|
1280
1247
|
"""
|
|
1281
1248
|
Adds a custom control module to the scenario simulation.
|
|
@@ -1934,9 +1901,6 @@ class ScenarioSimulator():
|
|
|
1934
1901
|
for event in self._system_events:
|
|
1935
1902
|
event.reset()
|
|
1936
1903
|
|
|
1937
|
-
if self._advanced_controls is not None:
|
|
1938
|
-
for c in self._advanced_controls:
|
|
1939
|
-
c.init(self.epanet_api)
|
|
1940
1904
|
if self._custom_controls is not None:
|
|
1941
1905
|
for control in self._custom_controls:
|
|
1942
1906
|
control.init(self.epanet_api)
|
|
@@ -2006,6 +1970,7 @@ class ScenarioSimulator():
|
|
|
2006
1970
|
result[data_type] = None
|
|
2007
1971
|
|
|
2008
1972
|
return ScadaData(**result,
|
|
1973
|
+
network_topo=self.get_topology(),
|
|
2009
1974
|
sensor_config=self._sensor_config,
|
|
2010
1975
|
sensor_reading_events=self._sensor_reading_events,
|
|
2011
1976
|
sensor_noise=self._sensor_noise,
|
|
@@ -2159,7 +2124,7 @@ class ScenarioSimulator():
|
|
|
2159
2124
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
2160
2125
|
"sensor_readings_time": np.array([0])}
|
|
2161
2126
|
else:
|
|
2162
|
-
data = ScadaData(sensor_config=self._sensor_config,
|
|
2127
|
+
data = ScadaData(network_topo=self.get_topology(), sensor_config=self._sensor_config,
|
|
2163
2128
|
bulk_species_node_concentration_raw=bulk_species_node_concentrations,
|
|
2164
2129
|
bulk_species_link_concentration_raw=bulk_species_link_concentrations,
|
|
2165
2130
|
surface_species_concentration_raw=surface_species_concentrations,
|
|
@@ -2203,7 +2168,8 @@ class ScenarioSimulator():
|
|
|
2203
2168
|
"surface_species_concentration_raw": surface_species_concentrations,
|
|
2204
2169
|
"sensor_readings_time": np.array([total_time])}
|
|
2205
2170
|
else:
|
|
2206
|
-
data = ScadaData(
|
|
2171
|
+
data = ScadaData(network_topo=self.get_topology(),
|
|
2172
|
+
sensor_config=self._sensor_config,
|
|
2207
2173
|
bulk_species_node_concentration_raw=
|
|
2208
2174
|
bulk_species_node_concentrations,
|
|
2209
2175
|
bulk_species_link_concentration_raw=
|
|
@@ -2284,6 +2250,7 @@ class ScenarioSimulator():
|
|
|
2284
2250
|
result[data_type] = np.concatenate(result[data_type], axis=0)
|
|
2285
2251
|
|
|
2286
2252
|
return ScadaData(**result,
|
|
2253
|
+
network_topo=self.get_topology(),
|
|
2287
2254
|
sensor_config=self._sensor_config,
|
|
2288
2255
|
sensor_reading_events=self._sensor_reading_events,
|
|
2289
2256
|
sensor_noise=self._sensor_noise,
|
|
@@ -2385,7 +2352,8 @@ class ScenarioSimulator():
|
|
|
2385
2352
|
"link_quality_data_raw": quality_link_data,
|
|
2386
2353
|
"sensor_readings_time": np.array([total_time])}
|
|
2387
2354
|
else:
|
|
2388
|
-
data = ScadaData(
|
|
2355
|
+
data = ScadaData(network_topo=self.get_topology(),
|
|
2356
|
+
sensor_config=self._sensor_config,
|
|
2389
2357
|
node_quality_data_raw=quality_node_data,
|
|
2390
2358
|
link_quality_data_raw=quality_link_data,
|
|
2391
2359
|
sensor_readings_time=np.array([total_time]),
|
|
@@ -2461,6 +2429,7 @@ class ScenarioSimulator():
|
|
|
2461
2429
|
result[data_type] = np.concatenate(result[data_type], axis=0)
|
|
2462
2430
|
|
|
2463
2431
|
result = ScadaData(**result,
|
|
2432
|
+
network_topo=self.get_topology(),
|
|
2464
2433
|
sensor_config=self._sensor_config,
|
|
2465
2434
|
sensor_reading_events=self._sensor_reading_events,
|
|
2466
2435
|
sensor_noise=self._sensor_noise,
|
|
@@ -2582,7 +2551,8 @@ class ScenarioSimulator():
|
|
|
2582
2551
|
link_valve_idx = self.epanet_api.getLinkValveIndex()
|
|
2583
2552
|
valves_state_data = self.epanet_api.getLinkStatus(link_valve_idx).reshape(1, -1)
|
|
2584
2553
|
|
|
2585
|
-
scada_data = ScadaData(
|
|
2554
|
+
scada_data = ScadaData(network_topo=self.get_topology(),
|
|
2555
|
+
sensor_config=self._sensor_config,
|
|
2586
2556
|
pressure_data_raw=pressure_data,
|
|
2587
2557
|
flow_data_raw=flow_data,
|
|
2588
2558
|
demand_data_raw=demand_data,
|
|
@@ -2623,8 +2593,6 @@ class ScenarioSimulator():
|
|
|
2623
2593
|
yield data
|
|
2624
2594
|
|
|
2625
2595
|
# Apply control modules
|
|
2626
|
-
for control in self._advanced_controls:
|
|
2627
|
-
control.step(scada_data)
|
|
2628
2596
|
for control in self._custom_controls:
|
|
2629
2597
|
control.step(scada_data)
|
|
2630
2598
|
|
|
@@ -2643,16 +2611,6 @@ class ScenarioSimulator():
|
|
|
2643
2611
|
self.__running_simulation = False
|
|
2644
2612
|
raise ex
|
|
2645
2613
|
|
|
2646
|
-
def run_simulation_as_generator(self, hyd_export: str = None, verbose: bool = False,
|
|
2647
|
-
support_abort: bool = False,
|
|
2648
|
-
return_as_dict: bool = False,
|
|
2649
|
-
frozen_sensor_config: bool = False,
|
|
2650
|
-
) -> Generator[Union[ScadaData, dict], bool, None]:
|
|
2651
|
-
warnings.warn("'run_simulation_as_generator' is deprecated and will be removed in " +
|
|
2652
|
-
"future releases -- use 'run_hydraulic_simulation_as_generator' instead")
|
|
2653
|
-
return self.run_hydraulic_simulation_as_generator(hyd_export, verbose, support_abort,
|
|
2654
|
-
return_as_dict, frozen_sensor_config)
|
|
2655
|
-
|
|
2656
2614
|
def run_simulation(self, hyd_export: str = None, verbose: bool = False,
|
|
2657
2615
|
frozen_sensor_config: bool = False) -> ScadaData:
|
|
2658
2616
|
"""
|
|
@@ -2945,6 +2903,202 @@ class ScenarioSimulator():
|
|
|
2945
2903
|
|
|
2946
2904
|
return list(set(events_times))
|
|
2947
2905
|
|
|
2906
|
+
def set_pump_energy_price_pattern(self, pump_id: str, pattern: np.ndarray,
|
|
2907
|
+
pattern_id: Optional[str] = None) -> None:
|
|
2908
|
+
"""
|
|
2909
|
+
Specifies/sets the energy price pattern of a given pump.
|
|
2910
|
+
|
|
2911
|
+
Overwrites any existing (energy price) patterns of the given pump.
|
|
2912
|
+
|
|
2913
|
+
Parameters
|
|
2914
|
+
----------
|
|
2915
|
+
pump_id : `str`
|
|
2916
|
+
ID of the pump.
|
|
2917
|
+
pattern : `numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
2918
|
+
Pattern of multipliers.
|
|
2919
|
+
pattern_id : `str`, optional
|
|
2920
|
+
ID of the pattern.
|
|
2921
|
+
If not specified, 'energy_price_{pump_id}' will be used as the pattern ID.
|
|
2922
|
+
|
|
2923
|
+
The default is None.
|
|
2924
|
+
"""
|
|
2925
|
+
if not isinstance(pump_id, str):
|
|
2926
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
2927
|
+
if pump_id not in self._sensor_config.pumps:
|
|
2928
|
+
raise ValueError(f"Unknown pump '{pump_id}'")
|
|
2929
|
+
if not isinstance(pattern, np.ndarray):
|
|
2930
|
+
raise TypeError("'pattern' must be an instance of 'numpy.ndarray' " +
|
|
2931
|
+
f"but no of '{type(pattern)}'")
|
|
2932
|
+
if len(pattern.shape) > 1:
|
|
2933
|
+
raise ValueError("'pattern' must be 1-dimensional")
|
|
2934
|
+
if pattern_id is not None:
|
|
2935
|
+
if not isinstance(pattern_id, str):
|
|
2936
|
+
raise TypeError("'pattern_id' must be an instance of 'str' " +
|
|
2937
|
+
f"but not of '{type(pattern_id)}'")
|
|
2938
|
+
else:
|
|
2939
|
+
pattern_id = f"energy_price_{pump_id}"
|
|
2940
|
+
|
|
2941
|
+
pattern_idx = self.epanet_api.getPatternIndex(pattern_id)
|
|
2942
|
+
if pattern_idx != 0:
|
|
2943
|
+
warnings.warn(f"Overwriting existing pattern '{pattern_id}'")
|
|
2944
|
+
|
|
2945
|
+
pump_idx = self.epanet_api.getLinkIndex(pump_id)
|
|
2946
|
+
pattern_idx = self.epanet_api.getLinkPumpEPat(pump_idx)
|
|
2947
|
+
if pattern_idx != 0:
|
|
2948
|
+
warnings.warn(f"Overwriting existing energy price pattern of pump '{pump_id}'")
|
|
2949
|
+
|
|
2950
|
+
self.add_pattern(pattern_id, pattern)
|
|
2951
|
+
pattern_idx = self.epanet_api.getPatternIndex(pattern_id)
|
|
2952
|
+
self.epanet_api.setLinkPumpEPat(pattern_idx)
|
|
2953
|
+
|
|
2954
|
+
def get_pump_energy_price_pattern(self, pump_id: str) -> np.ndarray:
|
|
2955
|
+
"""
|
|
2956
|
+
Returns the energy price pattern of a given pump.
|
|
2957
|
+
|
|
2958
|
+
Parameters
|
|
2959
|
+
----------
|
|
2960
|
+
pump_id : `str`
|
|
2961
|
+
ID of the pump.
|
|
2962
|
+
|
|
2963
|
+
Returns
|
|
2964
|
+
-------
|
|
2965
|
+
`numpy.ndarray <https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html>`_
|
|
2966
|
+
Energy price pattern. None, if none exists.
|
|
2967
|
+
"""
|
|
2968
|
+
if not isinstance(pump_id, str):
|
|
2969
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
2970
|
+
if pump_id not in self._sensor_config.pumps:
|
|
2971
|
+
raise ValueError(f"Unknown pump '{pump_id}'")
|
|
2972
|
+
|
|
2973
|
+
pump_idx = self.epanet_api.getLinkIndex(pump_id)
|
|
2974
|
+
pattern_idx = self.epanet_api.getLinkPumpEPat(pump_idx)
|
|
2975
|
+
if pattern_idx == 0:
|
|
2976
|
+
return None
|
|
2977
|
+
else:
|
|
2978
|
+
pattern_length = self.epanet_api.getPatternLengths(pattern_idx)
|
|
2979
|
+
return np.array([self.epanet_api.getPatternValue(pattern_idx, t+1)
|
|
2980
|
+
for t in range(pattern_length)])
|
|
2981
|
+
|
|
2982
|
+
def get_pump_energy_price(self, pump_id: str) -> float:
|
|
2983
|
+
"""
|
|
2984
|
+
Returns the energy price of a given pump.
|
|
2985
|
+
|
|
2986
|
+
Parameters
|
|
2987
|
+
----------
|
|
2988
|
+
pump_id : `str`
|
|
2989
|
+
ID of the pump.
|
|
2990
|
+
|
|
2991
|
+
Returns
|
|
2992
|
+
-------
|
|
2993
|
+
`float`
|
|
2994
|
+
Energy price.
|
|
2995
|
+
"""
|
|
2996
|
+
if not isinstance(pump_id, str):
|
|
2997
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
2998
|
+
if pump_id not in self._sensor_config.pumps:
|
|
2999
|
+
raise ValueError(f"Unknown pump '{pump_id}'")
|
|
3000
|
+
|
|
3001
|
+
pump_idx = self.epanet_api.getLinkIndex(pump_id)
|
|
3002
|
+
return self.epanet_api.getLinkPumpECost(pump_idx)
|
|
3003
|
+
|
|
3004
|
+
def set_pump_energy_price(self, pump_id, price: float) -> None:
|
|
3005
|
+
"""
|
|
3006
|
+
Sets the energy price of a given pump.
|
|
3007
|
+
|
|
3008
|
+
Parameters
|
|
3009
|
+
----------
|
|
3010
|
+
pump_id : `str`
|
|
3011
|
+
ID of the pump.
|
|
3012
|
+
price : `float`
|
|
3013
|
+
Energy price.
|
|
3014
|
+
"""
|
|
3015
|
+
if not isinstance(pump_id, str):
|
|
3016
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
3017
|
+
if pump_id not in self._sensor_config.pumps:
|
|
3018
|
+
raise ValueError(f"Unknown pump '{pump_id}'")
|
|
3019
|
+
if not isinstance(price, float):
|
|
3020
|
+
raise TypeError(f"'price' must be an instance of 'float' but not of '{type(price)}'")
|
|
3021
|
+
if price <= 0:
|
|
3022
|
+
raise ValueError("'price' must be positive")
|
|
3023
|
+
|
|
3024
|
+
pump_idx = self._sensor_config.pumps.index(pump_id) + 1
|
|
3025
|
+
pumps_energy_price = self.epanet_api.getLinkPumpECost()
|
|
3026
|
+
pumps_energy_price[pump_idx - 1] = price
|
|
3027
|
+
|
|
3028
|
+
self.epanet_api.setLinkPumpECost(pumps_energy_price)
|
|
3029
|
+
|
|
3030
|
+
def set_initial_link_status(self, link_id: str, status: int) -> None:
|
|
3031
|
+
"""
|
|
3032
|
+
Sets the initial status (open or closed) of a given link.
|
|
3033
|
+
|
|
3034
|
+
Parameters
|
|
3035
|
+
----------
|
|
3036
|
+
link_id : `str`
|
|
3037
|
+
ID of the link.
|
|
3038
|
+
status : `int`
|
|
3039
|
+
Initial status of the link. Must be one of the following EPANET constants:
|
|
3040
|
+
|
|
3041
|
+
- EN_CLOSED = 0
|
|
3042
|
+
- EN_OPEN = 1
|
|
3043
|
+
"""
|
|
3044
|
+
if not isinstance(link_id, str):
|
|
3045
|
+
raise TypeError(f"'link_id' must be an instance of 'str' but not of '{type(link_id)}'")
|
|
3046
|
+
if link_id not in self._sensor_config.pumps:
|
|
3047
|
+
raise ValueError("Invalid link ID '{link_id}'")
|
|
3048
|
+
if not isinstance(status, int):
|
|
3049
|
+
raise TypeError(f"'status' must be an instance of 'int' but not of '{type(status)}'")
|
|
3050
|
+
if status not in [ActuatorConstants.EN_CLOSED, ActuatorConstants.EN_OPEN]:
|
|
3051
|
+
raise ValueError(f"Invalid link status '{status}'")
|
|
3052
|
+
|
|
3053
|
+
link_idx = self.epanet_api.getLinkIndex(link_id)
|
|
3054
|
+
self.epanet_api.setLinkInitialStatus(link_idx, status)
|
|
3055
|
+
|
|
3056
|
+
def set_initial_pump_speed(self, pump_id: str, speed: float) -> None:
|
|
3057
|
+
"""
|
|
3058
|
+
Sets the initial pump speed of a given pump.
|
|
3059
|
+
|
|
3060
|
+
Parameters
|
|
3061
|
+
----------
|
|
3062
|
+
pump_id : `str`
|
|
3063
|
+
ID of the pump.
|
|
3064
|
+
speed : `float`
|
|
3065
|
+
Initial speed of the pump.
|
|
3066
|
+
"""
|
|
3067
|
+
if not isinstance(pump_id, str):
|
|
3068
|
+
raise TypeError(f"'pump_id' must be an instance of 'str' but not of '{type(pump_id)}'")
|
|
3069
|
+
if pump_id not in self._sensor_config.pumps:
|
|
3070
|
+
raise ValueError("Invalid pump ID '{tank_id}'")
|
|
3071
|
+
if not isinstance(speed, float):
|
|
3072
|
+
raise TypeError(f"'speed' must be an instance of 'int' but not of '{type(speed)}'")
|
|
3073
|
+
if speed < 0:
|
|
3074
|
+
raise ValueError("'speed' can not be negative")
|
|
3075
|
+
|
|
3076
|
+
pump_idx = self.epanet_api.getLinkIndex(pump_id)
|
|
3077
|
+
self.epanet_api.setLinkInitialSetting(pump_idx, speed)
|
|
3078
|
+
|
|
3079
|
+
def set_initial_tank_level(self, tank_id, level: int) -> None:
|
|
3080
|
+
"""
|
|
3081
|
+
Sets the initial water level of a given tank.
|
|
3082
|
+
|
|
3083
|
+
Parameters
|
|
3084
|
+
----------
|
|
3085
|
+
tank_id : `str`
|
|
3086
|
+
ID of the tank.
|
|
3087
|
+
level : `int`
|
|
3088
|
+
Initial water level in the tank.
|
|
3089
|
+
"""
|
|
3090
|
+
if not isinstance(tank_id, str):
|
|
3091
|
+
raise TypeError(f"'tank_id' must be an instance of 'str' but not of '{type(tank_id)}'")
|
|
3092
|
+
if tank_id not in self._sensor_config.tanks:
|
|
3093
|
+
raise ValueError("Invalid tank ID '{tank_id}'")
|
|
3094
|
+
if not isinstance(level, int):
|
|
3095
|
+
raise TypeError(f"'level' must be an instance of 'int' but not of '{type(level)}'")
|
|
3096
|
+
if level < 0:
|
|
3097
|
+
raise ValueError("'level' can not be negative")
|
|
3098
|
+
|
|
3099
|
+
tank_idx = self.epanet_api.getNodeIndex(tank_id)
|
|
3100
|
+
self.epanet_api.setNodeTankInitialLevel(tank_idx, level)
|
|
3101
|
+
|
|
2948
3102
|
def __warn_if_quality_set(self):
|
|
2949
3103
|
qual_info = self.epanet_api.getQualityInfo()
|
|
2950
3104
|
if qual_info.QualityCode != ToolkitConstants.EN_NONE:
|
|
@@ -3057,7 +3211,7 @@ class ScenarioSimulator():
|
|
|
3057
3211
|
if pattern is None and pattern_id is None:
|
|
3058
3212
|
raise ValueError("'pattern_id' and 'pattern' can not be None at the same time")
|
|
3059
3213
|
if pattern_id is None:
|
|
3060
|
-
pattern_id = f"
|
|
3214
|
+
pattern_id = f"qual_src_pat_{node_id}"
|
|
3061
3215
|
|
|
3062
3216
|
node_idx = self.epanet_api.getNodeIndex(node_id)
|
|
3063
3217
|
|
|
@@ -3065,11 +3219,220 @@ class ScenarioSimulator():
|
|
|
3065
3219
|
pattern_idx = self.epanet_api.getPatternIndex(pattern_id)
|
|
3066
3220
|
else:
|
|
3067
3221
|
pattern_idx = self.epanet_api.addPattern(pattern_id, pattern)
|
|
3222
|
+
if pattern_idx == 0:
|
|
3223
|
+
raise RuntimeError("Failed to add/get pattern! " +
|
|
3224
|
+
"Maybe pattern name contains invalid characters or is too long?")
|
|
3068
3225
|
|
|
3069
3226
|
self.epanet_api.api.ENsetnodevalue(node_idx, ToolkitConstants.EN_SOURCETYPE, source_type)
|
|
3070
3227
|
self.epanet_api.setNodeSourceQuality(node_idx, source_strength)
|
|
3071
3228
|
self.epanet_api.setNodeSourcePatternIndex(node_idx, pattern_idx)
|
|
3072
3229
|
|
|
3230
|
+
def set_initial_node_quality(self, node_id: str, initial_quality: float) -> None:
|
|
3231
|
+
"""
|
|
3232
|
+
Specifies the initial quality at a given node.
|
|
3233
|
+
Quality represents concentration for chemicals, hours for water age,
|
|
3234
|
+
or percent for source tracing.
|
|
3235
|
+
|
|
3236
|
+
Parameters
|
|
3237
|
+
----------
|
|
3238
|
+
node_id : `str`
|
|
3239
|
+
ID of the node.
|
|
3240
|
+
initial_quality : `float`
|
|
3241
|
+
Initial node quality.
|
|
3242
|
+
"""
|
|
3243
|
+
self.set_quality_parameters(initial_quality={node_id, initial_quality})
|
|
3244
|
+
|
|
3245
|
+
def set_quality_parameters(self, initial_quality: Optional[dict[str, float]] = None,
|
|
3246
|
+
order_wall: Optional[int] = None, order_tank: Optional[int] = None,
|
|
3247
|
+
order_bulk: Optional[int] = None,
|
|
3248
|
+
global_wall_reaction_coefficient: Optional[float] = None,
|
|
3249
|
+
global_bulk_reaction_coefficient: Optional[float] = None,
|
|
3250
|
+
local_wall_reaction_coefficient: Optional[dict[str, float]] = None,
|
|
3251
|
+
local_bulk_reaction_coefficient: Optional[dict[str, float]] = None,
|
|
3252
|
+
local_tank_reaction_coefficient: Optional[dict[str, float]] = None,
|
|
3253
|
+
limiting_potential: Optional[float] = None) -> None:
|
|
3254
|
+
"""
|
|
3255
|
+
Specifies some parameters of the EPANET quality analysis.
|
|
3256
|
+
Note that those parameters are only relevant for EPANET but not for EPANET-MSX.
|
|
3257
|
+
|
|
3258
|
+
Parameters
|
|
3259
|
+
----------
|
|
3260
|
+
initial_quality : `dict[str, float]`, optional
|
|
3261
|
+
Specifies the initial quality (value in the dictionary) at nodes
|
|
3262
|
+
(key in the dictionary).
|
|
3263
|
+
Quality represents concentration for chemicals, hours for water age,
|
|
3264
|
+
or percent for source tracing.
|
|
3265
|
+
|
|
3266
|
+
The default is None.
|
|
3267
|
+
order_wall : `int`, optional
|
|
3268
|
+
Specifies the order of reactions occurring in the bulk fluid at pipe walls.
|
|
3269
|
+
Value for wall reactions must be either 0 or 1.
|
|
3270
|
+
If not specified, the default reaction order is 1.0.
|
|
3271
|
+
|
|
3272
|
+
The default is None.
|
|
3273
|
+
order_bulk : `int`, optional
|
|
3274
|
+
Specifies the order of reactions occurring in the bulk fluid in tanks.
|
|
3275
|
+
Value must be either 0 or 1.
|
|
3276
|
+
If not specified, the default reaction order is 1.0.
|
|
3277
|
+
|
|
3278
|
+
The default is None.
|
|
3279
|
+
global_wall_reaction_coefficient : `float`, optional
|
|
3280
|
+
Specifies the global value for all pipe wall reaction coefficients (pipes and tanks).
|
|
3281
|
+
If not specified, the default value is zero.
|
|
3282
|
+
|
|
3283
|
+
The default is None.
|
|
3284
|
+
global_bulk_reaction_coefficient : `float`, optional
|
|
3285
|
+
Specifies the global value for all bulk reaction coefficients (pipes and tanks).
|
|
3286
|
+
If not specified, the default value is zero.
|
|
3287
|
+
|
|
3288
|
+
The default is None.
|
|
3289
|
+
local_wall_reaction_coefficient : `dict[str, float]`, optional
|
|
3290
|
+
Overrides the global reaction coefficients for specific pipes (key in dictionary).
|
|
3291
|
+
|
|
3292
|
+
The default is None.
|
|
3293
|
+
local_bulk_reaction_coefficient : `dict[str, float]`, optional
|
|
3294
|
+
Overrides the global reaction coefficients for specific pipes (key in dictionary).
|
|
3295
|
+
|
|
3296
|
+
The default is None.
|
|
3297
|
+
local_tank_reaction_coefficient : `dict[str, float]`, optional
|
|
3298
|
+
Overrides the global reaction coefficients for specific tanks (key in dictionary).
|
|
3299
|
+
|
|
3300
|
+
The default is None.
|
|
3301
|
+
limiting_potential : `float`, optional
|
|
3302
|
+
Specifies that reaction rates are proportional to the difference between the
|
|
3303
|
+
current concentration and some (specified) limiting potential value.
|
|
3304
|
+
|
|
3305
|
+
The default is None.
|
|
3306
|
+
"""
|
|
3307
|
+
if initial_quality is not None:
|
|
3308
|
+
if not isinstance(initial_quality, dict):
|
|
3309
|
+
raise TypeError("'initial_quality' must be an instance of 'dict[str, float]' " +
|
|
3310
|
+
f"but not of '{type(initial_quality)}'")
|
|
3311
|
+
if any(not isinstance(key, str) or not isinstance(value, float)
|
|
3312
|
+
for key, value in initial_quality):
|
|
3313
|
+
raise TypeError("'initial_quality' must be an instance of 'dict[str, float]'")
|
|
3314
|
+
for node_id, node_init_qual in initial_quality:
|
|
3315
|
+
if node_id not in self._sensor_config.nodes:
|
|
3316
|
+
raise ValueError(f"Invalid node ID '{node_id}'")
|
|
3317
|
+
if node_init_qual < 0:
|
|
3318
|
+
raise ValueError(f"{node_id}: Initial node quality can not be negative")
|
|
3319
|
+
|
|
3320
|
+
init_qual = self.epanet_api.getNodeInitialQuality()
|
|
3321
|
+
for node_id, node_init_qual in initial_quality:
|
|
3322
|
+
node_idx = self.epanet_api.getNodeIndex(node_id) - 1
|
|
3323
|
+
init_qual[node_idx] = node_init_qual
|
|
3324
|
+
|
|
3325
|
+
self.epanet_api.setNodeInitialQuality(init_qual)
|
|
3326
|
+
|
|
3327
|
+
if order_wall is not None:
|
|
3328
|
+
if not isinstance(order_wall, int):
|
|
3329
|
+
raise TypeError("'order_wall' must be an instance of 'int' " +
|
|
3330
|
+
f"but not of '{type(order_wall)}'")
|
|
3331
|
+
if order_wall not in [0, 1]:
|
|
3332
|
+
raise ValueError(f"Invalid value '{order_wall}' for order_wall")
|
|
3333
|
+
|
|
3334
|
+
self.epanet_api.setOptionsPipeWallReactionOrder(order_wall)
|
|
3335
|
+
|
|
3336
|
+
if order_bulk is not None:
|
|
3337
|
+
if not isinstance(order_bulk, int):
|
|
3338
|
+
raise TypeError("'order_bulk' must be an instance of 'int' " +
|
|
3339
|
+
f"but not of '{type(order_bulk)}'")
|
|
3340
|
+
if order_bulk not in [0, 1]:
|
|
3341
|
+
raise ValueError(f"Invalid value '{order_bulk}' for order_bulk")
|
|
3342
|
+
|
|
3343
|
+
self.epanet_api.setOptionsPipeBulkReactionOrder(order_bulk)
|
|
3344
|
+
|
|
3345
|
+
if order_tank is not None:
|
|
3346
|
+
if not isinstance(order_tank, int):
|
|
3347
|
+
raise TypeError("'order_tank' must be an instance of 'int' " +
|
|
3348
|
+
f"but not of '{type(order_tank)}'")
|
|
3349
|
+
if order_tank not in [0, 1]:
|
|
3350
|
+
raise ValueError(f"Invalid value '{order_tank}' for order_wall")
|
|
3351
|
+
|
|
3352
|
+
self.epanet_api.setOptionsTankBulkReactionOrder(order_tank)
|
|
3353
|
+
|
|
3354
|
+
if global_wall_reaction_coefficient is not None:
|
|
3355
|
+
if not isinstance(global_wall_reaction_coefficient, float):
|
|
3356
|
+
raise TypeError("'global_wall_reaction_coefficient' must be an instance of " +
|
|
3357
|
+
f"'float' but not of '{type(global_wall_reaction_coefficient)}'")
|
|
3358
|
+
|
|
3359
|
+
wall_reaction_coeff = self.epanet_api.getLinkWallReactionCoeff()
|
|
3360
|
+
for i in range(len(wall_reaction_coeff)):
|
|
3361
|
+
wall_reaction_coeff[i] = global_wall_reaction_coefficient
|
|
3362
|
+
|
|
3363
|
+
self.epanet_api.setLinkWallReactionCoeff(wall_reaction_coeff)
|
|
3364
|
+
|
|
3365
|
+
if global_bulk_reaction_coefficient is not None:
|
|
3366
|
+
if not isinstance(global_bulk_reaction_coefficient, float):
|
|
3367
|
+
raise TypeError("'global_bulk_reaction_coefficient' must be an instance of " +
|
|
3368
|
+
f"'float' but not of '{type(global_bulk_reaction_coefficient)}'")
|
|
3369
|
+
|
|
3370
|
+
bulk_reaction_coeff = self.epanet_api.getLinkBulkReactionCoeff()
|
|
3371
|
+
for i in range(len(bulk_reaction_coeff)):
|
|
3372
|
+
bulk_reaction_coeff[i] = global_bulk_reaction_coefficient
|
|
3373
|
+
|
|
3374
|
+
self.epanet_api.setLinkBulkReactionCoeff(bulk_reaction_coeff)
|
|
3375
|
+
|
|
3376
|
+
if local_wall_reaction_coefficient is not None:
|
|
3377
|
+
if not isinstance(local_wall_reaction_coefficient, dict):
|
|
3378
|
+
raise TypeError("'local_wall_reaction_coefficient' must be an instance " +
|
|
3379
|
+
"of 'dict[str, float]' but not of " +
|
|
3380
|
+
f"'{type(local_wall_reaction_coefficient)}'")
|
|
3381
|
+
if any(not isinstance(key, str) or not isinstance(value, float)
|
|
3382
|
+
for key, value in local_wall_reaction_coefficient):
|
|
3383
|
+
raise TypeError("'local_wall_reaction_coefficient' must be an instance " +
|
|
3384
|
+
"of 'dict[str, float]'")
|
|
3385
|
+
for link_id, _ in local_wall_reaction_coefficient:
|
|
3386
|
+
if link_id not in self._sensor_config.links:
|
|
3387
|
+
raise ValueError(f"Invalid link ID '{link_id}'")
|
|
3388
|
+
|
|
3389
|
+
for link_id, link_reaction_coeff in local_wall_reaction_coefficient:
|
|
3390
|
+
link_idx = self.epanet_api.getLinkIndex(link_id)
|
|
3391
|
+
self.epanet_api.setLinkWallReactionCoeff(link_idx, link_reaction_coeff)
|
|
3392
|
+
|
|
3393
|
+
if local_bulk_reaction_coefficient is not None:
|
|
3394
|
+
if not isinstance(local_bulk_reaction_coefficient, dict):
|
|
3395
|
+
raise TypeError("'local_bulk_reaction_coefficient' must be an instance " +
|
|
3396
|
+
"of 'dict[str, float]' but not of " +
|
|
3397
|
+
f"'{type(local_bulk_reaction_coefficient)}'")
|
|
3398
|
+
if any(not isinstance(key, str) or not isinstance(value, float)
|
|
3399
|
+
for key, value in local_bulk_reaction_coefficient):
|
|
3400
|
+
raise TypeError("'local_bulk_reaction_coefficient' must be an instance " +
|
|
3401
|
+
"of 'dict[str, float]'")
|
|
3402
|
+
for link_id, _ in local_bulk_reaction_coefficient:
|
|
3403
|
+
if link_id not in self._sensor_config.links:
|
|
3404
|
+
raise ValueError(f"Invalid link ID '{link_id}'")
|
|
3405
|
+
|
|
3406
|
+
for link_id, link_reaction_coeff in local_bulk_reaction_coefficient:
|
|
3407
|
+
link_idx = self.epanet_api.getLinkIndex(link_id)
|
|
3408
|
+
self.epanet_api.setLinkBulkReactionCoeff(link_idx, link_reaction_coeff)
|
|
3409
|
+
|
|
3410
|
+
if local_tank_reaction_coefficient is not None:
|
|
3411
|
+
if not isinstance(local_tank_reaction_coefficient, dict):
|
|
3412
|
+
raise TypeError("'local_tank_reaction_coefficient' must be an instance " +
|
|
3413
|
+
"of 'dict[str, float]' but not of " +
|
|
3414
|
+
f"'{type(local_tank_reaction_coefficient)}'")
|
|
3415
|
+
if any(not isinstance(key, str) or not isinstance(value, float)
|
|
3416
|
+
for key, value in local_tank_reaction_coefficient):
|
|
3417
|
+
raise TypeError("'local_tank_reaction_coefficient' must be an instance " +
|
|
3418
|
+
"of 'dict[str, float]'")
|
|
3419
|
+
for tank_id, _ in local_tank_reaction_coefficient:
|
|
3420
|
+
if tank_id not in self._sensor_config.tanks:
|
|
3421
|
+
raise ValueError(f"Invalid tank ID '{tank_id}'")
|
|
3422
|
+
|
|
3423
|
+
for tank_id, tank_reaction_coeff in local_tank_reaction_coefficient:
|
|
3424
|
+
tank_idx = self.epanet_api.getNodeTankIndex(tank_id)
|
|
3425
|
+
self.epanet_api.setNodeTankBulkReactionCoeff(tank_idx, tank_reaction_coeff)
|
|
3426
|
+
|
|
3427
|
+
if limiting_potential is not None:
|
|
3428
|
+
if not isinstance(limiting_potential, float):
|
|
3429
|
+
raise TypeError("'limiting_potential' must be an instance of 'float' " +
|
|
3430
|
+
f"but not of '{type(limiting_potential)}'")
|
|
3431
|
+
if limiting_potential < 0:
|
|
3432
|
+
raise ValueError("'limiting_potential' can not be negative")
|
|
3433
|
+
|
|
3434
|
+
self.epanet_api.setOptionsLimitingConcentration(limiting_potential)
|
|
3435
|
+
|
|
3073
3436
|
def enable_sourcetracing_analysis(self, trace_node_id: str) -> None:
|
|
3074
3437
|
"""
|
|
3075
3438
|
Set source tracing analysis -- i.e. tracks the percentage of flow from a given node
|
|
@@ -3098,6 +3461,8 @@ class ScenarioSimulator():
|
|
|
3098
3461
|
"""
|
|
3099
3462
|
Adds a new external (bulk or surface) species injection source at a particular node.
|
|
3100
3463
|
|
|
3464
|
+
Only for EPANET-MSX scenarios.
|
|
3465
|
+
|
|
3101
3466
|
Parameters
|
|
3102
3467
|
----------
|
|
3103
3468
|
species_id : `str`
|
|
@@ -3153,3 +3518,81 @@ class ScenarioSimulator():
|
|
|
3153
3518
|
self.epanet_api.addMSXPattern(pattern_id, pattern)
|
|
3154
3519
|
self.epanet_api.setMSXSources(node_id, species_id, source_type_, source_strength,
|
|
3155
3520
|
pattern_id)
|
|
3521
|
+
|
|
3522
|
+
def set_bulk_species_node_initial_concentrations(self,
|
|
3523
|
+
inital_conc: dict[str, list[tuple[str, float]]]
|
|
3524
|
+
) -> None:
|
|
3525
|
+
"""
|
|
3526
|
+
Species the initial bulk species concentration at nodes.
|
|
3527
|
+
|
|
3528
|
+
Only for EPANET-MSX scenarios.
|
|
3529
|
+
|
|
3530
|
+
Parameters
|
|
3531
|
+
----------
|
|
3532
|
+
inital_conc : `dict[str, list[tuple[str, float]]]`
|
|
3533
|
+
Initial concentration of species (key) at nodes -- i.e.
|
|
3534
|
+
value: list of node ID and initial concentration.
|
|
3535
|
+
"""
|
|
3536
|
+
if not isinstance(inital_conc, dict) or \
|
|
3537
|
+
any(not isinstance(species_id, str) or not isinstance(node_initial_conc, list)
|
|
3538
|
+
for species_id, node_initial_conc in inital_conc.items()) or \
|
|
3539
|
+
any(not isinstance(node_initial_conc, tuple)
|
|
3540
|
+
for node_initial_conc in inital_conc.values()) or \
|
|
3541
|
+
any(not isinstance(node_id, str) or not isinstance(conc, float)
|
|
3542
|
+
for node_id, conc in inital_conc.values()):
|
|
3543
|
+
raise TypeError("'inital_conc' must be an instance of " +
|
|
3544
|
+
"'dict[str, list[tuple[str, float]]'")
|
|
3545
|
+
if any(species_id not in self.sensor_config.bulk_species
|
|
3546
|
+
for species_id in inital_conc.keys()):
|
|
3547
|
+
raise ValueError("Unknown bulk species in 'inital_conc'")
|
|
3548
|
+
if any(node_id not in self.sensor_config.nodes for node_id, _ in inital_conc.values()):
|
|
3549
|
+
raise ValueError("Unknown node ID in 'inital_conc'")
|
|
3550
|
+
if any(conc < 0 for _, conc in inital_conc.values()):
|
|
3551
|
+
raise ValueError("Initial node concentration can not be negative")
|
|
3552
|
+
|
|
3553
|
+
for species_id, node_initial_conc in inital_conc.items():
|
|
3554
|
+
species_idx, = self.epanet_api.getMSXSpeciesIndex([species_id])
|
|
3555
|
+
|
|
3556
|
+
for node_id, initial_conc in node_initial_conc:
|
|
3557
|
+
node_idx = self.epanet_api.getNodeIndex(node_id)
|
|
3558
|
+
self.epanet_api.msx.MSXsetinitqual(ToolkitConstants.MSX_NODE, node_idx, species_idx,
|
|
3559
|
+
initial_conc)
|
|
3560
|
+
|
|
3561
|
+
def set_species_link_initial_concentrations(self,
|
|
3562
|
+
inital_conc: dict[str, list[tuple[str, float]]]
|
|
3563
|
+
) -> None:
|
|
3564
|
+
"""
|
|
3565
|
+
Species the initial (bulk or surface) species concentration at links.
|
|
3566
|
+
|
|
3567
|
+
Only for EPANET-MSX scenarios.
|
|
3568
|
+
|
|
3569
|
+
Parameters
|
|
3570
|
+
----------
|
|
3571
|
+
inital_conc : `dict[str, list[tuple[str, float]]]`
|
|
3572
|
+
Initial concentration of species (key) at links -- i.e.
|
|
3573
|
+
value: list of link ID and initial concentration.
|
|
3574
|
+
"""
|
|
3575
|
+
if not isinstance(inital_conc, dict) or \
|
|
3576
|
+
any(not isinstance(species_id, str) or not isinstance(link_initial_conc, list)
|
|
3577
|
+
for species_id, link_initial_conc in inital_conc.items()) or \
|
|
3578
|
+
any(not isinstance(link_initial_conc, tuple)
|
|
3579
|
+
for link_initial_conc in inital_conc.values()) or \
|
|
3580
|
+
any(not isinstance(link_id, str) or not isinstance(conc, float)
|
|
3581
|
+
for link_id, conc in inital_conc.values()):
|
|
3582
|
+
raise TypeError("'inital_conc' must be an instance of " +
|
|
3583
|
+
"'dict[str, list[tuple[str, float]]'")
|
|
3584
|
+
if any(species_id not in self.sensor_config.bulk_species
|
|
3585
|
+
for species_id in inital_conc.keys()):
|
|
3586
|
+
raise ValueError("Unknown bulk species in 'inital_conc'")
|
|
3587
|
+
if any(link_id not in self.sensor_config.links for link_id, _ in inital_conc.values()):
|
|
3588
|
+
raise ValueError("Unknown link ID in 'inital_conc'")
|
|
3589
|
+
if any(conc < 0 for _, conc in inital_conc.values()):
|
|
3590
|
+
raise ValueError("Initial link concentration can not be negative")
|
|
3591
|
+
|
|
3592
|
+
for species_id, link_initial_conc in inital_conc.items():
|
|
3593
|
+
species_idx, = self.epanet_api.getMSXSpeciesIndex([species_id])
|
|
3594
|
+
|
|
3595
|
+
for link_id, initial_conc in link_initial_conc:
|
|
3596
|
+
link_idx = self.epanet_api.getLinkIndex(link_id)
|
|
3597
|
+
self.epanet_api.msx.MSXsetinitqual(ToolkitConstants.MSX_LINK, link_idx, species_idx,
|
|
3598
|
+
initial_conc)
|