epyt-flow 0.6.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.6.0
1
+ 0.7.0
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
 
@@ -309,6 +309,39 @@ class JsonSerializable(Serializable):
309
309
  """
310
310
  return my_load_from_json(data)
311
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
+
312
345
 
313
346
  def load(data: Union[bytes, BufferedIOBase]) -> Any:
314
347
  """
@@ -2150,7 +2150,7 @@ class ScadaData(Serializable):
2150
2150
  raise TypeError("'bulk_species_sensor_locations' must be an instance of 'dict'" +
2151
2151
  f" but not of '{type(bulk_species_sensor_locations)}'")
2152
2152
  for species_id in bulk_species_sensor_locations:
2153
- if species_id not in self.__sensor_config.bulk_species_sensors:
2153
+ if species_id not in self.__sensor_config.bulk_species_node_sensors:
2154
2154
  raise ValueError(f"Species '{species_id}' is not included in the " +
2155
2155
  "sensor configuration")
2156
2156
 
@@ -94,6 +94,7 @@ class ScenarioSimulator():
94
94
  self.__controls = []
95
95
  self.__system_events = []
96
96
  self.__sensor_reading_events = []
97
+ self.__running_simulation = False
97
98
 
98
99
  custom_epanet_lib = None
99
100
  custom_epanetmsx_lib = None
@@ -150,7 +151,8 @@ class ScenarioSimulator():
150
151
 
151
152
  self.epanet_api = epanet(my_f_inp_in, ph=self.__f_msx_in is None,
152
153
  customlib=custom_epanet_lib, loadfile=True,
153
- display_msg=epanet_verbose)
154
+ display_msg=epanet_verbose,
155
+ display_warnings=False)
154
156
 
155
157
  if self.__f_msx_in is not None:
156
158
  self.epanet_api.loadMSXFile(my_f_msx_in, customMSXlib=custom_epanetmsx_lib)
@@ -432,7 +434,7 @@ class ScenarioSimulator():
432
434
  new_sensor_config.quality_node_sensors = self.__sensor_config.quality_node_sensors
433
435
  new_sensor_config.quality_link_sensors = self.__sensor_config.quality_link_sensors
434
436
  new_sensor_config.pump_state_sensors = self.__sensor_config.pump_state_sensors
435
- new_sensor_config.pump_efficiecy_sensors = self.__sensor_config.pump_efficiency_sensors
437
+ new_sensor_config.pump_efficiency_sensors = self.__sensor_config.pump_efficiency_sensors
436
438
  new_sensor_config.pump_energyconsumption_sensors = self.__sensor_config.\
437
439
  pump_energyconsumption_sensors
438
440
  new_sensor_config.valve_state_sensors = self.__sensor_config.valve_state_sensors
@@ -456,8 +458,6 @@ class ScenarioSimulator():
456
458
 
457
459
  if self.__my_f_inp_in is not None:
458
460
  shutil.rmtree(pathlib.Path(self.__my_f_inp_in).parent)
459
- if self.__my_f_msx_in is not None:
460
- shutil.rmtree(pathlib.Path(self.__my_f_msx_in).parent)
461
461
 
462
462
  def __enter__(self):
463
463
  return self
@@ -744,6 +744,19 @@ class ScenarioSimulator():
744
744
  "units": qualityunit_to_id(qual_info.QualityChemUnits),
745
745
  "trace_node_id": qual_info.TraceNode}
746
746
 
747
+ def get_reporting_time_step(self) -> int:
748
+ """
749
+ Gets the reporting time steps -- i.e. time steps at which sensor readings are provided.
750
+
751
+ Is always a multiple of the hydraulic time step.
752
+
753
+ Returns
754
+ -------
755
+ `int`
756
+ Reporting time steps in seconds.
757
+ """
758
+ return self.epanet_api.getTimeReportingStep()
759
+
747
760
  def get_scenario_config(self) -> ScenarioConfig:
748
761
  """
749
762
  Gets the configuration of this scenario -- i.e. all information & elements
@@ -758,7 +771,7 @@ class ScenarioSimulator():
758
771
 
759
772
  general_params = {"hydraulic_time_step": self.get_hydraulic_time_step(),
760
773
  "quality_time_step": self.get_quality_time_step(),
761
- "reporting_time_step": self.epanet_api.getTimeReportingStep(),
774
+ "reporting_time_step": self.get_reporting_time_step(),
762
775
  "simulation_duration": self.get_simulation_duration(),
763
776
  "flow_units_id": self.get_flow_units(),
764
777
  "quality_model": self.get_quality_model(),
@@ -874,6 +887,9 @@ class ScenarioSimulator():
874
887
  """
875
888
  Randomizes all demand patterns.
876
889
  """
890
+ if self.__running_simulation is True:
891
+ raise RuntimeError("Can not change general parameters when simulation is running.")
892
+
877
893
  self.__adapt_to_network_changes()
878
894
 
879
895
  # Get all demand patterns
@@ -996,6 +1012,9 @@ class ScenarioSimulator():
996
1012
  event : :class:`~epyt_flow.simulation.events.system_event.SystemEvent`
997
1013
  System event.
998
1014
  """
1015
+ if self.__running_simulation is True:
1016
+ raise RuntimeError("Can not add events when simulation is running.")
1017
+
999
1018
  self.__adapt_to_network_changes()
1000
1019
 
1001
1020
  if not isinstance(event, SystemEvent):
@@ -1015,6 +1034,9 @@ class ScenarioSimulator():
1015
1034
  sensor_fault_event : :class:`~epyt_flow.simulation.events.sensor_faults.SensorFault`
1016
1035
  Sensor fault specifications.
1017
1036
  """
1037
+ if self.__running_simulation is True:
1038
+ raise RuntimeError("Can not add events when simulation is running.")
1039
+
1018
1040
  self.__adapt_to_network_changes()
1019
1041
 
1020
1042
  sensor_fault_event.validate(self.__sensor_config)
@@ -1035,6 +1057,9 @@ class ScenarioSimulator():
1035
1057
  sensor_reading_attack : :class:`~epyt_flow.simulation.events.sensor_reading_attack.SensorReadingAttack`
1036
1058
  Sensor fault specifications.
1037
1059
  """
1060
+ if self.__running_simulation is True:
1061
+ raise RuntimeError("Can not add events when simulation is running.")
1062
+
1038
1063
  self.__adapt_to_network_changes()
1039
1064
 
1040
1065
  sensor_reading_attack.validate(self.__sensor_config)
@@ -1055,6 +1080,9 @@ class ScenarioSimulator():
1055
1080
  event : :class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`
1056
1081
  Sensor reading event.
1057
1082
  """
1083
+ if self.__running_simulation is True:
1084
+ raise RuntimeError("Can not add events when simulation is running.")
1085
+
1058
1086
  self.__adapt_to_network_changes()
1059
1087
 
1060
1088
  event.validate(self.__sensor_config)
@@ -1134,11 +1162,21 @@ class ScenarioSimulator():
1134
1162
  """
1135
1163
  self.set_sensors(SENSOR_TYPE_NODE_PRESSURE, sensor_locations)
1136
1164
 
1137
- def place_pressure_sensors_everywhere(self) -> None:
1165
+ def place_pressure_sensors_everywhere(self, junctions_only: bool = False) -> None:
1138
1166
  """
1139
1167
  Places a pressure sensor at every node in the network.
1168
+
1169
+ Parameters
1170
+ ----------
1171
+ junctions_only : `bool`, optional
1172
+ If True, pressure sensors are only placed at junctions but not at tanks and reservoirs.
1173
+
1174
+ The default is False.
1140
1175
  """
1141
- self.set_pressure_sensors(self.__sensor_config.nodes)
1176
+ if junctions_only is True:
1177
+ self.set_pressure_sensors(self.epanet_api.getNodeJunctionNameID())
1178
+ else:
1179
+ self.set_pressure_sensors(self.__sensor_config.nodes)
1142
1180
 
1143
1181
  def set_flow_sensors(self, sensor_locations: list[str]) -> None:
1144
1182
  """
@@ -1442,6 +1480,9 @@ class ScenarioSimulator():
1442
1480
  :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
1443
1481
  Quality simulation results as SCADA data.
1444
1482
  """
1483
+ if self.__running_simulation is True:
1484
+ raise RuntimeError("A simulation is already running.")
1485
+
1445
1486
  if self.__f_msx_in is None:
1446
1487
  raise ValueError("No .msx file specified")
1447
1488
 
@@ -1506,6 +1547,9 @@ class ScenarioSimulator():
1506
1547
  Generator containing the current EPANET-MSX simulation results as SCADA data
1507
1548
  (i.e. species concentrations).
1508
1549
  """
1550
+ if self.__running_simulation is True:
1551
+ raise RuntimeError("A simulation is already running.")
1552
+
1509
1553
  if self.__f_msx_in is None:
1510
1554
  raise ValueError("No .msx file specified")
1511
1555
 
@@ -1522,6 +1566,8 @@ class ScenarioSimulator():
1522
1566
 
1523
1567
  self.epanet_api.initializeMSXQualityAnalysis(ToolkitConstants.EN_NOSAVE)
1524
1568
 
1569
+ self.__running_simulation = True
1570
+
1525
1571
  bulk_species_idx = self.epanet_api.getMSXSpeciesIndex(self.__sensor_config.bulk_species)
1526
1572
  surface_species_idx = self.epanet_api.getMSXSpeciesIndex(
1527
1573
  self.__sensor_config.surface_species)
@@ -1614,6 +1660,7 @@ class ScenarioSimulator():
1614
1660
 
1615
1661
  # Run step-by-step simulation
1616
1662
  tleft = 1
1663
+ total_time = 0
1617
1664
  while tleft > 0:
1618
1665
  if support_abort is True: # Can the simulation be aborted? If so, handle it.
1619
1666
  abort = yield
@@ -1656,6 +1703,8 @@ class ScenarioSimulator():
1656
1703
  sensor_noise=self.__sensor_noise,
1657
1704
  frozen_sensor_config=frozen_sensor_config)
1658
1705
 
1706
+ self.__running_simulation = False
1707
+
1659
1708
  def run_basic_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
1660
1709
  frozen_sensor_config: bool = False) -> ScadaData:
1661
1710
  """
@@ -1681,6 +1730,9 @@ class ScenarioSimulator():
1681
1730
  :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
1682
1731
  Quality simulation results as SCADA data.
1683
1732
  """
1733
+ if self.__running_simulation is True:
1734
+ raise RuntimeError("A simulation is already running.")
1735
+
1684
1736
  result = None
1685
1737
 
1686
1738
  # Run simulation step-by-step
@@ -1741,6 +1793,9 @@ class ScenarioSimulator():
1741
1793
  :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
1742
1794
  Generator with the current simulation results/states as SCADA data.
1743
1795
  """
1796
+ if self.__running_simulation is True:
1797
+ raise RuntimeError("A simulation is already running.")
1798
+
1744
1799
  requested_time_step = self.epanet_api.getTimeHydraulicStep()
1745
1800
  reporting_time_start = self.epanet_api.getTimeReportingStart()
1746
1801
  reporting_time_step = self.epanet_api.getTimeReportingStep()
@@ -1834,6 +1889,9 @@ class ScenarioSimulator():
1834
1889
  :class:`~epyt_flow.simulation.scada.scada_data.ScadaData`
1835
1890
  Simulation results as SCADA data (i.e. sensor readings).
1836
1891
  """
1892
+ if self.__running_simulation is True:
1893
+ raise RuntimeError("A simulation is already running.")
1894
+
1837
1895
  self.__adapt_to_network_changes()
1838
1896
 
1839
1897
  result = None
@@ -1928,10 +1986,15 @@ class ScenarioSimulator():
1928
1986
  Generator with the current simulation results/states as SCADA data
1929
1987
  (i.e. sensor readings).
1930
1988
  """
1989
+ if self.__running_simulation is True:
1990
+ raise RuntimeError("A simulation is already running.")
1991
+
1931
1992
  self.__adapt_to_network_changes()
1932
1993
 
1933
1994
  self.__prepare_simulation()
1934
1995
 
1996
+ self.__running_simulation = True
1997
+
1935
1998
  self.epanet_api.openHydraulicAnalysis()
1936
1999
  self.epanet_api.openQualityAnalysis()
1937
2000
  self.epanet_api.initializeHydraulicAnalysis(ToolkitConstants.EN_SAVE)
@@ -2041,9 +2104,12 @@ class ScenarioSimulator():
2041
2104
  self.epanet_api.closeQualityAnalysis()
2042
2105
  self.epanet_api.closeHydraulicAnalysis()
2043
2106
 
2107
+ self.__running_simulation = False
2108
+
2044
2109
  if hyd_export is not None:
2045
2110
  self.epanet_api.saveHydraulicFile(hyd_export)
2046
2111
  except Exception as ex:
2112
+ self.__running_simulation = False
2047
2113
  raise ex
2048
2114
 
2049
2115
  def set_model_uncertainty(self, model_uncertainty: ModelUncertainty) -> None:
@@ -2055,6 +2121,9 @@ class ScenarioSimulator():
2055
2121
  model_uncertainty : :class:`~epyt_flow.uncertainty.model_uncertainty.ModelUncertainty`
2056
2122
  Model uncertainty specifications.
2057
2123
  """
2124
+ if self.__running_simulation is True:
2125
+ raise RuntimeError("Can not set uncertainties when simulation is running.")
2126
+
2058
2127
  self.__adapt_to_network_changes()
2059
2128
 
2060
2129
  if not isinstance(model_uncertainty, ModelUncertainty):
@@ -2073,6 +2142,9 @@ class ScenarioSimulator():
2073
2142
  sensor_noise : :class:`~epyt_flow.uncertainties.sensor_noise.SensorNoise`
2074
2143
  Sensor noise specification.
2075
2144
  """
2145
+ if self.__running_simulation is True:
2146
+ raise RuntimeError("Can not set sensor noise when simulation is running.")
2147
+
2076
2148
  self.__adapt_to_network_changes()
2077
2149
 
2078
2150
  if not isinstance(sensor_noise, SensorNoise):
@@ -2155,6 +2227,9 @@ class ScenarioSimulator():
2155
2227
 
2156
2228
  The default is None.
2157
2229
  """
2230
+ if self.__running_simulation is True:
2231
+ raise RuntimeError("Can not change general parameters when simulation is running.")
2232
+
2158
2233
  self.__adapt_to_network_changes()
2159
2234
 
2160
2235
  if flow_units_id is not None:
@@ -2277,6 +2352,9 @@ class ScenarioSimulator():
2277
2352
  Sets water age analysis -- i.e. estimates the water age (in hours) at
2278
2353
  all places in the network.
2279
2354
  """
2355
+ if self.__running_simulation is True:
2356
+ raise RuntimeError("Can not change general parameters when simulation is running.")
2357
+
2280
2358
  self.__adapt_to_network_changes()
2281
2359
 
2282
2360
  self.__warn_if_quality_set()
@@ -2305,6 +2383,9 @@ class ScenarioSimulator():
2305
2383
 
2306
2384
  The default is MASS_UNIT_MG.
2307
2385
  """
2386
+ if self.__running_simulation is True:
2387
+ raise RuntimeError("Can not change general parameters when simulation is running.")
2388
+
2308
2389
  self.__adapt_to_network_changes()
2309
2390
 
2310
2391
  self.__warn_if_quality_set()
@@ -2349,6 +2430,9 @@ class ScenarioSimulator():
2349
2430
 
2350
2431
  The default is 1.
2351
2432
  """
2433
+ if self.__running_simulation is True:
2434
+ raise RuntimeError("Can not change general parameters when simulation is running.")
2435
+
2352
2436
  self.__adapt_to_network_changes()
2353
2437
 
2354
2438
  if self.epanet_api.getQualityInfo().QualityCode != ToolkitConstants.EN_CHEM:
@@ -2385,6 +2469,9 @@ class ScenarioSimulator():
2385
2469
  trace_node_id : `str`
2386
2470
  ID of the node traced in the source tracing analysis.
2387
2471
  """
2472
+ if self.__running_simulation is True:
2473
+ raise RuntimeError("Can not change general parameters when simulation is running.")
2474
+
2388
2475
  self.__adapt_to_network_changes()
2389
2476
 
2390
2477
  if trace_node_id not in self.__sensor_config.nodes:
@@ -284,7 +284,7 @@ def is_flowunit_simetric(unit_id: int) -> bool:
284
284
  `bool`
285
285
  True if the fiven unit is a SI metric unit, False otherwise.
286
286
  """
287
- return unit_id in [ToolkitConstants.EN_LPM, ToolkitConstants.EN_MLD,
287
+ return unit_id in [ToolkitConstants.EN_LPS, ToolkitConstants.EN_LPM, ToolkitConstants.EN_MLD,
288
288
  ToolkitConstants.EN_CMH, ToolkitConstants.EN_CMD]
289
289
 
290
290
 
epyt_flow/topology.py CHANGED
@@ -507,11 +507,11 @@ class NetworkTopology(nx.Graph, JsonSerializable):
507
507
 
508
508
  return super().__eq__(other) and \
509
509
  self.get_all_nodes() == other.get_all_nodes() \
510
- and all(link_a[0] == link_b[0] and all(link_a[1] == link_b[1])
510
+ and all(link_a[0] == link_b[0] and link_a[1] == link_b[1]
511
511
  for link_a, link_b in zip(self.get_all_links(), other.get_all_links())) \
512
512
  and self.__units == other.units \
513
- and self.__pumps == other.pumps \
514
- and self.__valves == other.valves
513
+ and self.get_all_pumps() == other.get_all_pumps() \
514
+ and self.get_all_valves() == other.get_all_valves()
515
515
 
516
516
  def __str__(self) -> str:
517
517
  return f"f_inp: {self.name} nodes: {self.__nodes} links: {self.__links} " +\
@@ -334,8 +334,16 @@ class PercentageDeviationUncertainty(UniformUncertainty, JsonSerializable):
334
334
  if not 0 < deviation_percentage < 1:
335
335
  raise ValueError("'deviation_percentage' must be in (0,1)")
336
336
 
337
+ if "low" in kwds:
338
+ del kwds["low"]
339
+ if "high" in kwds:
340
+ del kwds["high"]
341
+
337
342
  super().__init__(low=1. - deviation_percentage, high=1. + deviation_percentage, **kwds)
338
343
 
344
+ def get_attributes(self) -> dict:
345
+ return super().get_attributes() | {"deviation_percentage": self.high - 1.}
346
+
339
347
  def apply(self, data: float) -> float:
340
348
  data *= np.random.uniform(low=self.low, high=self.high)
341
349
 
epyt_flow/utils.py CHANGED
@@ -68,7 +68,8 @@ def volume_to_level(tank_volume: float, tank_diameter: float) -> float:
68
68
 
69
69
 
70
70
  def plot_timeseries_data(data: np.ndarray, labels: list[str] = None, x_axis_label: str = None,
71
- y_axis_label: str = None, show: bool = True,
71
+ y_axis_label: str = None, y_ticks: tuple[list[float], list[str]] = None,
72
+ show: bool = True, save_to_file: str = None,
72
73
  ax: matplotlib.axes.Axes = None) -> matplotlib.axes.Axes:
73
74
  """
74
75
  Plots a single or multiple time series.
@@ -89,6 +90,10 @@ def plot_timeseries_data(data: np.ndarray, labels: list[str] = None, x_axis_labe
89
90
  y_axis_label : `str`, optional
90
91
  Y axis label.
91
92
 
93
+ The default is None.
94
+ y_ticks: `(list[float], list[str])`, optional
95
+ Tuple of ticks (numbers) and labels (strings) for the y-axis.
96
+
92
97
  The default is None.
93
98
  show : `bool`, optional
94
99
  If True, the plot/figure is shown in a window.
@@ -96,6 +101,13 @@ def plot_timeseries_data(data: np.ndarray, labels: list[str] = None, x_axis_labe
96
101
  Only considered when 'ax' is None.
97
102
 
98
103
  The default is True.
104
+ save_to_file : `str`, optional
105
+ File to which the plot is saved.
106
+
107
+ If specified, 'show' must be set to False --
108
+ i.e. a plot can not be shown and saved to a file at the same time!
109
+
110
+ The default is None.
99
111
  ax : `matplotlib.axes.Axes`, optional
100
112
  If not None, 'ax' is used for plotting.
101
113
 
@@ -122,8 +134,18 @@ def plot_timeseries_data(data: np.ndarray, labels: list[str] = None, x_axis_labe
122
134
  if not isinstance(y_axis_label, str):
123
135
  raise TypeError("'y_axis_label' must be an instance of 'str' " +
124
136
  f"but not of '{type(y_axis_label)}'")
137
+ if y_ticks is not None:
138
+ if len(y_ticks) != 2:
139
+ raise ValueError("'y_ticks' must be a tuple ticks (numbers) and labels (strings)")
125
140
  if not isinstance(show, bool):
126
141
  raise TypeError(f"'show' must be an instance of 'bool' but not of '{type(show)}'")
142
+ if save_to_file is not None:
143
+ if show is True:
144
+ raise ValueError("'show' must be False if 'save_to_file' is set")
145
+
146
+ if not isinstance(save_to_file, str):
147
+ raise TypeError("'save_to_file' must be an instance of 'str' but not of " +
148
+ f"'{type(save_to_file)}'")
127
149
  if ax is not None:
128
150
  if not isinstance(ax, matplotlib.axes.Axes):
129
151
  raise TypeError("ax' must be an instance of 'matplotlib.axes.Axes'" +
@@ -145,9 +167,20 @@ def plot_timeseries_data(data: np.ndarray, labels: list[str] = None, x_axis_labe
145
167
  ax.set_xlabel(x_axis_label)
146
168
  if y_axis_label is not None:
147
169
  ax.set_ylabel(y_axis_label)
170
+ if y_ticks is not None:
171
+ yticks_pos, yticks_labels = y_ticks
172
+ ax.set_yticks(yticks_pos, labels=yticks_labels)
148
173
 
149
174
  if show is True and fig is not None:
150
175
  plt.show()
176
+ if save_to_file is not None:
177
+ folder_path = str(Path(save_to_file).parent.absolute())
178
+ create_path_if_not_exist(folder_path)
179
+
180
+ if fig is None:
181
+ plt.savefig(save_to_file, bbox_inches='tight')
182
+ else:
183
+ fig.savefig(save_to_file, bbox_inches='tight')
151
184
 
152
185
  return ax
153
186
 
@@ -155,7 +188,9 @@ def plot_timeseries_data(data: np.ndarray, labels: list[str] = None, x_axis_labe
155
188
  def plot_timeseries_prediction(y: np.ndarray, y_pred: np.ndarray,
156
189
  confidence_interval: np.ndarray = None,
157
190
  x_axis_label: str = None, y_axis_label: str = None,
158
- show: bool = True, ax: matplotlib.axes.Axes = None
191
+ y_ticks: tuple[list[float], list[str]] = None,
192
+ show: bool = True, save_to_file: str = None,
193
+ ax: matplotlib.axes.Axes = None
159
194
  ) -> matplotlib.axes.Axes:
160
195
  """
161
196
  Plots the prediction (e.g. forecast) of *single* time series together with the
@@ -179,6 +214,10 @@ def plot_timeseries_prediction(y: np.ndarray, y_pred: np.ndarray,
179
214
  y_axis_label : `str`, optional
180
215
  Y axis label.
181
216
 
217
+ The default is None.
218
+ y_ticks: `(list[float], list[str])`, optional
219
+ Tuple of ticks (numbers) and labels (strings) for the y-axis.
220
+
182
221
  The default is None.
183
222
  show : `bool`, optional
184
223
  If True, the plot/figure is shown in a window.
@@ -186,6 +225,13 @@ def plot_timeseries_prediction(y: np.ndarray, y_pred: np.ndarray,
186
225
  Only considered when 'ax' is None.
187
226
 
188
227
  The default is True.
228
+ save_to_file : `str`, optional
229
+ File to which the plot is saved.
230
+
231
+ If specified, 'show' must be set to False --
232
+ i.e. a plot can not be shown and saved to a file at the same time!
233
+
234
+ The default is None.
189
235
  ax : `matplotlib.axes.Axes`, optional
190
236
  If not None, 'axes' is used for plotting.
191
237
 
@@ -216,8 +262,18 @@ def plot_timeseries_prediction(y: np.ndarray, y_pred: np.ndarray,
216
262
  if not isinstance(y_axis_label, str):
217
263
  raise TypeError("'y_axis_label' must be an instance of 'str' " +
218
264
  f"but not of '{type(y_axis_label)}'")
265
+ if y_ticks is not None:
266
+ if len(y_ticks) != 2:
267
+ raise ValueError("'y_ticks' must be a tuple ticks (numbers) and labels (strings)")
219
268
  if not isinstance(show, bool):
220
269
  raise TypeError(f"'show' must be an instance of 'bool' but not of '{type(show)}'")
270
+ if save_to_file is not None:
271
+ if show is True:
272
+ raise ValueError("'show' must be False if 'save_to_file' is set")
273
+
274
+ if not isinstance(save_to_file, str):
275
+ raise TypeError("'save_to_file' must be an instance of 'str' but not of " +
276
+ f"'{type(save_to_file)}'")
221
277
  if ax is not None:
222
278
  if not isinstance(ax, matplotlib.axes.Axes):
223
279
  raise TypeError("ax' must be an instance of 'matplotlib.axes.Axes'" +
@@ -240,9 +296,20 @@ def plot_timeseries_prediction(y: np.ndarray, y_pred: np.ndarray,
240
296
  ax.set_xlabel(x_axis_label)
241
297
  if y_axis_label is not None:
242
298
  ax.set_ylabel(y_axis_label)
299
+ if y_ticks is not None:
300
+ yticks_pos, yticks_labels = y_ticks
301
+ ax.set_yticks(yticks_pos, labels=yticks_labels)
243
302
 
244
303
  if show is True and fig is not None:
245
304
  plt.show()
305
+ if save_to_file is not None:
306
+ folder_path = str(Path(save_to_file).parent.absolute())
307
+ create_path_if_not_exist(folder_path)
308
+
309
+ if fig is None:
310
+ plt.savefig(save_to_file, bbox_inches='tight')
311
+ else:
312
+ fig.savefig(save_to_file, bbox_inches='tight')
246
313
 
247
314
  return ax
248
315
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: epyt-flow
3
- Version: 0.6.0
3
+ Version: 0.7.0
4
4
  Summary: EPyT-Flow -- EPANET Python Toolkit - Flow
5
5
  Author-email: André Artelt <aartelt@techfak.uni-bielefeld.de>, "Marios S. Kyriakou" <kiriakou.marios@ucy.ac.cy>, "Stelios G. Vrachimis" <vrachimis.stelios@ucy.ac.cy>
6
6
  License: MIT License
@@ -20,7 +20,7 @@ Classifier: Programming Language :: Python :: 3.12
20
20
  Requires-Python: >=3.9
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
- Requires-Dist: epyt >=1.1.9
23
+ Requires-Dist: epyt >=1.2.0
24
24
  Requires-Dist: requests >=2.31.0
25
25
  Requires-Dist: scipy >=1.11.4
26
26
  Requires-Dist: u-msgpack-python >=2.8.0
@@ -121,7 +121,7 @@ pip install .
121
121
  ```python
122
122
  from epyt_flow.data.benchmarks import load_leakdb_scenarios
123
123
  from epyt_flow.simulation import ScenarioSimulator
124
- from epyt_flow.utils import to_seconds
124
+ from epyt_flow.utils import to_seconds, plot_timeseries_data
125
125
 
126
126
 
127
127
  if __name__ == "__main__":
@@ -142,14 +142,49 @@ if __name__ == "__main__":
142
142
  # Run entire simulation
143
143
  scada_data = sim.run_simulation()
144
144
 
145
- # Show sensor readings over the entire simulation
145
+ # Print & plot sensor readings over the entire simulation
146
146
  print(f"Pressure readings: {scada_data.get_data_pressures()}")
147
+ plot_timeseries_data(scada_data.get_data_pressures().T,
148
+ labels=[f"Node {n_id}" for n_id in
149
+ scada_data.sensor_config.pressure_sensors],
150
+ x_axis_label="Time (30min steps)",
151
+ y_axis_label="Pressure in $m$")
152
+
147
153
  print(f"Flow readings: {scada_data.get_data_flows()}")
154
+ plot_timeseries_data(scada_data.get_data_flows().T,
155
+ x_axis_label="Time (30min steps)",
156
+ y_axis_label="Flow rate in $m^3/h$")
148
157
  ```
158
+ ### Generated plots
159
+
160
+ <div>
161
+ <img src="https://github.com/WaterFutures/EPyT-Flow/blob/dev/docs/_static/examples_basic_usage_pressure.png?raw=true" width="49%"/>
162
+ <img src="https://github.com/WaterFutures/EPyT-Flow/blob/dev/docs/_static/examples_basic_usage_flow.png?raw=true" width="49%"/>
163
+ </div>
149
164
 
150
165
  ## Documentation
151
166
 
152
- Documentation is available on readthedocs:[https://epyt-flow.readthedocs.io/en/latest/](https://epyt-flow.readthedocs.io/en/stable)
167
+ Documentation is available on readthedocs: [https://epyt-flow.readthedocs.io/en/latest/](https://epyt-flow.readthedocs.io/en/stable)
168
+
169
+ ## How to Get Started?
170
+
171
+ EPyT-Flow is accompanied by an extensive documentation
172
+ [https://epyt-flow.readthedocs.io/en/latest/](https://epyt-flow.readthedocs.io/en/stable)
173
+ (including many [examples](https://epyt-flow.readthedocs.io/en/stable/#examples)).
174
+
175
+ If you are new to water distribution networks, we recommend first to read the chapter on
176
+ [Modeling of Water Distribution Networks](https://epyt-flow.readthedocs.io/en/stable/tut.intro.html).
177
+ You might also want to check out some lecture notes on
178
+ [Smart Water Systems](https://github.com/KIOS-Research/ece808-smart-water-systems).
179
+
180
+ If you are already familiar with WDNs (and software such as EPANET), we recommend checking out
181
+ our [WDSA CCWI 2024 tutorial](https://github.com/WaterFutures/EPyT-and-EPyT-Flow-Tutorial) which
182
+ not only teaches you how to use EPyT and EPyT-Flow but also contains some examples of applying
183
+ Machine Learning in WDNs.
184
+ Besides that, you can read in-depth about the different functionalities of EPyT-Flow in the
185
+ [In-depth Tutorial](https://epyt-flow.readthedocs.io/en/stable/tutorial.html) of the documentation --
186
+ we recommend reading the chapters in the order in which they are presented;
187
+ you might decide to skip some of the last chapters if their content is not relevant to you.
153
188
 
154
189
  ## License
155
190
 
@@ -170,6 +205,14 @@ If you use this software, please cite it as follows:
170
205
  }
171
206
  ```
172
207
 
208
+ ## How to get Support?
209
+
210
+ If you come across any bug or need assistance please feel free to open a new
211
+ [issue](https://github.com/WaterFutures/EPyT-Flow/issues/)
212
+ if non of the existing issues answers your questions.
213
+
173
214
  ## How to Contribute?
174
215
 
175
- Contributions (e.g. creating issues, pull-requests, etc.) are welcome -- please make sure to read the [code of conduct](CODE_OF_CONDUCT.md) and follow the [developers' guidelines](DEVELOPERS.md).
216
+ Contributions (e.g. creating issues, pull-requests, etc.) are welcome --
217
+ please make sure to read the [code of conduct](CODE_OF_CONDUCT.md) and
218
+ follow the [developers' guidelines](DEVELOPERS.md).
@@ -1,9 +1,9 @@
1
- epyt_flow/VERSION,sha256=l6XW5UCmEg0Jw53bZn4Ojiusf8wv_vgTuC4I_WA2W84,6
1
+ epyt_flow/VERSION,sha256=ln2a-xATRmZxZvLnboGRC8GQSI19QdUMoAcunZLwDjI,6
2
2
  epyt_flow/__init__.py,sha256=KNDiPWiHdB9a5ZF1ipjA1uoq61TwU2ThjaStpvSLBtY,1742
3
- epyt_flow/metrics.py,sha256=kvt42pzZrUR9PSlCyK4uq5kj6UlYHkt7OcCjLnI1RQE,12883
4
- epyt_flow/serialization.py,sha256=YhqDFZ7FLLcYOPnhOQdS0K8uyqM6bIXOkKDzGJQ-d3o,13075
5
- epyt_flow/topology.py,sha256=pWG3Cq48Kq-mql-Lg3HUwvj5BGN0SrySa_OfDYNcGxg,25244
6
- epyt_flow/utils.py,sha256=AB2MuknQ_16UE-URQe1WShIS7dmSyFwZYVHxMVT539k,12379
3
+ epyt_flow/metrics.py,sha256=W-dolnrmWfoanyvg-knoe2QMUtFwV1xODp4D4EwsQ00,14261
4
+ epyt_flow/serialization.py,sha256=ltWcLiTw62s0KG2DSgkQBkn6CCkxy9soDGq_ENohkrI,13998
5
+ epyt_flow/topology.py,sha256=8gqgJrKxw0zY69sIKo4NxrQAoXHP1Ni00U2DV09vR6g,25275
6
+ epyt_flow/utils.py,sha256=GJDktl7ciUPJxqMg9f2nCnQf6DosNd2mxeKOy7omkik,15196
7
7
  epyt_flow/EPANET/compile_linux.sh,sha256=wcrDyiB8NkivmaC-X9FI2WxhY3IJqDLiyIbVTv2XEPY,489
8
8
  epyt_flow/EPANET/compile_macos.sh,sha256=1K33-bPdgr01EIf87YUvmOFHXyOkBWI6mKXQ8x1Hzmo,504
9
9
  epyt_flow/EPANET/EPANET/SRC_engines/AUTHORS,sha256=yie5yAsEEPY0984PmkSRUdqEU9rVvRSGGWmjxdwCYMU,925
@@ -112,9 +112,9 @@ epyt_flow/rest_api/scenario/uncertainty_handlers.py,sha256=uuu6AP11ZZUp2P3Dnukjg
112
112
  epyt_flow/simulation/__init__.py,sha256=VGGJqJRUoXZjKJ0-m6KPp3JQqD_1TFW0pofLgkwZJ8M,164
113
113
  epyt_flow/simulation/parallel_simulation.py,sha256=VmC7xemjxRB_N0fx1AAQ7ux82tnyTi7jk7jfFpeg7gM,6523
114
114
  epyt_flow/simulation/scenario_config.py,sha256=NyadeCihpR4bpsWVPj7J-1DU2w1CEN0pAo2yh0t8Xkg,26751
115
- epyt_flow/simulation/scenario_simulator.py,sha256=_DINJgPVBdKds6hq0w4zJuCIoQVBnkqBwU04KIi5egY,101950
115
+ epyt_flow/simulation/scenario_simulator.py,sha256=VYYUlpukvD9vLs8zmZWTb7-HCrmQlydxw52JyHiWKzE,105251
116
116
  epyt_flow/simulation/scenario_visualizer.py,sha256=fpj67zl69q-byg7Oxocqhmu1S3P7B3ROCkSYzWyM--0,2187
117
- epyt_flow/simulation/sensor_config.py,sha256=HSNT8BAfWo0G4k5PXI8BhWDl8YzpS8CYJqlv0pmbPZc,90921
117
+ epyt_flow/simulation/sensor_config.py,sha256=rX13QQv-OBoybCSJZR0gBsGP0GejBAuJ8BRDULKMIDU,90946
118
118
  epyt_flow/simulation/events/__init__.py,sha256=tIdqzs7_Cus4X2kbZG4Jl2zs-zsk_4rnajFOCvL0zlI,185
119
119
  epyt_flow/simulation/events/actuator_events.py,sha256=2_MPYbYO9As6fMkm5Oy9pjSB9kCvFuKpGu8ykYDAydg,7903
120
120
  epyt_flow/simulation/events/event.py,sha256=kARPV20XCAl6zxnJwI9U7ICtZUPACO_rgAmtHm1mGCs,2603
@@ -125,15 +125,15 @@ epyt_flow/simulation/events/sensor_reading_event.py,sha256=rQ-CmdpSUyZzDFYwNUGH2
125
125
  epyt_flow/simulation/events/system_event.py,sha256=0KI2iaAaOyC9Y-FIfFVazeKT_4ORQRp26gWyMBUu_3c,2396
126
126
  epyt_flow/simulation/scada/__init__.py,sha256=ZFAxJVqwEVsgiyFilFetnb13gPhZg1JEOPWYvKIJT4c,90
127
127
  epyt_flow/simulation/scada/advanced_control.py,sha256=5h7dmSMcNlTE7TMZa8gQVnOCGMf7uZy60r9aOfKDxMc,4487
128
- epyt_flow/simulation/scada/scada_data.py,sha256=YBgVfs2L85XQxHRcC4-X-yTudB2E8p2oYCxqazPrI9k,110586
128
+ epyt_flow/simulation/scada/scada_data.py,sha256=SlJEcAB06JNCZBl0IO8nv-axamcvznzUAg5SwCdjc2U,110591
129
129
  epyt_flow/simulation/scada/scada_data_export.py,sha256=0BwDgV-5qZx17wIyWQ8Nl2TPgho3mBI49027RDq8sDA,11217
130
130
  epyt_flow/uncertainty/__init__.py,sha256=ZRjuJL9rDpWVSdPwObPxFpEmMTcgAl3VmPOsS6cIyGg,89
131
131
  epyt_flow/uncertainty/model_uncertainty.py,sha256=-2QT2AffZerKZyZ_w_mmeqYpfBALyPDvV61sCrvcK1o,13966
132
132
  epyt_flow/uncertainty/sensor_noise.py,sha256=zJVULxnxVPSSqc6UW0iwZ9O-HGf9dn4CwScPqf4yCY0,2324
133
- epyt_flow/uncertainty/uncertainties.py,sha256=X-o7GZUC0HELtzpoXIAJaAeYOw35N05TuRoSmStcCpI,17669
133
+ epyt_flow/uncertainty/uncertainties.py,sha256=jzaAwv5--HGc-H4-SwB0s-pAnzhhFuc06IXck7rC5l8,17902
134
134
  epyt_flow/uncertainty/utils.py,sha256=gq66c9-QMOxOqI6wgWLyFxjVV0fbG0_8Yzd6mQjNYNo,5315
135
- epyt_flow-0.6.0.dist-info/LICENSE,sha256=-4hYIY2BLmCkdOv2_PehEwlnMKTCes8_oyIUXjKtkug,1076
136
- epyt_flow-0.6.0.dist-info/METADATA,sha256=8dlCwOkZghE4xDbbWWQ2dOjMBRGo8m48cZuzhmJ0_ds,7087
137
- epyt_flow-0.6.0.dist-info/WHEEL,sha256=Wyh-_nZ0DJYolHNn1_hMa4lM7uDedD_RGVwbmTjyItk,91
138
- epyt_flow-0.6.0.dist-info/top_level.txt,sha256=Wh_kd7TRL8ownCw3Y3dxx-9C0iTSk6wNauv_NX9JcrY,10
139
- epyt_flow-0.6.0.dist-info/RECORD,,
135
+ epyt_flow-0.7.0.dist-info/LICENSE,sha256=-4hYIY2BLmCkdOv2_PehEwlnMKTCes8_oyIUXjKtkug,1076
136
+ epyt_flow-0.7.0.dist-info/METADATA,sha256=UCqXTyaYY6EEBHf85PKDVH2sFglzFvWK-30vBrGP2mI,9420
137
+ epyt_flow-0.7.0.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
138
+ epyt_flow-0.7.0.dist-info/top_level.txt,sha256=Wh_kd7TRL8ownCw3Y3dxx-9C0iTSk6wNauv_NX9JcrY,10
139
+ epyt_flow-0.7.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (71.1.0)
2
+ Generator: setuptools (75.3.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5