epyt-flow 0.9.0__py3-none-any.whl → 0.10.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.
@@ -5,6 +5,8 @@ import sys
5
5
  import os
6
6
  import pathlib
7
7
  import time
8
+ from datetime import timedelta
9
+ from datetime import datetime
8
10
  from typing import Generator, Union
9
11
  from copy import deepcopy
10
12
  import shutil
@@ -28,7 +30,8 @@ from .sensor_config import SensorConfig, areaunit_to_id, massunit_to_id, quality
28
30
  from ..uncertainty import ModelUncertainty, SensorNoise
29
31
  from .events import SystemEvent, Leakage, ActuatorEvent, SensorFault, SensorReadingAttack, \
30
32
  SensorReadingEvent
31
- from .scada import ScadaData, AdvancedControlModule
33
+ from .scada import ScadaData, CustomControlModule, SimpleControlModule, ComplexControlModule, \
34
+ RuleCondition, RuleAction, ActuatorConstants, EN_R_ACTION_SETTING
32
35
  from ..topology import NetworkTopology, UNITS_SIMETRIC, UNITS_USCUSTOM
33
36
  from ..utils import get_temp_folder
34
37
 
@@ -67,8 +70,12 @@ class ScenarioSimulator():
67
70
  Sensor noise.
68
71
  _sensor_config : :class:`~epyt_flow.simulation.sensor_config.SensorConfig`, protected
69
72
  Sensor configuration.
70
- _controls : list[:class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`], protected
73
+ _custom_controls : list[:class:`~epyt_flow.simulation.scada.custom_control.CustomControlModule`], protected
71
74
  List of custom control modules.
75
+ _simple_controls : list[:class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`], protected
76
+ List of simle EPANET control rules.
77
+ _complex_controls : list[:class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`], protected
78
+ List of complex (IF-THEN-ELSE) EPANET control rules.
72
79
  _system_events : list[:class:`~epyt_flow.simulation.events.system_event.SystemEvent`], protected
73
80
  Lsit of system events such as leakages.
74
81
  _sensor_reading_events : list[:class:`~epyt_flow.simulation.events.sensor_reading_event.SensorReadingEvent`], protected
@@ -105,7 +112,10 @@ class ScenarioSimulator():
105
112
  self._model_uncertainty = ModelUncertainty()
106
113
  self._sensor_noise = None
107
114
  self._sensor_config = None
108
- self._controls = []
115
+ self._advanced_controls = []
116
+ self._custom_controls = []
117
+ self._simple_controls = []
118
+ self._complex_controls = []
109
119
  self._system_events = []
110
120
  self._sensor_reading_events = []
111
121
  self.__running_simulation = False
@@ -168,6 +178,9 @@ class ScenarioSimulator():
168
178
  if self.__f_msx_in is not None:
169
179
  self.epanet_api.loadMSXFile(my_f_msx_in, customMSXlib=custom_epanetmsx_lib)
170
180
 
181
+ self._simple_controls = self._parse_simple_control_rules()
182
+ self._complex_controls = self._parse_complex_control_rules()
183
+
171
184
  self._sensor_config = self._get_empty_sensor_config()
172
185
  if scenario_config is not None:
173
186
  if scenario_config.general_params is not None:
@@ -177,8 +190,15 @@ class ScenarioSimulator():
177
190
  self._sensor_noise = scenario_config.sensor_noise
178
191
  self._sensor_config = scenario_config.sensor_config
179
192
 
180
- for control in scenario_config.controls:
181
- self.add_control(control)
193
+ if scenario_config.advanced_controls is not None:
194
+ for control in scenario_config.advanced_controls:
195
+ self.add_advanced_control(control)
196
+ for control in scenario_config.custom_controls:
197
+ self.add_custom_control(control)
198
+ for control in scenario_config.simple_controls:
199
+ self.add_simple_control(control)
200
+ for control in scenario_config.complex_controls:
201
+ self.add_complex_control(control)
182
202
  for event in scenario_config.system_events:
183
203
  self.add_system_event(event)
184
204
  for event in scenario_config.sensor_reading_events:
@@ -323,18 +343,62 @@ class ScenarioSimulator():
323
343
  self._sensor_config = sensor_config
324
344
 
325
345
  @property
326
- def controls(self) -> list[AdvancedControlModule]:
346
+ def advanced_controls(self) -> list:
327
347
  """
328
- Gets all control modules.
348
+ Returns all advanced control modules.
329
349
 
330
350
  Returns
331
351
  -------
332
352
  list[:class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`]
333
- All control modules.
353
+ All advanced control modules.
354
+ """
355
+ warnings.warn("'AdvancedControlModule' is deprecated and will be removed in a " +
356
+ "future release -- use 'CustomControlModule' instead")
357
+ self._adapt_to_network_changes()
358
+
359
+ return deepcopy(self._advanced_controls)
360
+
361
+ @property
362
+ def custom_controls(self) -> list[CustomControlModule]:
363
+ """
364
+ Returns all custom control modules.
365
+
366
+ Returns
367
+ -------
368
+ list[:class:`~epyt_flow.simulation.scada.custom_control.CustomControlModule`]
369
+ All custom control modules.
370
+ """
371
+ self._adapt_to_network_changes()
372
+
373
+ return deepcopy(self._custom_controls)
374
+
375
+ @property
376
+ def simple_controls(self) -> list[SimpleControlModule]:
377
+ """
378
+ Gets all simple EPANET control rules.
379
+
380
+ Returns
381
+ -------
382
+ list[:class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`]
383
+ All simple EPANET control rules.
334
384
  """
335
385
  self._adapt_to_network_changes()
336
386
 
337
- return deepcopy(self._controls)
387
+ return deepcopy(self._simple_controls)
388
+
389
+ @property
390
+ def complex_controls(self) -> list[SimpleControlModule]:
391
+ """
392
+ Gets all complex (IF-THEN-ELSE) EPANET control rules.
393
+
394
+ Returns
395
+ -------
396
+ list[:class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`]
397
+ All complex EPANET control rules.
398
+ """
399
+ self._adapt_to_network_changes()
400
+
401
+ return deepcopy(self._complex_controls)
338
402
 
339
403
  @property
340
404
  def leakages(self) -> list[Leakage]:
@@ -422,6 +486,132 @@ class ScenarioSimulator():
422
486
 
423
487
  return deepcopy(self._sensor_reading_events)
424
488
 
489
+ def _parse_simple_control_rules(self) -> list[SimpleControlModule]:
490
+ controls = []
491
+
492
+ for idx in self.epanet_api.getControls():
493
+ control = self.epanet_api.getControls(idx)
494
+
495
+ if control.Setting == "OPEN":
496
+ link_status = ActuatorConstants.EN_OPEN
497
+ else:
498
+ link_status = ActuatorConstants.EN_CLOSED
499
+
500
+ if control.Type == "LOWLEVEL":
501
+ cond_type = ToolkitConstants.EN_LOWLEVEL
502
+ elif control.Type == "HIGHLEVEL":
503
+ cond_type = ToolkitConstants.EN_HILEVEL
504
+ elif control.Type == "TIMER":
505
+ cond_type = ToolkitConstants.EN_TIMER
506
+ elif control.Type == "TIMEOFDAY":
507
+ cond_type = ToolkitConstants.EN_TIMEOFDAY
508
+
509
+ if control.NodeID is not None:
510
+ cond_var_value = control.NodeID
511
+ cond_comp_value = control.Value
512
+ else:
513
+ if cond_type == ToolkitConstants.EN_TIMER:
514
+ cond_var_value = int(control.Value / 3600)
515
+ elif cond_type == ToolkitConstants.EN_TIMEOFDAY:
516
+ sec = control.Value
517
+ if sec <= 43200:
518
+ cond_var_value = \
519
+ f"{':'.join(str(timedelta(seconds=sec)).split(':')[:2])} AM"
520
+ else:
521
+ sec -= 43200
522
+ cond_var_value = \
523
+ f"{':'.join(str(timedelta(seconds=sec)).split(':')[:2])} PM"
524
+ cond_comp_value = None
525
+
526
+ controls.append(SimpleControlModule(link_id=control.LinkID,
527
+ link_status=link_status,
528
+ cond_type=cond_type,
529
+ cond_var_value=cond_var_value,
530
+ cond_comp_value=cond_comp_value))
531
+
532
+ return controls
533
+
534
+ def _parse_complex_control_rules(self) -> list[ComplexControlModule]:
535
+ controls = []
536
+
537
+ rules = self.epanet_api.getRules()
538
+ for rule_idx, rule in rules.items():
539
+ rule_info = self.epanet_api.getRuleInfo(rule_idx)
540
+
541
+ rule_id = rule["Rule_ID"]
542
+ rule_priority, *_ = rule_info.Priority
543
+
544
+ # Parse conditions
545
+ n_rule_premises, *_ = rule_info.Premises
546
+
547
+ condition_1 = None
548
+ additional_conditions = []
549
+ for j in range(1, n_rule_premises + 1):
550
+ [logop, object_type_id, obj_idx, variable_type_id, relop, status, value_premise] = \
551
+ self.epanet_api.api.ENgetpremise(rule_idx, j)
552
+
553
+ object_id = None
554
+ if object_type_id == ToolkitConstants.EN_R_NODE:
555
+ object_id = self.epanet_api.getNodeNameID(obj_idx)
556
+ elif object_type_id == ToolkitConstants.EN_R_LINK:
557
+ object_id = self.epanet_api.getLinkNameID(obj_idx)
558
+ elif object_type_id == ToolkitConstants.EN_R_SYSTEM:
559
+ object_id = ""
560
+
561
+ if variable_type_id >= ToolkitConstants.EN_R_TIME:
562
+ value_premise = datetime.fromtimestamp(value_premise)\
563
+ .strftime("%I:%M %p")
564
+ if status != 0:
565
+ value_premise = self.epanet_api.RULESTATUS[status - 1]
566
+
567
+ condition = RuleCondition(object_type_id, object_id, variable_type_id,
568
+ relop, value_premise)
569
+ if condition_1 is None:
570
+ condition_1 = condition
571
+ else:
572
+ additional_conditions.append((logop, condition))
573
+
574
+ # Parse actions
575
+ n_rule_then_actions, *_ = rule_info.ThenActions
576
+ actions = []
577
+ for j in range(1, n_rule_then_actions + 1):
578
+ [link_idx, link_status, link_setting] = \
579
+ self.epanet_api.api.ENgetthenaction(rule_idx, j)
580
+
581
+ link_type_id = self.epanet_api.getLinkTypeIndex(link_idx)
582
+ link_id = self.epanet_api.getLinkNameID(link_idx)
583
+ if link_status >= 0:
584
+ action_type_id = link_status
585
+ action_value = link_status
586
+ else:
587
+ action_type_id = EN_R_ACTION_SETTING
588
+ action_value = link_setting
589
+
590
+ actions.append(RuleAction(link_type_id, link_id, action_type_id, action_value))
591
+
592
+ n_rule_else_actions, *_ = rule_info.ElseActions
593
+ else_actions = []
594
+ for j in range(1, n_rule_else_actions + 1):
595
+ [link_idx, link_status, link_setting] = \
596
+ self.epanet_api.api.ENgetelseaction(rule_idx, j)
597
+
598
+ link_type_id = self.epanet_api.getLinkType(link_idx)
599
+ link_id = self.epanet_api.getLinkNameID(link_idx)
600
+ if link_status <= 3:
601
+ action_type_id = link_status
602
+ action_value = link_status
603
+ else:
604
+ action_type_id = EN_R_ACTION_SETTING
605
+ action_value = link_setting
606
+
607
+ else_actions.append(RuleAction(link_type_id, link_id, action_type_id, action_value))
608
+
609
+ # Create and add control module
610
+ controls.append(ComplexControlModule(rule_id, condition_1, additional_conditions,
611
+ actions, else_actions, int(rule_priority)))
612
+
613
+ return controls
614
+
425
615
  def _adapt_to_network_changes(self):
426
616
  nodes = self.epanet_api.getNodeNameID()
427
617
  links = self.epanet_api.getLinkNameID()
@@ -800,7 +990,11 @@ class ScenarioSimulator():
800
990
  return ScenarioConfig(f_inp_in=self.__f_inp_in, f_msx_in=self.__f_msx_in,
801
991
  general_params=general_params, sensor_config=self.sensor_config,
802
992
  memory_consumption_estimate=self.estimate_memory_consumption(),
803
- controls=self.controls, sensor_noise=self.sensor_noise,
993
+ advanced_controls=None if len(self._advanced_controls) == 0 else self.advanced_controls,
994
+ custom_controls=self.custom_controls,
995
+ simple_controls=self.simple_controls,
996
+ complex_controls=self.complex_controls,
997
+ sensor_noise=self.sensor_noise,
804
998
  model_uncertainty=self.model_uncertainty,
805
999
  system_events=self.system_events,
806
1000
  sensor_reading_events=self.sensor_reading_events)
@@ -1063,23 +1257,147 @@ class ScenarioSimulator():
1063
1257
  self.epanet_api.setNodeJunctionData(node_idx, self.epanet_api.getNodeElevations(node_idx),
1064
1258
  base_demand, demand_pattern_id)
1065
1259
 
1066
- def add_control(self, control: AdvancedControlModule) -> None:
1260
+ def add_advanced_control(self, control) -> None:
1067
1261
  """
1068
- Adds a control module to the scenario simulation.
1262
+ Adds an advanced control module to the scenario simulation.
1069
1263
 
1070
1264
  Parameters
1071
1265
  ----------
1072
1266
  control : :class:`~epyt_flow.simulation.scada.advanced_control.AdvancedControlModule`
1073
- Control module.
1267
+ Advanced control module.
1074
1268
  """
1075
1269
  self._adapt_to_network_changes()
1076
1270
 
1271
+ from .scada.advanced_control import AdvancedControlModule
1077
1272
  if not isinstance(control, AdvancedControlModule):
1078
1273
  raise TypeError("'control' must be an instance of " +
1079
1274
  "'epyt_flow.simulation.scada.AdvancedControlModule' not of " +
1080
1275
  f"'{type(control)}'")
1081
1276
 
1082
- self._controls.append(control)
1277
+ self._advanced_controls.append(control)
1278
+
1279
+ def add_custom_control(self, control: CustomControlModule) -> None:
1280
+ """
1281
+ Adds a custom control module to the scenario simulation.
1282
+
1283
+ Parameters
1284
+ ----------
1285
+ control : :class:`~epyt_flow.simulation.scada.custom_control.CustomControlModule`
1286
+ Custom control module.
1287
+ """
1288
+ self._adapt_to_network_changes()
1289
+
1290
+ if not isinstance(control, CustomControlModule):
1291
+ raise TypeError("'control' must be an instance of " +
1292
+ "'epyt_flow.simulation.scada.CustomControlModule' not of " +
1293
+ f"'{type(control)}'")
1294
+
1295
+ self._custom_controls.append(control)
1296
+
1297
+ def add_simple_control(self, control: SimpleControlModule) -> None:
1298
+ """
1299
+ Adds a simple EPANET control rule to the scenario simulation.
1300
+
1301
+ Parameters
1302
+ ----------
1303
+ control : :class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`
1304
+ Simple EPANET control module.
1305
+ """
1306
+ self._adapt_to_network_changes()
1307
+
1308
+ if not isinstance(control, SimpleControlModule):
1309
+ raise TypeError("'control' must be an instance of " +
1310
+ "'epyt_flow.simulation.scada.SimpleControlModule' not of " +
1311
+ f"'{type(control)}'")
1312
+
1313
+ if not any(c == control for c in self._simple_controls):
1314
+ self._simple_controls.append(control)
1315
+ self.epanet_api.addControls(str(control))
1316
+
1317
+ def remove_all_simple_controls(self) -> None:
1318
+ """
1319
+ Removes all simple EPANET controls from the scenario.
1320
+ """
1321
+ self.epanet_api.deleteControls()
1322
+ self._simple_controls = []
1323
+
1324
+ def remove_simple_control(self, control: SimpleControlModule) -> None:
1325
+ """
1326
+ Removes a given simple EPANET control rule from the scenario.
1327
+
1328
+ Parameters
1329
+ ----------
1330
+ control : :class:`~epyt_flow.simulation.scada.simple_control.SimpleControlModule`
1331
+ Simple EPANET control module to be removed.
1332
+ """
1333
+ self._adapt_to_network_changes()
1334
+
1335
+ if not isinstance(control, SimpleControlModule):
1336
+ raise TypeError("'control' must be an instance of " +
1337
+ "'epyt_flow.simulation.scada.SimpleControlModule' not of " +
1338
+ f"'{type(control)}'")
1339
+
1340
+ control_idx = None
1341
+ for idx, c in enumerate(self._simple_controls):
1342
+ if c == control:
1343
+ control_idx = idx + 1
1344
+ break
1345
+ if control_idx is None:
1346
+ raise ValueError("Invalid/Unknown control module.")
1347
+
1348
+ self.epanet_api.deleteControls(control_idx)
1349
+ self._simple_controls.remove(control)
1350
+
1351
+ def add_complex_control(self, control: ComplexControlModule) -> None:
1352
+ """
1353
+ Adds an complex (IF-THEN-ELSE) EPANET control rule to the scenario simulation.
1354
+
1355
+ Parameters
1356
+ ----------
1357
+ control : :class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`
1358
+ Complex EPANET control module.
1359
+ """
1360
+ self._adapt_to_network_changes()
1361
+
1362
+ if not isinstance(control, ComplexControlModule):
1363
+ raise TypeError("'control' must be an instance of " +
1364
+ "'epyt_flow.simulation.scada.ComplexControlModule' not of " +
1365
+ f"'{type(control)}'")
1366
+
1367
+ if not any(c == control for c in self._complex_controls):
1368
+ self._complex_controls.append(control)
1369
+ self.epanet_api.addRules(str(control))
1370
+
1371
+ def remove_all_complex_controls(self) -> None:
1372
+ """
1373
+ Removes all complex EPANET controls from the scenario.
1374
+ """
1375
+ self.epanet_api.deleteRules()
1376
+ self._complex_controls = []
1377
+
1378
+ def remove_complex_control(self, control: ComplexControlModule) -> None:
1379
+ """
1380
+ Removes a given complex (IF-THEN-ELSE) EPANET control rule from the scenario.
1381
+
1382
+ Parameters
1383
+ ----------
1384
+ control : :class:`~epyt_flow.simulation.scada.complex_control.ComplexControlModule`
1385
+ Complex EPANET control module to be removed.
1386
+ """
1387
+ self._adapt_to_network_changes()
1388
+
1389
+ if not isinstance(control, ComplexControlModule):
1390
+ raise TypeError("'control' must be an instance of " +
1391
+ "'epyt_flow.simulation.scada.ComplexControlModule' not of " +
1392
+ f"'{type(control)}'")
1393
+
1394
+ if control.rule_id not in self.epanet_api.getRuleID():
1395
+ raise ValueError("Invalid/Unknown control module. " +
1396
+ f"Can not find rule ID '{control.rule_id}'")
1397
+
1398
+ rule_idx = self.epanet_api.getRuleID().index(control.rule_id) + 1
1399
+ self.epanet_api.deleteRules(rule_idx)
1400
+ self._complex_controls.remove(control)
1083
1401
 
1084
1402
  def add_leakage(self, leakage_event: Leakage) -> None:
1085
1403
  """
@@ -1616,12 +1934,17 @@ class ScenarioSimulator():
1616
1934
  for event in self._system_events:
1617
1935
  event.reset()
1618
1936
 
1619
- if self._controls is not None:
1620
- for c in self._controls:
1937
+ if self._advanced_controls is not None:
1938
+ for c in self._advanced_controls:
1621
1939
  c.init(self.epanet_api)
1940
+ if self._custom_controls is not None:
1941
+ for control in self._custom_controls:
1942
+ control.init(self.epanet_api)
1622
1943
 
1623
1944
  def run_advanced_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
1624
- frozen_sensor_config: bool = False) -> ScadaData:
1945
+ frozen_sensor_config: bool = False,
1946
+ use_quality_time_step_as_reporting_time_step: bool = False
1947
+ ) -> ScadaData:
1625
1948
  """
1626
1949
  Runs an advanced quality analysis using EPANET-MSX.
1627
1950
 
@@ -1638,6 +1961,13 @@ class ScenarioSimulator():
1638
1961
  If True, the sensor config can not be changed and only the required sensor nodes/links
1639
1962
  will be stored -- this usually leads to a significant reduction in memory consumption.
1640
1963
 
1964
+ The default is False.
1965
+ use_quality_time_step_as_reporting_time_step : `bool`, optional
1966
+ If True, the water quality time step will be used as the reporting time step.
1967
+
1968
+ As a consequence, the simualtion results can not be merged
1969
+ with the hydraulic simulation.
1970
+
1641
1971
  The default is False.
1642
1972
 
1643
1973
  Returns
@@ -1657,7 +1987,9 @@ class ScenarioSimulator():
1657
1987
  for scada_data in gen(hyd_file_in=hyd_file_in,
1658
1988
  verbose=verbose,
1659
1989
  return_as_dict=True,
1660
- frozen_sensor_config=frozen_sensor_config):
1990
+ frozen_sensor_config=frozen_sensor_config,
1991
+ use_quality_time_step_as_reporting_time_step=
1992
+ use_quality_time_step_as_reporting_time_step):
1661
1993
  if result is None:
1662
1994
  result = {}
1663
1995
  for data_type, data in scada_data.items():
@@ -1682,7 +2014,8 @@ class ScenarioSimulator():
1682
2014
  def run_advanced_quality_simulation_as_generator(self, hyd_file_in: str, verbose: bool = False,
1683
2015
  support_abort: bool = False,
1684
2016
  return_as_dict: bool = False,
1685
- frozen_sensor_config: bool = False
2017
+ frozen_sensor_config: bool = False,
2018
+ use_quality_time_step_as_reporting_time_step: bool = False,
1686
2019
  ) -> Generator[Union[ScadaData, dict], bool, None]:
1687
2020
  """
1688
2021
  Runs an advanced quality analysis using EPANET-MSX.
@@ -1704,6 +2037,13 @@ class ScenarioSimulator():
1704
2037
  If True, the sensor config can not be changed and only the required sensor nodes/links
1705
2038
  will be stored -- this usually leads to a significant reduction in memory consumption.
1706
2039
 
2040
+ The default is False.
2041
+ use_quality_time_step_as_reporting_time_step : `bool`, optional
2042
+ If True, the water quality time step will be used as the reporting time step.
2043
+
2044
+ As a consequence, the simualtion results can not be merged
2045
+ with the hydraulic simulation.
2046
+
1707
2047
  The default is False.
1708
2048
 
1709
2049
  Returns
@@ -1729,6 +2069,11 @@ class ScenarioSimulator():
1729
2069
  reporting_time_step = self.epanet_api.getTimeReportingStep()
1730
2070
  hyd_time_step = self.epanet_api.getTimeHydraulicStep()
1731
2071
 
2072
+ if use_quality_time_step_as_reporting_time_step is True:
2073
+ quality_time_step = self.epanet_api.getMSXTimeStep()
2074
+ reporting_time_step = quality_time_step
2075
+ hyd_time_step = quality_time_step
2076
+
1732
2077
  self.epanet_api.initializeMSXQualityAnalysis(ToolkitConstants.EN_NOSAVE)
1733
2078
 
1734
2079
  self.__running_simulation = True
@@ -1880,7 +2225,9 @@ class ScenarioSimulator():
1880
2225
  self.__running_simulation = False
1881
2226
 
1882
2227
  def run_basic_quality_simulation(self, hyd_file_in: str, verbose: bool = False,
1883
- frozen_sensor_config: bool = False) -> ScadaData:
2228
+ frozen_sensor_config: bool = False,
2229
+ use_quality_time_step_as_reporting_time_step: bool = False
2230
+ ) -> ScadaData:
1884
2231
  """
1885
2232
  Runs a basic quality analysis using EPANET.
1886
2233
 
@@ -1897,6 +2244,13 @@ class ScenarioSimulator():
1897
2244
  If True, the sensor config can not be changed and only the required sensor nodes/links
1898
2245
  will be stored -- this usually leads to a significant reduction in memory consumption.
1899
2246
 
2247
+ The default is False.
2248
+ use_quality_time_step_as_reporting_time_step : `bool`, optional
2249
+ If True, the water quality time step will be used as the reporting time step.
2250
+
2251
+ As a consequence, the simualtion results can not be merged
2252
+ with the hydraulic simulation.
2253
+
1900
2254
  The default is False.
1901
2255
 
1902
2256
  Returns
@@ -1914,7 +2268,9 @@ class ScenarioSimulator():
1914
2268
  for scada_data in gen(hyd_file_in=hyd_file_in,
1915
2269
  verbose=verbose,
1916
2270
  return_as_dict=True,
1917
- frozen_sensor_config=frozen_sensor_config):
2271
+ frozen_sensor_config=frozen_sensor_config,
2272
+ use_quality_time_step_as_reporting_time_step=
2273
+ use_quality_time_step_as_reporting_time_step):
1918
2274
  if result is None:
1919
2275
  result = {}
1920
2276
  for data_type, data in scada_data.items():
@@ -1937,6 +2293,7 @@ class ScenarioSimulator():
1937
2293
  support_abort: bool = False,
1938
2294
  return_as_dict: bool = False,
1939
2295
  frozen_sensor_config: bool = False,
2296
+ use_quality_time_step_as_reporting_time_step: bool = False
1940
2297
  ) -> Generator[Union[ScadaData, dict], bool, None]:
1941
2298
  """
1942
2299
  Runs a basic quality analysis using EPANET.
@@ -1960,6 +2317,13 @@ class ScenarioSimulator():
1960
2317
  If True, the sensor config can not be changed and only the required sensor nodes/links
1961
2318
  will be stored -- this usually leads to a significant reduction in memory consumption.
1962
2319
 
2320
+ The default is False.
2321
+ use_quality_time_step_as_reporting_time_step : `bool`, optional
2322
+ If True, the water quality time step will be used as the reporting time step.
2323
+
2324
+ As a consequence, the simualtion results can not be merged
2325
+ with the hydraulic simulation.
2326
+
1963
2327
  The default is False.
1964
2328
 
1965
2329
  Returns
@@ -1974,6 +2338,11 @@ class ScenarioSimulator():
1974
2338
  reporting_time_start = self.epanet_api.getTimeReportingStart()
1975
2339
  reporting_time_step = self.epanet_api.getTimeReportingStep()
1976
2340
 
2341
+ if use_quality_time_step_as_reporting_time_step is True:
2342
+ quality_time_step = self.epanet_api.getTimeQualityStep()
2343
+ requested_time_step = quality_time_step
2344
+ reporting_time_step = quality_time_step
2345
+
1977
2346
  self.epanet_api.useHydraulicFile(hyd_file_in)
1978
2347
 
1979
2348
  self.epanet_api.openQualityAnalysis()
@@ -2032,7 +2401,7 @@ class ScenarioSimulator():
2032
2401
  yield data
2033
2402
 
2034
2403
  # Next
2035
- tstep = self.epanet_api.nextQualityAnalysisStep()
2404
+ tstep = self.epanet_api.stepQualityAnalysisTimeLeft()
2036
2405
 
2037
2406
  self.epanet_api.closeHydraulicAnalysis()
2038
2407
 
@@ -2254,7 +2623,9 @@ class ScenarioSimulator():
2254
2623
  yield data
2255
2624
 
2256
2625
  # Apply control modules
2257
- for control in self._controls:
2626
+ for control in self._advanced_controls:
2627
+ control.step(scada_data)
2628
+ for control in self._custom_controls:
2258
2629
  control.step(scada_data)
2259
2630
 
2260
2631
  # Next
@@ -1276,6 +1276,22 @@ class SensorConfig(JsonSerializable):
1276
1276
  """
1277
1277
  return self.__links.copy()
1278
1278
 
1279
+ @property
1280
+ def junctions(self) -> list[str]:
1281
+ """
1282
+ Returns all junction IDs.
1283
+
1284
+ Returns
1285
+ -------
1286
+ `list[str]`
1287
+ All juncitons IDs.
1288
+ """
1289
+ junctions = self.nodes
1290
+ for tank_id in self.tanks:
1291
+ junctions.remove(tank_id)
1292
+
1293
+ return junctions
1294
+
1279
1295
  @property
1280
1296
  def valves(self) -> list[str]:
1281
1297
  """
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2024 EPyT-Flow Developers
3
+ Copyright (c) EPyT-Flow Developers
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.2
2
2
  Name: epyt-flow
3
- Version: 0.9.0
3
+ Version: 0.10.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
@@ -17,10 +17,11 @@ Classifier: Programming Language :: Python :: 3.9
17
17
  Classifier: Programming Language :: Python :: 3.10
18
18
  Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
20
21
  Requires-Python: >=3.9
21
22
  Description-Content-Type: text/markdown
22
23
  License-File: LICENSE
23
- Requires-Dist: epyt>=1.2.0
24
+ Requires-Dist: epyt>=1.2.1
24
25
  Requires-Dist: requests>=2.31.0
25
26
  Requires-Dist: scipy>=1.11.4
26
27
  Requires-Dist: u-msgpack-python>=2.8.0
@@ -66,8 +67,8 @@ Unique features of EPyT-Flow that make it superior to other (Python) toolboxes a
66
67
  - High- and low-level interface
67
68
  - Object-orientated design that is easy to extend and customize
68
69
  - Sensor configurations
69
- - Wide variety of pre-defined events (e.g. leakages, sensor faults, actuator events, cyber-attacks, etc.)
70
- - Wide variety of pre-defined types of uncertainties (e.g. model uncertainties)
70
+ - Wide variety of pre-defined events (e.g. leakages, sensor faults, actuator events, contamination, cyber-attacks, etc.)
71
+ - Wide variety of pre-defined types of global & local uncertainties (e.g. model uncertainties)
71
72
  - Step-wise simulation and environment for training and evaluating control strategies
72
73
  - Serialization module for easy exchange of data and (scenario) configurations
73
74
  - REST API to make EPyT-Flow accessible in other applications
@@ -76,7 +77,7 @@ Unique features of EPyT-Flow that make it superior to other (Python) toolboxes a
76
77
 
77
78
  ## Installation
78
79
 
79
- EPyT-Flow supports Python 3.9 - 3.12
80
+ EPyT-Flow supports Python 3.9 - 3.13
80
81
 
81
82
  Note that [EPANET and EPANET-MSX sources](epyt_flow/EPANET/) are compiled and overwrite the binaries
82
83
  shipped by EPyT **IF** EPyT-Flow is installed on a Unix system and the *gcc* compiler is available.