epyt-flow 0.5.0__py3-none-any.whl → 0.7.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 CHANGED
@@ -1 +1 @@
1
- 0.5.0
1
+ 0.7.0
@@ -458,10 +458,14 @@ def load_scenario(return_test_scenario: bool, download_dir: str = None,
458
458
  ltown_config = load_ltown(use_realistic_demands=True, include_default_sensor_placement=True,
459
459
  verbose=verbose)
460
460
 
461
- # Set simulation duration
461
+ # Set simulation duration and other general parameters such as the demand model
462
462
  general_params = {"simulation_duration": to_seconds(days=365), # One year
463
463
  "hydraulic_time_step": to_seconds(minutes=5), # 5min time steps
464
- "reporting_time_step": to_seconds(minutes=5)} | ltown_config.general_params
464
+ "reporting_time_step": to_seconds(minutes=5),
465
+ "demand_model": {"type": "PDA", "pressure_min": 0,
466
+ "pressure_required": 0.1,
467
+ "pressure_exponent": 0.5}
468
+ } | ltown_config.general_params
465
469
 
466
470
  # Add events
467
471
  start_time = START_TIME_TEST if return_test_scenario is True else START_TIME_TRAIN
@@ -30,7 +30,7 @@ from .leakdb_data import NET1_LEAKAGES, HANOI_LEAKAGES
30
30
  from ...utils import get_temp_folder, to_seconds, unpack_zip_archive, create_path_if_not_exist, \
31
31
  download_if_necessary
32
32
  from ...metrics import f1_score, true_positive_rate, true_negative_rate
33
- from ...simulation import ScenarioSimulator
33
+ from ...simulation import ScenarioSimulator, ToolkitConstants
34
34
  from ...simulation.events import AbruptLeakage, IncipientLeakage
35
35
  from ...simulation import ScenarioConfig
36
36
  from ...simulation.scada import ScadaData
@@ -424,14 +424,19 @@ def load_scenarios(scenarios_id: list[int], use_net1: bool = True,
424
424
  download_dir = download_dir if download_dir is not None else get_temp_folder()
425
425
  network_config = load_network(download_dir)
426
426
 
427
- # Set simulation duration
427
+ # Set simulation duration and other general parameters such as the demand model and flow units
428
428
  hydraulic_time_step = to_seconds(minutes=30) # 30min time steps
429
429
  general_params = {"simulation_duration": to_seconds(days=365), # One year
430
430
  "hydraulic_time_step": hydraulic_time_step,
431
- "reporting_time_step": hydraulic_time_step} | network_config.general_params
431
+ "reporting_time_step": hydraulic_time_step,
432
+ "flow_units_id": ToolkitConstants.EN_CMH,
433
+ "demand_model": {"type": "PDA", "pressure_min": 0,
434
+ "pressure_required": 0.1,
435
+ "pressure_exponent": 0.5}
436
+ } | network_config.general_params
432
437
 
433
438
  # Add demand patterns
434
- def gen_dem(download_dir, use_net1):
439
+ def gen_dem(download_dir):
435
440
  # Taken from https://github.com/KIOS-Research/LeakDB/blob/master/CCWI-WDSA2018/Dataset_Generator_Py3/demandGenerator.py
436
441
  week_pat = scipy.io.loadmat(os.path.join(download_dir, "weekPat_30min.mat"))
437
442
  a_w = week_pat['Aw']
@@ -503,10 +508,7 @@ def load_scenarios(scenarios_id: list[int], use_net1: bool = True,
503
508
 
504
509
  if not os.path.exists(f_inp_in):
505
510
  with ScenarioSimulator(f_inp_in=network_config.f_inp_in) as wdn:
506
- wdn.epanet_api.setTimeHydraulicStep(general_params["hydraulic_time_step"])
507
- wdn.epanet_api.setTimeSimulationDuration(general_params["simulation_duration"])
508
- wdn.epanet_api.setTimePatternStep(general_params["hydraulic_time_step"])
509
- wdn.epanet_api.setFlowUnitsCMH()
511
+ wdn.set_general_parameters(**general_params)
510
512
 
511
513
  wdn.epanet_api.deletePatternsAll()
512
514
 
@@ -519,7 +521,7 @@ def load_scenarios(scenarios_id: list[int], use_net1: bool = True,
519
521
  node_idx = wdn.epanet_api.getNodeIndex(node_id)
520
522
  base_demand = wdn.epanet_api.getNodeBaseDemands(node_idx)[1][0]
521
523
 
522
- my_demand_pattern = np.array(gen_dem(download_dir, use_net1))
524
+ my_demand_pattern = np.array(gen_dem(download_dir))
523
525
 
524
526
  wdn.set_node_demand_pattern(node_id=node_id, base_demand=base_demand,
525
527
  demand_pattern_id=f"demand_{node_id}",
@@ -56,8 +56,7 @@ def get_default_hydraulic_options(flow_units_id: int = None) -> dict:
56
56
  Dictionary with default hydraulics options that can be passed to
57
57
  :func:`~epyt_flow.simulation.scenario_simulator.ScenarioSimulator.set_general_parameters`.
58
58
  """
59
- params = {"demand_model": {"type": "PDA", "pressure_min": 0, "pressure_required": 0.1,
60
- "pressure_exponent": 0.5}}
59
+ params = {}
61
60
  if flow_units_id is not None:
62
61
  params |= {"flow_units_id": flow_units_id}
63
62
 
@@ -2,7 +2,9 @@
2
2
  Module provides a base class for control environments.
3
3
  """
4
4
  from abc import abstractmethod, ABC
5
- from copy import deepcopy
5
+ from typing import Union
6
+ import warnings
7
+ import numpy as np
6
8
 
7
9
  from ..simulation import ScenarioSimulator, ScenarioConfig, ScadaData
8
10
 
@@ -32,8 +34,13 @@ class ScenarioControlEnv(ABC):
32
34
  def autoreset(self) -> bool:
33
35
  """
34
36
  True, if environment automatically resets after it terminated.
37
+
38
+ Returns
39
+ -------
40
+ `bool`
41
+ True, if environment automatically resets after it terminated.
35
42
  """
36
- return deepcopy(self.__autoreset)
43
+ return self.__autoreset
37
44
 
38
45
  def __enter__(self):
39
46
  return self
@@ -58,6 +65,11 @@ class ScenarioControlEnv(ABC):
58
65
  def reset(self) -> ScadaData:
59
66
  """
60
67
  Resets the environment (i.e. simulation).
68
+
69
+ Returns
70
+ -------
71
+ :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
72
+ Current SCADA data (i.e. sensor readings).
61
73
  """
62
74
  if self._scenario_sim is not None:
63
75
  self._scenario_sim.close()
@@ -83,19 +95,102 @@ class ScenarioControlEnv(ABC):
83
95
  else:
84
96
  return None, True
85
97
 
98
+ def set_pump_status(self, pump_id: str, status: int) -> None:
99
+ """
100
+ Sets the status of a pump.
101
+
102
+ Parameters
103
+ ----------
104
+ pump_id : `str`
105
+ ID of the pump for which the status is set.
106
+ status : `int`
107
+ New status of the pump -- either active (i.e. open) or inactive (i.e. closed).
108
+
109
+ Must be one of the following constants defined in
110
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
111
+
112
+ - EN_CLOSED = 0
113
+ - EN_OPEN = 1
114
+ """
115
+ pump_idx = self._scenario_sim.epanet_api.getLinkPumpNameID().index(pump_id)
116
+ pump_link_idx = self._scenario_sim.epanet_api.getLinkPumpIndex(pump_idx + 1)
117
+ self._scenario_sim.epanet_api.setLinkStatus(pump_link_idx, status)
118
+
119
+ def set_pump_speed(self, pump_id: str, speed: float) -> None:
120
+ """
121
+ Sets the speed of a pump.
122
+
123
+ Parameters
124
+ ----------
125
+ pump_id : `str`
126
+ ID of the pump for which the pump speed is set.
127
+ speed : `float`
128
+ New pump speed.
129
+ """
130
+ pump_idx = self._scenario_sim.epanet_api.getLinkPumpNameID().index(pump_id)
131
+ pattern_idx = self._scenario_sim.epanet_api.getLinkPumpPatternIndex(pump_idx + 1)
132
+
133
+ if pattern_idx == 0:
134
+ warnings.warn(f"No pattern for pump '{pump_id}' found -- a new pattern is created")
135
+ pattern_idx = self._scenario_sim.epanet_api.addPattern(f"pump_speed_{pump_id}")
136
+ self._scenario_sim.epanet_api.setLinkPumpPatternIndex(pattern_idx)
137
+
138
+ self._scenario_sim.epanet_api.setPattern(pattern_idx, np.array([speed]))
139
+
140
+ def set_valve_status(self, valve_id: str, status: int) -> None:
141
+ """
142
+ Sets the status of a valve.
143
+
144
+ Parameters
145
+ ----------
146
+ valve_id : `str`
147
+ ID of the valve for which the status is set.
148
+ status : `int`
149
+ New status of the valve -- either open or closed.
150
+
151
+ Must be one of the following constants defined in
152
+ :class:`~epyt_flow.simulation.events.actuator_events.ActuatorConstants`:
153
+
154
+ - EN_CLOSED = 0
155
+ - EN_OPEN = 1
156
+ """
157
+ valve_idx = self._scenario_sim.epanet_api.getLinkValveNameID().index(valve_id)
158
+ valve_link_idx = self._scenario_sim.epanet_api.getLinkValveIndex()[valve_idx]
159
+ self._scenario_sim.epanet_api.setLinkStatus(valve_link_idx, status)
160
+
161
+ def set_node_quality_source_value(self, node_id: str, pattern_id: str,
162
+ qual_value: float) -> None:
163
+ """
164
+ Sets the quality source at a particular node to a specific value -- e.g.
165
+ setting the chlorine concentration injection to a specified value.
166
+
167
+ Parameters
168
+ ----------
169
+ node_id : `str`
170
+ ID of the node.
171
+ pattern_id : `str`
172
+ ID of the quality pattern at the specific node.
173
+ qual_value : `float`
174
+ New quality source value.
175
+ """
176
+ node_idx = self._scenario_sim.epanet_api.getNodeIndex(node_id)
177
+ pattern_idx = self._scenario_sim.epanet_api.getPatternIndex(pattern_id)
178
+ self._scenario_sim.epanet_api.setNodeSourceQuality(node_idx, 1)
179
+ self._scenario_sim.epanet_api.setPattern(pattern_idx, np.array([qual_value]))
180
+
86
181
  @abstractmethod
87
- def step(self) -> tuple[ScadaData, float, bool]:
182
+ def step(self, *actions) -> Union[tuple[ScadaData, float, bool], tuple[ScadaData, float]]:
88
183
  """
89
184
  Performs the next step by applying an action and observing
90
185
  the consequences (SCADA data, reward, terminated).
91
186
 
92
187
  Note that `terminated` is only returned if `autoreset=False` otherwise
93
- only SCADA data and reward are returned.
188
+ only the current SCADA data and reward are returned.
94
189
 
95
190
  Returns
96
191
  -------
97
- `(ScadaData, float, bool)`
98
- Triple of observations (:class:`~epyt_flow.simuation.scada.scada_data.ScadaData`),
192
+ `(` :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` `, float, bool)` or `(` :class:`~epyt_flow.simulation.scada.scada_data.ScadaData` `, float)`
193
+ Triple or tuple of observations (:class:`~epyt_flow.simulation.scada.scada_data.ScadaData`),
99
194
  reward (`float`), and terminated (`bool`).
100
195
  """
101
196
  raise NotImplementedError()
epyt_flow/metrics.py CHANGED
@@ -3,12 +3,74 @@ This module provides different metrics for evaluation.
3
3
  """
4
4
  import numpy as np
5
5
  from sklearn.metrics import roc_auc_score as skelarn_roc_auc_score, f1_score as skelarn_f1_scpre, \
6
- mean_absolute_error
6
+ mean_absolute_error, root_mean_squared_error, r2_score as sklearn_r2_score
7
7
 
8
8
 
9
- def running_mse(y_pred: np.ndarray, y: np.ndarray):
9
+ def r2_score(y_pred: np.ndarray, y: np.ndarray) -> float:
10
10
  """
11
- Computes the running Mean Squared Error (MSE).
11
+ Computes the R^2 score (also called the coefficient of determination).
12
+
13
+ Parameters
14
+ ----------
15
+ y_pred : `numpy.ndarray`
16
+ Predicted outputs.
17
+ y : `numpy.ndarray`
18
+ Ground truth outputs.
19
+
20
+ Returns
21
+ -------
22
+ `float`
23
+ R^2 score.
24
+ """
25
+ return sklearn_r2_score(y, y_pred)
26
+
27
+
28
+ def running_r2_score(y_pred: np.ndarray, y: np.ndarray) -> list[float]:
29
+ """
30
+ Computes and returns the running R^2 score -- i.e. the R^2 score for every point in time.
31
+
32
+ Parameters
33
+ ----------
34
+ y_pred : `numpy.ndarray`
35
+ Predicted outputs.
36
+ y : `numpy.ndarray`
37
+ Ground truth outputs.
38
+
39
+ Returns
40
+ -------
41
+ `list[float]`
42
+ The running R^2 score.
43
+ """
44
+ r = []
45
+
46
+ for t in range(2, len(y_pred)):
47
+ r.append(r2_score(y_pred[:t], y[:t]))
48
+
49
+ return r
50
+
51
+
52
+ def mean_squared_error(y_pred: np.ndarray, y: np.ndarray) -> float:
53
+ """
54
+ Computes the Mean Squared Error (MSE).
55
+
56
+ Parameters
57
+ ----------
58
+ y_pred : `numpy.ndarray`
59
+ Predicted outputs.
60
+ y : `numpy.ndarray`
61
+ Ground truth outputs.
62
+
63
+ Returns
64
+ -------
65
+ `float`
66
+ MSE.
67
+ """
68
+ return root_mean_squared_error(y, y_pred)**2
69
+
70
+
71
+ def running_mse(y_pred: np.ndarray, y: np.ndarray) -> list[float]:
72
+ """
73
+ Computes the running Mean Squared Error (MSE) -- i.e. the MSE for every point in time.
12
74
 
13
75
  Parameters
14
76
  ----------
@@ -39,7 +101,7 @@ def running_mse(y_pred: np.ndarray, y: np.ndarray):
39
101
  r_mse = list(esq for esq in e_sq)
40
102
 
41
103
  for i in range(1, len(y)):
42
- r_mse[i] = ((i * r_mse[i - 1]) / (i + 1)) + (r_mse[i] / (i + 1))
104
+ r_mse[i] = float((i * r_mse[i - 1]) / (i + 1)) + (r_mse[i] / (i + 1))
43
105
 
44
106
  return r_mse
45
107
 
@@ -4,6 +4,7 @@ Module provides functions and classes for serialization.
4
4
  from typing import Any, Union
5
5
  from abc import abstractmethod, ABC
6
6
  from io import BufferedIOBase
7
+ import pathlib
7
8
  import importlib
8
9
  import json
9
10
  import gzip
@@ -97,7 +98,9 @@ class Serializable(ABC):
97
98
  Base class for a serializable class -- must be used in conjunction with the
98
99
  :func:`~epyt_flow.serialization.serializable` decorator.
99
100
  """
100
- def __init__(self, **kwds):
101
+ def __init__(self, _parent_path: str = "", **kwds):
102
+ self._parent_path = _parent_path
103
+
101
104
  super().__init__(**kwds)
102
105
 
103
106
  @abstractmethod
@@ -306,6 +309,39 @@ class JsonSerializable(Serializable):
306
309
  """
307
310
  return my_load_from_json(data)
308
311
 
312
+ @staticmethod
313
+ def load_from_json_file(f_in: str) -> Any:
314
+ """
315
+ Deserializes an instance of this class from a JSON file.
316
+
317
+ Parameters
318
+ ----------
319
+ f_in : `str`
320
+ Path to the JSON file from which to deserialize the object.
321
+
322
+ Returns
323
+ -------
324
+ `Any`
325
+ Deserialized object.
326
+ """
327
+ with open(f_in, "r", encoding="utf-8") as f:
328
+ return my_load_from_json(f.read())
329
+
330
+ def save_to_json_file(self, f_out: str) -> None:
331
+ """
332
+ Serializes this instance and stores it in a JSON file.
333
+
334
+ Parameters
335
+ ----------
336
+ f_in : `str`
337
+ Path to the JSON file where this serialized object will be stored.
338
+ """
339
+ if not f_out.endswith(self.file_ext()):
340
+ f_out += self.file_ext()
341
+
342
+ with open(f_out, "w", encoding="utf-8") as f:
343
+ f.write(self.to_json())
344
+
309
345
 
310
346
  def load(data: Union[bytes, BufferedIOBase]) -> Any:
311
347
  """
@@ -376,12 +412,19 @@ def load_from_file(f_in: str, use_compression: bool = True) -> Any:
376
412
  `Any`
377
413
  Deserialized data.
378
414
  """
415
+ inst = None
416
+
379
417
  if use_compression is False:
380
418
  with open(f_in, "rb") as f:
381
- return umsgpack.unpack(f, ext_handlers=ext_handler_unpack)
419
+ inst = load(f.read())
382
420
  else:
383
421
  with gzip.open(f_in, "rb") as f:
384
- return load(f.read())
422
+ inst = load(f.read())
423
+
424
+ if isinstance(inst, Serializable):
425
+ inst._parent_path = pathlib.Path(f_in).parent.resolve()
426
+
427
+ return inst
385
428
 
386
429
 
387
430
  def save_to_file(f_out: str, data: Any, use_compression: bool = True) -> None:
@@ -98,7 +98,7 @@ class AdvancedControlModule(ABC):
98
98
  - EN_OPEN = 1
99
99
  """
100
100
  valve_idx = self._epanet_api.getLinkValveNameID().index(valve_id)
101
- valve_link_idx = self._epanet_api.getLinkValveIndex(valve_idx + 1)
101
+ valve_link_idx = self._epanet_api.getLinkValveIndex()[valve_idx]
102
102
  self._epanet_api.setLinkStatus(valve_link_idx, status)
103
103
 
104
104
  def set_node_quality_source_value(self, node_id: str, pattern_id: str,