epyt-flow 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (131) hide show
  1. epyt_flow/EPANET/EPANET/SRC_engines/AUTHORS +28 -0
  2. epyt_flow/EPANET/EPANET/SRC_engines/LICENSE +21 -0
  3. epyt_flow/EPANET/EPANET/SRC_engines/Readme_SRC_Engines.txt +18 -0
  4. epyt_flow/EPANET/EPANET/SRC_engines/enumstxt.h +134 -0
  5. epyt_flow/EPANET/EPANET/SRC_engines/epanet.c +5578 -0
  6. epyt_flow/EPANET/EPANET/SRC_engines/epanet2.c +865 -0
  7. epyt_flow/EPANET/EPANET/SRC_engines/epanet2.def +131 -0
  8. epyt_flow/EPANET/EPANET/SRC_engines/errors.dat +73 -0
  9. epyt_flow/EPANET/EPANET/SRC_engines/funcs.h +193 -0
  10. epyt_flow/EPANET/EPANET/SRC_engines/genmmd.c +1000 -0
  11. epyt_flow/EPANET/EPANET/SRC_engines/hash.c +177 -0
  12. epyt_flow/EPANET/EPANET/SRC_engines/hash.h +28 -0
  13. epyt_flow/EPANET/EPANET/SRC_engines/hydcoeffs.c +1151 -0
  14. epyt_flow/EPANET/EPANET/SRC_engines/hydraul.c +1117 -0
  15. epyt_flow/EPANET/EPANET/SRC_engines/hydsolver.c +720 -0
  16. epyt_flow/EPANET/EPANET/SRC_engines/hydstatus.c +476 -0
  17. epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2.h +431 -0
  18. epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_2.h +1786 -0
  19. epyt_flow/EPANET/EPANET/SRC_engines/include/epanet2_enums.h +468 -0
  20. epyt_flow/EPANET/EPANET/SRC_engines/inpfile.c +810 -0
  21. epyt_flow/EPANET/EPANET/SRC_engines/input1.c +707 -0
  22. epyt_flow/EPANET/EPANET/SRC_engines/input2.c +864 -0
  23. epyt_flow/EPANET/EPANET/SRC_engines/input3.c +2170 -0
  24. epyt_flow/EPANET/EPANET/SRC_engines/main.c +93 -0
  25. epyt_flow/EPANET/EPANET/SRC_engines/mempool.c +142 -0
  26. epyt_flow/EPANET/EPANET/SRC_engines/mempool.h +24 -0
  27. epyt_flow/EPANET/EPANET/SRC_engines/output.c +852 -0
  28. epyt_flow/EPANET/EPANET/SRC_engines/project.c +1359 -0
  29. epyt_flow/EPANET/EPANET/SRC_engines/quality.c +685 -0
  30. epyt_flow/EPANET/EPANET/SRC_engines/qualreact.c +743 -0
  31. epyt_flow/EPANET/EPANET/SRC_engines/qualroute.c +694 -0
  32. epyt_flow/EPANET/EPANET/SRC_engines/report.c +1489 -0
  33. epyt_flow/EPANET/EPANET/SRC_engines/rules.c +1362 -0
  34. epyt_flow/EPANET/EPANET/SRC_engines/smatrix.c +871 -0
  35. epyt_flow/EPANET/EPANET/SRC_engines/text.h +497 -0
  36. epyt_flow/EPANET/EPANET/SRC_engines/types.h +874 -0
  37. epyt_flow/EPANET/EPANET-MSX/MSX_Updates.txt +53 -0
  38. epyt_flow/EPANET/EPANET-MSX/Src/dispersion.h +27 -0
  39. epyt_flow/EPANET/EPANET-MSX/Src/hash.c +107 -0
  40. epyt_flow/EPANET/EPANET-MSX/Src/hash.h +28 -0
  41. epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx.h +102 -0
  42. epyt_flow/EPANET/EPANET-MSX/Src/include/epanetmsx_export.h +42 -0
  43. epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.c +937 -0
  44. epyt_flow/EPANET/EPANET-MSX/Src/mathexpr.h +39 -0
  45. epyt_flow/EPANET/EPANET-MSX/Src/mempool.c +204 -0
  46. epyt_flow/EPANET/EPANET-MSX/Src/mempool.h +24 -0
  47. epyt_flow/EPANET/EPANET-MSX/Src/msxchem.c +1285 -0
  48. epyt_flow/EPANET/EPANET-MSX/Src/msxcompiler.c +368 -0
  49. epyt_flow/EPANET/EPANET-MSX/Src/msxdict.h +42 -0
  50. epyt_flow/EPANET/EPANET-MSX/Src/msxdispersion.c +586 -0
  51. epyt_flow/EPANET/EPANET-MSX/Src/msxerr.c +116 -0
  52. epyt_flow/EPANET/EPANET-MSX/Src/msxfile.c +260 -0
  53. epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.c +175 -0
  54. epyt_flow/EPANET/EPANET-MSX/Src/msxfuncs.h +35 -0
  55. epyt_flow/EPANET/EPANET-MSX/Src/msxinp.c +1504 -0
  56. epyt_flow/EPANET/EPANET-MSX/Src/msxout.c +401 -0
  57. epyt_flow/EPANET/EPANET-MSX/Src/msxproj.c +791 -0
  58. epyt_flow/EPANET/EPANET-MSX/Src/msxqual.c +2010 -0
  59. epyt_flow/EPANET/EPANET-MSX/Src/msxrpt.c +400 -0
  60. epyt_flow/EPANET/EPANET-MSX/Src/msxtank.c +422 -0
  61. epyt_flow/EPANET/EPANET-MSX/Src/msxtoolkit.c +1164 -0
  62. epyt_flow/EPANET/EPANET-MSX/Src/msxtypes.h +551 -0
  63. epyt_flow/EPANET/EPANET-MSX/Src/msxutils.c +524 -0
  64. epyt_flow/EPANET/EPANET-MSX/Src/msxutils.h +56 -0
  65. epyt_flow/EPANET/EPANET-MSX/Src/newton.c +158 -0
  66. epyt_flow/EPANET/EPANET-MSX/Src/newton.h +34 -0
  67. epyt_flow/EPANET/EPANET-MSX/Src/rk5.c +287 -0
  68. epyt_flow/EPANET/EPANET-MSX/Src/rk5.h +39 -0
  69. epyt_flow/EPANET/EPANET-MSX/Src/ros2.c +293 -0
  70. epyt_flow/EPANET/EPANET-MSX/Src/ros2.h +35 -0
  71. epyt_flow/EPANET/EPANET-MSX/Src/smatrix.c +816 -0
  72. epyt_flow/EPANET/EPANET-MSX/Src/smatrix.h +29 -0
  73. epyt_flow/EPANET/EPANET-MSX/readme.txt +14 -0
  74. epyt_flow/EPANET/compile.sh +4 -0
  75. epyt_flow/VERSION +1 -0
  76. epyt_flow/__init__.py +24 -0
  77. epyt_flow/data/__init__.py +0 -0
  78. epyt_flow/data/benchmarks/__init__.py +11 -0
  79. epyt_flow/data/benchmarks/batadal.py +257 -0
  80. epyt_flow/data/benchmarks/batadal_data.py +28 -0
  81. epyt_flow/data/benchmarks/battledim.py +473 -0
  82. epyt_flow/data/benchmarks/battledim_data.py +51 -0
  83. epyt_flow/data/benchmarks/gecco_water_quality.py +267 -0
  84. epyt_flow/data/benchmarks/leakdb.py +592 -0
  85. epyt_flow/data/benchmarks/leakdb_data.py +18923 -0
  86. epyt_flow/data/benchmarks/water_usage.py +123 -0
  87. epyt_flow/data/networks.py +650 -0
  88. epyt_flow/gym/__init__.py +4 -0
  89. epyt_flow/gym/control_gyms.py +47 -0
  90. epyt_flow/gym/scenario_control_env.py +101 -0
  91. epyt_flow/metrics.py +404 -0
  92. epyt_flow/models/__init__.py +2 -0
  93. epyt_flow/models/event_detector.py +31 -0
  94. epyt_flow/models/sensor_interpolation_detector.py +118 -0
  95. epyt_flow/rest_api/__init__.py +4 -0
  96. epyt_flow/rest_api/base_handler.py +70 -0
  97. epyt_flow/rest_api/res_manager.py +95 -0
  98. epyt_flow/rest_api/scada_data_handler.py +476 -0
  99. epyt_flow/rest_api/scenario_handler.py +352 -0
  100. epyt_flow/rest_api/server.py +106 -0
  101. epyt_flow/serialization.py +438 -0
  102. epyt_flow/simulation/__init__.py +5 -0
  103. epyt_flow/simulation/events/__init__.py +6 -0
  104. epyt_flow/simulation/events/actuator_events.py +259 -0
  105. epyt_flow/simulation/events/event.py +81 -0
  106. epyt_flow/simulation/events/leakages.py +404 -0
  107. epyt_flow/simulation/events/sensor_faults.py +267 -0
  108. epyt_flow/simulation/events/sensor_reading_attack.py +185 -0
  109. epyt_flow/simulation/events/sensor_reading_event.py +170 -0
  110. epyt_flow/simulation/events/system_event.py +88 -0
  111. epyt_flow/simulation/parallel_simulation.py +147 -0
  112. epyt_flow/simulation/scada/__init__.py +3 -0
  113. epyt_flow/simulation/scada/advanced_control.py +134 -0
  114. epyt_flow/simulation/scada/scada_data.py +1589 -0
  115. epyt_flow/simulation/scada/scada_data_export.py +255 -0
  116. epyt_flow/simulation/scenario_config.py +608 -0
  117. epyt_flow/simulation/scenario_simulator.py +1897 -0
  118. epyt_flow/simulation/scenario_visualizer.py +61 -0
  119. epyt_flow/simulation/sensor_config.py +1289 -0
  120. epyt_flow/topology.py +290 -0
  121. epyt_flow/uncertainty/__init__.py +3 -0
  122. epyt_flow/uncertainty/model_uncertainty.py +302 -0
  123. epyt_flow/uncertainty/sensor_noise.py +73 -0
  124. epyt_flow/uncertainty/uncertainties.py +555 -0
  125. epyt_flow/uncertainty/utils.py +206 -0
  126. epyt_flow/utils.py +306 -0
  127. epyt_flow-0.1.0.dist-info/LICENSE +21 -0
  128. epyt_flow-0.1.0.dist-info/METADATA +139 -0
  129. epyt_flow-0.1.0.dist-info/RECORD +131 -0
  130. epyt_flow-0.1.0.dist-info/WHEEL +5 -0
  131. epyt_flow-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,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,3 @@
1
+ from .scada_data import *
2
+ from .scada_data_export import *
3
+ from .advanced_control import *
@@ -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()