epyt-flow 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- epyt_flow/EPANET/EPANET/SRC_engines/AUTHORS +28 -0
- epyt_flow/EPANET/EPANET/SRC_engines/LICENSE +21 -0
- epyt_flow/EPANET/EPANET/SRC_engines/Readme_SRC_Engines.txt +18 -0
- epyt_flow/EPANET/EPANET/SRC_engines/enumstxt.h +134 -0
- epyt_flow/EPANET/EPANET/SRC_engines/epanet.c +5578 -0
- epyt_flow/EPANET/EPANET/SRC_engines/epanet2.c +865 -0
- epyt_flow/EPANET/EPANET/SRC_engines/epanet2.def +131 -0
- epyt_flow/EPANET/EPANET/SRC_engines/errors.dat +73 -0
- epyt_flow/EPANET/EPANET/SRC_engines/funcs.h +193 -0
- epyt_flow/EPANET/EPANET/SRC_engines/genmmd.c +1000 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hash.c +177 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hash.h +28 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydcoeffs.c +1151 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydraul.c +1117 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydsolver.c +720 -0
- epyt_flow/EPANET/EPANET/SRC_engines/hydstatus.c +476 -0
- epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2.h +431 -0
- epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_2.h +1786 -0
- epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_enums.h +468 -0
- epyt_flow/EPANET/EPANET/SRC_engines/inpfile.c +810 -0
- epyt_flow/EPANET/EPANET/SRC_engines/input1.c +707 -0
- epyt_flow/EPANET/EPANET/SRC_engines/input2.c +864 -0
- epyt_flow/EPANET/EPANET/SRC_engines/input3.c +2170 -0
- epyt_flow/EPANET/EPANET/SRC_engines/main.c +93 -0
- epyt_flow/EPANET/EPANET/SRC_engines/mempool.c +142 -0
- epyt_flow/EPANET/EPANET/SRC_engines/mempool.h +24 -0
- epyt_flow/EPANET/EPANET/SRC_engines/output.c +852 -0
- epyt_flow/EPANET/EPANET/SRC_engines/project.c +1359 -0
- epyt_flow/EPANET/EPANET/SRC_engines/quality.c +685 -0
- epyt_flow/EPANET/EPANET/SRC_engines/qualreact.c +743 -0
- epyt_flow/EPANET/EPANET/SRC_engines/qualroute.c +694 -0
- epyt_flow/EPANET/EPANET/SRC_engines/report.c +1489 -0
- epyt_flow/EPANET/EPANET/SRC_engines/rules.c +1362 -0
- epyt_flow/EPANET/EPANET/SRC_engines/smatrix.c +871 -0
- epyt_flow/EPANET/EPANET/SRC_engines/text.h +497 -0
- epyt_flow/EPANET/EPANET/SRC_engines/types.h +874 -0
- epyt_flow/EPANET/EPANET-MSX/MSX_Updates.txt +53 -0
- epyt_flow/EPANET/EPANET-MSX/Src/dispersion.h +27 -0
- epyt_flow/EPANET/EPANET-MSX/Src/hash.c +107 -0
- epyt_flow/EPANET/EPANET-MSX/Src/hash.h +28 -0
- epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx.h +102 -0
- epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx_export.h +42 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.c +937 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.h +39 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mempool.c +204 -0
- epyt_flow/EPANET/EPANET-MSX/Src/mempool.h +24 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxchem.c +1285 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxcompiler.c +368 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxdict.h +42 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxdispersion.c +586 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxerr.c +116 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxfile.c +260 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.c +175 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.h +35 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxinp.c +1504 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxout.c +401 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxproj.c +791 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxqual.c +2010 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxrpt.c +400 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxtank.c +422 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxtoolkit.c +1164 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxtypes.h +551 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxutils.c +524 -0
- epyt_flow/EPANET/EPANET-MSX/Src/msxutils.h +56 -0
- epyt_flow/EPANET/EPANET-MSX/Src/newton.c +158 -0
- epyt_flow/EPANET/EPANET-MSX/Src/newton.h +34 -0
- epyt_flow/EPANET/EPANET-MSX/Src/rk5.c +287 -0
- epyt_flow/EPANET/EPANET-MSX/Src/rk5.h +39 -0
- epyt_flow/EPANET/EPANET-MSX/Src/ros2.c +293 -0
- epyt_flow/EPANET/EPANET-MSX/Src/ros2.h +35 -0
- epyt_flow/EPANET/EPANET-MSX/Src/smatrix.c +816 -0
- epyt_flow/EPANET/EPANET-MSX/Src/smatrix.h +29 -0
- epyt_flow/EPANET/EPANET-MSX/readme.txt +14 -0
- epyt_flow/EPANET/compile.sh +4 -0
- epyt_flow/VERSION +1 -0
- epyt_flow/__init__.py +24 -0
- epyt_flow/data/__init__.py +0 -0
- epyt_flow/data/benchmarks/__init__.py +11 -0
- epyt_flow/data/benchmarks/batadal.py +257 -0
- epyt_flow/data/benchmarks/batadal_data.py +28 -0
- epyt_flow/data/benchmarks/battledim.py +473 -0
- epyt_flow/data/benchmarks/battledim_data.py +51 -0
- epyt_flow/data/benchmarks/gecco_water_quality.py +267 -0
- epyt_flow/data/benchmarks/leakdb.py +592 -0
- epyt_flow/data/benchmarks/leakdb_data.py +18923 -0
- epyt_flow/data/benchmarks/water_usage.py +123 -0
- epyt_flow/data/networks.py +650 -0
- epyt_flow/gym/__init__.py +4 -0
- epyt_flow/gym/control_gyms.py +47 -0
- epyt_flow/gym/scenario_control_env.py +101 -0
- epyt_flow/metrics.py +404 -0
- epyt_flow/models/__init__.py +2 -0
- epyt_flow/models/event_detector.py +31 -0
- epyt_flow/models/sensor_interpolation_detector.py +118 -0
- epyt_flow/rest_api/__init__.py +4 -0
- epyt_flow/rest_api/base_handler.py +70 -0
- epyt_flow/rest_api/res_manager.py +95 -0
- epyt_flow/rest_api/scada_data_handler.py +476 -0
- epyt_flow/rest_api/scenario_handler.py +352 -0
- epyt_flow/rest_api/server.py +106 -0
- epyt_flow/serialization.py +438 -0
- epyt_flow/simulation/__init__.py +5 -0
- epyt_flow/simulation/events/__init__.py +6 -0
- epyt_flow/simulation/events/actuator_events.py +259 -0
- epyt_flow/simulation/events/event.py +81 -0
- epyt_flow/simulation/events/leakages.py +404 -0
- epyt_flow/simulation/events/sensor_faults.py +267 -0
- epyt_flow/simulation/events/sensor_reading_attack.py +185 -0
- epyt_flow/simulation/events/sensor_reading_event.py +170 -0
- epyt_flow/simulation/events/system_event.py +88 -0
- epyt_flow/simulation/parallel_simulation.py +147 -0
- epyt_flow/simulation/scada/__init__.py +3 -0
- epyt_flow/simulation/scada/advanced_control.py +134 -0
- epyt_flow/simulation/scada/scada_data.py +1589 -0
- epyt_flow/simulation/scada/scada_data_export.py +255 -0
- epyt_flow/simulation/scenario_config.py +608 -0
- epyt_flow/simulation/scenario_simulator.py +1897 -0
- epyt_flow/simulation/scenario_visualizer.py +61 -0
- epyt_flow/simulation/sensor_config.py +1289 -0
- epyt_flow/topology.py +290 -0
- epyt_flow/uncertainty/__init__.py +3 -0
- epyt_flow/uncertainty/model_uncertainty.py +302 -0
- epyt_flow/uncertainty/sensor_noise.py +73 -0
- epyt_flow/uncertainty/uncertainties.py +555 -0
- epyt_flow/uncertainty/utils.py +206 -0
- epyt_flow/utils.py +306 -0
- epyt_flow-0.1.0.dist-info/LICENSE +21 -0
- epyt_flow-0.1.0.dist-info/METADATA +139 -0
- epyt_flow-0.1.0.dist-info/RECORD +131 -0
- epyt_flow-0.1.0.dist-info/WHEEL +5 -0
- epyt_flow-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module provides base classes for system events such as leakages, actuator events, etc.
|
|
3
|
+
"""
|
|
4
|
+
from abc import abstractmethod
|
|
5
|
+
import epyt
|
|
6
|
+
|
|
7
|
+
from .event import Event
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SystemEvent(Event):
|
|
11
|
+
"""
|
|
12
|
+
Base class for a system event -- i.e. an event that affects the EPANET simulation.
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self, **kwds):
|
|
15
|
+
self._epanet_api = None
|
|
16
|
+
self.__exit_called = False
|
|
17
|
+
|
|
18
|
+
super().__init__(**kwds)
|
|
19
|
+
|
|
20
|
+
def init(self, epanet_api: epyt.epanet) -> None:
|
|
21
|
+
"""
|
|
22
|
+
Initializes the event.
|
|
23
|
+
|
|
24
|
+
Parameters
|
|
25
|
+
----------
|
|
26
|
+
epanet_api : `epyt.epanet`
|
|
27
|
+
API to EPANET and EPANET-MSX.
|
|
28
|
+
"""
|
|
29
|
+
self._epanet_api = epanet_api
|
|
30
|
+
|
|
31
|
+
def __getstate__(self):
|
|
32
|
+
state = self.__dict__.copy()
|
|
33
|
+
if "_epanet_api" in state:
|
|
34
|
+
# Do not serialize EPyT instance!
|
|
35
|
+
del state["_epanet_api"]
|
|
36
|
+
|
|
37
|
+
return state
|
|
38
|
+
|
|
39
|
+
def __call__(self, cur_time) -> None:
|
|
40
|
+
return self.step(cur_time)
|
|
41
|
+
|
|
42
|
+
def step(self, cur_time) -> None:
|
|
43
|
+
"""
|
|
44
|
+
Is called at every iteration (time step) in the simulation.
|
|
45
|
+
`apply` or `exit` are called if necessary.
|
|
46
|
+
|
|
47
|
+
Parameters
|
|
48
|
+
----------
|
|
49
|
+
cur_time : `int`
|
|
50
|
+
Current time (seconds since the start) in the simulation.
|
|
51
|
+
"""
|
|
52
|
+
if self.start_time <= cur_time < self.end_time:
|
|
53
|
+
self.apply(cur_time)
|
|
54
|
+
elif cur_time > self.end_time:
|
|
55
|
+
if self.__exit_called is False:
|
|
56
|
+
self.exit(cur_time)
|
|
57
|
+
self.__exit_called = True
|
|
58
|
+
|
|
59
|
+
def reset(self) -> None:
|
|
60
|
+
"""
|
|
61
|
+
Resets this event -- i.e. make it ready for another simulation run.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def exit(self, cur_time) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Is called ONCE after the event is over -- i.e. next time step after `end_time`.
|
|
67
|
+
|
|
68
|
+
Any "clean-up" or "exiting" logic should go here.
|
|
69
|
+
|
|
70
|
+
Parameters
|
|
71
|
+
----------
|
|
72
|
+
cur_time : `int`
|
|
73
|
+
Current time (seconds since the start) in the simulation.
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def apply(self, cur_time: int) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Implements the event using EPANET and EPANET-MSX.
|
|
80
|
+
|
|
81
|
+
This function is only called when the event is active.
|
|
82
|
+
|
|
83
|
+
Parameters
|
|
84
|
+
----------
|
|
85
|
+
cur_time : `int`
|
|
86
|
+
Current time (seconds since the start) in the simulation.
|
|
87
|
+
"""
|
|
88
|
+
raise NotImplementedError()
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module provides functions for simulating several scenarios in parallel.
|
|
3
|
+
"""
|
|
4
|
+
from typing import Callable
|
|
5
|
+
import os
|
|
6
|
+
import warnings
|
|
7
|
+
from multiprocess import Pool, cpu_count
|
|
8
|
+
import shutil
|
|
9
|
+
import psutil
|
|
10
|
+
|
|
11
|
+
from .scenario_config import ScenarioConfig
|
|
12
|
+
from .scada import ScadaData
|
|
13
|
+
from .scenario_simulator import ScenarioSimulator
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def callback_save_to_file(folder_out: str = "") -> Callable[[ScadaData, ScenarioConfig, int], None]:
|
|
17
|
+
"""
|
|
18
|
+
Creates a callback for storing the simulation results in a .epytflow_scada_data file.
|
|
19
|
+
The returned callback can be directly passed to
|
|
20
|
+
:func:`~epyt_flow.simulation.parallel_simulation.ParallelScenarioSimulation.run`.
|
|
21
|
+
|
|
22
|
+
Parameters
|
|
23
|
+
----------
|
|
24
|
+
folder_out : `str`, optional
|
|
25
|
+
Path to the folder where the simulation results will be stored.
|
|
26
|
+
|
|
27
|
+
The default is the current working directory.
|
|
28
|
+
|
|
29
|
+
Returns
|
|
30
|
+
-------
|
|
31
|
+
`Callable[[ScadaData, ScenarioConfig, int], None]`
|
|
32
|
+
Callback storing the simulation results.
|
|
33
|
+
"""
|
|
34
|
+
def callback(scada_data: ScadaData, _, scenario_idx: int) -> None:
|
|
35
|
+
scada_data.save_to_file(os.path.join(folder_out, f"{scenario_idx}"))
|
|
36
|
+
|
|
37
|
+
return callback
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _run_scenario_simulation(scenario_config: ScenarioConfig, scenario_idx: int,
|
|
41
|
+
callback: Callable[[ScadaData, ScenarioConfig, int], None]) -> None:
|
|
42
|
+
with ScenarioSimulator(scenario_config=scenario_config) as sim:
|
|
43
|
+
callback(sim.run_simulation(), scenario_config, scenario_idx)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ParallelScenarioSimulation():
|
|
47
|
+
"""
|
|
48
|
+
Class providing functions to run scenario simulations in parallel.
|
|
49
|
+
"""
|
|
50
|
+
@staticmethod
|
|
51
|
+
def run(scenarios: list[ScenarioConfig], n_jobs: int = -1,
|
|
52
|
+
max_working_memory_consumption: int = None,
|
|
53
|
+
callback: Callable[[ScadaData, ScenarioConfig, int], None] = callback_save_to_file()
|
|
54
|
+
) -> None:
|
|
55
|
+
"""
|
|
56
|
+
Simulates multiple scenarios in parallel.
|
|
57
|
+
|
|
58
|
+
Parameters
|
|
59
|
+
----------
|
|
60
|
+
scenarios : list[:class:`~epyt_flow.simulation.scenario_config.ScenarioConfig`]
|
|
61
|
+
List of scenarios to be simulated.
|
|
62
|
+
n_jobs : `int`, optional
|
|
63
|
+
Number of CPUs that can be used by the simulations -- usually, this translates to
|
|
64
|
+
the number of scenarios that are simulated in parallel.
|
|
65
|
+
|
|
66
|
+
If -1, all CPUs are used.
|
|
67
|
+
|
|
68
|
+
The default is -1
|
|
69
|
+
max_working_memory_consumption : `int`, optional
|
|
70
|
+
Maximum amount of working memory in MB that can be used by the simulations.
|
|
71
|
+
Note that this might limit the number of scenarios that can be simulated in parallel.
|
|
72
|
+
|
|
73
|
+
The default is None.
|
|
74
|
+
callback: `Callable[[ScadaData, ScenarioConfig, int], None]`, optional
|
|
75
|
+
Callback that is called after the simulation of a scenario finished.
|
|
76
|
+
|
|
77
|
+
The callback gets the simulation results as a
|
|
78
|
+
:class:`~epyt_flow.simulation.scada.scada_data.ScadaData` instance, the scenario
|
|
79
|
+
configuration as a :class: `epyt_flow.simulation.scenario_config.ScenarioConfig`
|
|
80
|
+
instance, and the index of the scenario in 'scenarios' as arguments.
|
|
81
|
+
|
|
82
|
+
The default is :func:`~epyt_flow.simulation.parallel_simulation.callback_save_to_file`.
|
|
83
|
+
"""
|
|
84
|
+
if not isinstance(scenarios, list):
|
|
85
|
+
raise TypeError("'scenarios' must be an instance of 'list[ScenarioConfig]' " +
|
|
86
|
+
f"but not of '{type(scenarios)}'")
|
|
87
|
+
if any(not isinstance(item, ScenarioConfig) for item in scenarios):
|
|
88
|
+
raise TypeError("Each item in 'scenarios' must be an instance of 'ScenarioConfig'")
|
|
89
|
+
|
|
90
|
+
if not isinstance(n_jobs, int):
|
|
91
|
+
raise TypeError(f"'n_jobs' must be an instance of 'int' but not of '{type(n_jobs)}'")
|
|
92
|
+
if not (n_jobs == -1 or n_jobs > 0):
|
|
93
|
+
raise ValueError("'n_jobs' must be either -1 or a positive integer")
|
|
94
|
+
|
|
95
|
+
if max_working_memory_consumption is not None:
|
|
96
|
+
if not isinstance(max_working_memory_consumption, int) or \
|
|
97
|
+
max_working_memory_consumption <= 0:
|
|
98
|
+
raise ValueError("'max_working_memory_consumption' must be a positive integer")
|
|
99
|
+
|
|
100
|
+
if not callable(callback):
|
|
101
|
+
raise TypeError("'callback' mut be a callable " +
|
|
102
|
+
"'Callable[[ScadaData, ScenarioConfig, int], None]'")
|
|
103
|
+
|
|
104
|
+
# Get free memory in MB
|
|
105
|
+
ram_free_memory = psutil.virtual_memory().free * .000001
|
|
106
|
+
if max_working_memory_consumption is not None:
|
|
107
|
+
ram_free_memory = max(ram_free_memory, max_working_memory_consumption)
|
|
108
|
+
|
|
109
|
+
harddisk_free_memory = shutil.disk_usage(".").free * .000001
|
|
110
|
+
|
|
111
|
+
# Check memory requirements of each scenario
|
|
112
|
+
max_memory_required = max(s_config.memory_consumption_estimate
|
|
113
|
+
if s_config.memory_consumption_estimate is not None else 0
|
|
114
|
+
for s_config in scenarios)
|
|
115
|
+
if max_memory_required > ram_free_memory:
|
|
116
|
+
raise RuntimeError("Not enough working memory avaialble! " +
|
|
117
|
+
f"Requested {max_memory_required} MB but only " +
|
|
118
|
+
f"{ram_free_memory} MB are available")
|
|
119
|
+
|
|
120
|
+
if sum(s_config.memory_consumption_estimate
|
|
121
|
+
if s_config.memory_consumption_estimate is not None else 0
|
|
122
|
+
for s_config in scenarios) >= harddisk_free_memory:
|
|
123
|
+
warnings.warn("There might not be enough free space on the hard disk " +
|
|
124
|
+
"to store all scenario results")
|
|
125
|
+
|
|
126
|
+
# Compute number of processes that can run in parallel
|
|
127
|
+
n_available_cpus = cpu_count()
|
|
128
|
+
if n_jobs != -1:
|
|
129
|
+
n_available_cpus = min(n_available_cpus, n_jobs)
|
|
130
|
+
|
|
131
|
+
required_memory_bound = min(ram_free_memory, harddisk_free_memory)
|
|
132
|
+
n_max_parallel_scenarios = n_available_cpus
|
|
133
|
+
if max_memory_required != 0:
|
|
134
|
+
n_max_parallel_scenarios = int(required_memory_bound / max_memory_required)
|
|
135
|
+
|
|
136
|
+
n_parallel_scenarios = min(n_available_cpus, n_max_parallel_scenarios)
|
|
137
|
+
|
|
138
|
+
if any(s_config.f_msx_in is not None for s_config in scenarios):
|
|
139
|
+
n_parallel_scenarios = 1
|
|
140
|
+
|
|
141
|
+
# Run scenario simulations
|
|
142
|
+
scenarios_task = []
|
|
143
|
+
for scenario_idx, scenario in enumerate(scenarios):
|
|
144
|
+
scenarios_task.append((scenario, scenario_idx, callback))
|
|
145
|
+
|
|
146
|
+
with Pool(processes=n_parallel_scenarios, maxtasksperchild=1) as pool:
|
|
147
|
+
pool.starmap(_run_scenario_simulation, scenarios_task)
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module provides a base class for control modules.
|
|
3
|
+
"""
|
|
4
|
+
from abc import abstractmethod, ABC
|
|
5
|
+
import warnings
|
|
6
|
+
import numpy as np
|
|
7
|
+
import epyt
|
|
8
|
+
|
|
9
|
+
from . import ScadaData
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AdvancedControlModule(ABC):
|
|
13
|
+
"""
|
|
14
|
+
Base class for a control module.
|
|
15
|
+
|
|
16
|
+
Attributes
|
|
17
|
+
----------
|
|
18
|
+
epanet_api : `epyt.epanet`
|
|
19
|
+
API to EPANET and EPANET-MSX. Is set in :func:`init`.
|
|
20
|
+
"""
|
|
21
|
+
def __init__(self, **kwds):
|
|
22
|
+
self._epanet_api = None
|
|
23
|
+
|
|
24
|
+
super().__init__(**kwds)
|
|
25
|
+
|
|
26
|
+
def init(self, epanet_api: epyt.epanet) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Initializes the control module.
|
|
29
|
+
|
|
30
|
+
Parameters
|
|
31
|
+
----------
|
|
32
|
+
epanet_api : `epyt.epanet`
|
|
33
|
+
API to EPANET for implementing the control module.
|
|
34
|
+
"""
|
|
35
|
+
if not isinstance(epanet_api, epyt.epanet):
|
|
36
|
+
raise TypeError("'epanet_api' must be an instance of 'epyt.epanet' but not of " +
|
|
37
|
+
f"'{type(epanet_api)}'")
|
|
38
|
+
|
|
39
|
+
self._epanet_api = epanet_api
|
|
40
|
+
|
|
41
|
+
def set_pump_status(self, pump_id: str, status: int) -> None:
|
|
42
|
+
"""
|
|
43
|
+
Sets the status of a pump.
|
|
44
|
+
|
|
45
|
+
Parameters
|
|
46
|
+
----------
|
|
47
|
+
pump_id : `str`
|
|
48
|
+
ID of the pump for which the status is set.
|
|
49
|
+
status : `int`
|
|
50
|
+
New status of the pump -- either active (i.e. open) or inactive (i.e. closed).
|
|
51
|
+
|
|
52
|
+
Must be one of the following constants defined in
|
|
53
|
+
:class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
|
|
54
|
+
|
|
55
|
+
- EN_CLOSED = 0
|
|
56
|
+
- EN_OPEN = 1
|
|
57
|
+
"""
|
|
58
|
+
pump_idx = self._epanet_api.getLinkPumpNameID().index(pump_id)
|
|
59
|
+
pump_link_idx = self._epanet_api.getLinkPumpIndex(pump_idx + 1)
|
|
60
|
+
self._epanet_api.setLinkStatus(pump_link_idx, status)
|
|
61
|
+
|
|
62
|
+
def set_pump_speed(self, pump_id: str, speed: float) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Sets the speed of a pump.
|
|
65
|
+
|
|
66
|
+
Parameters
|
|
67
|
+
----------
|
|
68
|
+
pump_id : `str`
|
|
69
|
+
ID of the pump for which the pump speed is set.
|
|
70
|
+
speed : `float`
|
|
71
|
+
New pump speed.
|
|
72
|
+
"""
|
|
73
|
+
pump_idx = self._epanet_api.getLinkPumpNameID().index(pump_id)
|
|
74
|
+
pattern_idx = self._epanet_api.getLinkPumpPatternIndex(pump_idx + 1)
|
|
75
|
+
|
|
76
|
+
if pattern_idx == 0:
|
|
77
|
+
warnings.warn(f"No pattern for pump '{pump_id}' found -- a new pattern is created")
|
|
78
|
+
pattern_idx = self._epanet_api.addPattern(f"pump_speed_{pump_id}")
|
|
79
|
+
self._epanet_api.setLinkPumpPatternIndex(pattern_idx)
|
|
80
|
+
|
|
81
|
+
self._epanet_api.setPattern(pattern_idx, np.array([speed]))
|
|
82
|
+
|
|
83
|
+
def set_valve_status(self, valve_id: str, status: int) -> None:
|
|
84
|
+
"""
|
|
85
|
+
Sets the status of a valve.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
valve_id : `str`
|
|
90
|
+
ID of the valve for which the status is set.
|
|
91
|
+
status : `int`
|
|
92
|
+
New status of the valve -- either open or closed.
|
|
93
|
+
|
|
94
|
+
Must be one of the following constants defined in
|
|
95
|
+
:class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
|
|
96
|
+
|
|
97
|
+
- EN_CLOSED = 0
|
|
98
|
+
- EN_OPEN = 1
|
|
99
|
+
"""
|
|
100
|
+
valve_idx = self._epanet_api.getLinkValveNameID().index(valve_id)
|
|
101
|
+
valve_link_idx = self._epanet_api.getLinkValveIndex(valve_idx + 1)
|
|
102
|
+
self._epanet_api.setLinkStatus(valve_link_idx, status)
|
|
103
|
+
|
|
104
|
+
def set_node_quality_source_value(self, node_id: str, pattern_id: str,
|
|
105
|
+
qual_value: float) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Sets the quality source at a particular node to a specific value -- e.g.
|
|
108
|
+
setting the chlorine concentration injection to a specified value.
|
|
109
|
+
|
|
110
|
+
Parameters
|
|
111
|
+
----------
|
|
112
|
+
node_id : `str`
|
|
113
|
+
ID of the node.
|
|
114
|
+
pattern_id : `str`
|
|
115
|
+
ID of the quality pattern at the specific node.
|
|
116
|
+
qual_value : `float`
|
|
117
|
+
New quality source value.
|
|
118
|
+
"""
|
|
119
|
+
node_idx = self._epanet_api.getNodeIndex(node_id)
|
|
120
|
+
pattern_idx = self._epanet_api.getPatternIndex(pattern_id)
|
|
121
|
+
self._epanet_api.setNodeSourceQuality(node_idx, 1)
|
|
122
|
+
self._epanet_api.setPattern(pattern_idx, np.array([qual_value]))
|
|
123
|
+
|
|
124
|
+
@abstractmethod
|
|
125
|
+
def step(self, scada_data: ScadaData) -> None:
|
|
126
|
+
"""
|
|
127
|
+
Implements the control algorithm -- i.e. mapping of sensor reading to actions.
|
|
128
|
+
|
|
129
|
+
Parameters
|
|
130
|
+
----------
|
|
131
|
+
scada_data : :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
|
|
132
|
+
Sensor readings.
|
|
133
|
+
"""
|
|
134
|
+
raise NotImplementedError()
|