floodmodeller-api 0.5.0.post1__py3-none-any.whl → 0.5.2__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 (119) hide show
  1. floodmodeller_api/__init__.py +11 -1
  2. floodmodeller_api/_base.py +55 -36
  3. floodmodeller_api/backup.py +15 -12
  4. floodmodeller_api/dat.py +191 -121
  5. floodmodeller_api/diff.py +4 -4
  6. floodmodeller_api/hydrology_plus/hydrology_plus_export.py +15 -14
  7. floodmodeller_api/ied.py +8 -10
  8. floodmodeller_api/ief.py +56 -42
  9. floodmodeller_api/ief_flags.py +1 -1
  10. floodmodeller_api/inp.py +7 -10
  11. floodmodeller_api/logs/lf.py +25 -26
  12. floodmodeller_api/logs/lf_helpers.py +20 -20
  13. floodmodeller_api/logs/lf_params.py +1 -5
  14. floodmodeller_api/mapping.py +11 -2
  15. floodmodeller_api/test/__init__.py +2 -2
  16. floodmodeller_api/test/conftest.py +2 -3
  17. floodmodeller_api/test/test_backup.py +2 -2
  18. floodmodeller_api/test/test_conveyance.py +13 -7
  19. floodmodeller_api/test/test_dat.py +168 -20
  20. floodmodeller_api/test/test_data/EX18_DAT_expected.json +164 -144
  21. floodmodeller_api/test/test_data/EX3_DAT_expected.json +6 -2
  22. floodmodeller_api/test/test_data/EX6_DAT_expected.json +12 -46
  23. floodmodeller_api/test/test_data/encoding_test_cp1252.dat +1081 -0
  24. floodmodeller_api/test/test_data/encoding_test_utf8.dat +1081 -0
  25. floodmodeller_api/test/test_data/integrated_bridge/AR_NoSP_NoBl_2O_NO_OneFRC.ied +33 -0
  26. floodmodeller_api/test/test_data/integrated_bridge/AR_vSP_25pc_1O.ied +32 -0
  27. floodmodeller_api/test/test_data/integrated_bridge/PL_vSP_25pc_1O.ied +34 -0
  28. floodmodeller_api/test/test_data/integrated_bridge/SBTwoFRCsStaggered.IED +32 -0
  29. floodmodeller_api/test/test_data/integrated_bridge/US_NoSP_NoBl_OR_RN.ied +28 -0
  30. floodmodeller_api/test/test_data/integrated_bridge/US_SP_NoBl_OR_frc_PT2-5_RN.ied +34 -0
  31. floodmodeller_api/test/test_data/integrated_bridge/US_fSP_NoBl_1O.ied +30 -0
  32. floodmodeller_api/test/test_data/integrated_bridge/US_nSP_NoBl_1O.ied +49 -0
  33. floodmodeller_api/test/test_data/integrated_bridge/US_vSP_NoBl_2O_Para.ied +35 -0
  34. floodmodeller_api/test/test_data/integrated_bridge.dat +40 -0
  35. floodmodeller_api/test/test_data/network.ied +2 -2
  36. floodmodeller_api/test/test_data/network_dat_expected.json +141 -243
  37. floodmodeller_api/test/test_data/network_ied_expected.json +2 -2
  38. floodmodeller_api/test/test_data/network_with_comments.ied +2 -2
  39. floodmodeller_api/test/test_data/structure_logs/EX17_expected.csv +4 -0
  40. floodmodeller_api/test/test_data/structure_logs/EX17_expected.json +69 -0
  41. floodmodeller_api/test/test_data/structure_logs/EX18_expected.csv +20 -0
  42. floodmodeller_api/test/test_data/structure_logs/EX18_expected.json +292 -0
  43. floodmodeller_api/test/test_data/structure_logs/EX6_expected.csv +4 -0
  44. floodmodeller_api/test/test_data/structure_logs/EX6_expected.json +35 -0
  45. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_flow.csv +182 -0
  46. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_fr.csv +182 -0
  47. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_mode.csv +182 -0
  48. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_stage.csv +182 -0
  49. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_state.csv +182 -0
  50. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_velocity.csv +182 -0
  51. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_left_fp_h.csv +182 -0
  52. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_left_fp_mode.csv +182 -0
  53. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_link_inflow.csv +182 -0
  54. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_max.csv +87 -0
  55. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_right_fp_h.csv +182 -0
  56. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_right_fp_mode.csv +182 -0
  57. floodmodeller_api/test/test_flowtimeprofile.py +2 -2
  58. floodmodeller_api/test/test_hydrology_plus_export.py +4 -2
  59. floodmodeller_api/test/test_ied.py +3 -3
  60. floodmodeller_api/test/test_ief.py +12 -4
  61. floodmodeller_api/test/test_inp.py +2 -2
  62. floodmodeller_api/test/test_integrated_bridge.py +159 -0
  63. floodmodeller_api/test/test_json.py +14 -13
  64. floodmodeller_api/test/test_logs_lf.py +50 -29
  65. floodmodeller_api/test/test_read_file.py +1 -0
  66. floodmodeller_api/test/test_river.py +12 -12
  67. floodmodeller_api/test/test_tool.py +8 -5
  68. floodmodeller_api/test/test_toolbox_structure_log.py +148 -158
  69. floodmodeller_api/test/test_xml2d.py +14 -16
  70. floodmodeller_api/test/test_zz.py +143 -0
  71. floodmodeller_api/to_from_json.py +9 -9
  72. floodmodeller_api/tool.py +15 -11
  73. floodmodeller_api/toolbox/example_tool.py +5 -1
  74. floodmodeller_api/toolbox/model_build/add_siltation_definition.py +13 -9
  75. floodmodeller_api/toolbox/model_build/structure_log/structure_log.py +500 -194
  76. floodmodeller_api/toolbox/model_build/structure_log_definition.py +5 -1
  77. floodmodeller_api/units/__init__.py +15 -0
  78. floodmodeller_api/units/_base.py +87 -20
  79. floodmodeller_api/units/_helpers.py +343 -0
  80. floodmodeller_api/units/boundaries.py +59 -71
  81. floodmodeller_api/units/comment.py +1 -1
  82. floodmodeller_api/units/conduits.py +57 -54
  83. floodmodeller_api/units/connectors.py +112 -0
  84. floodmodeller_api/units/controls.py +107 -0
  85. floodmodeller_api/units/conveyance.py +1 -1
  86. floodmodeller_api/units/iic.py +2 -9
  87. floodmodeller_api/units/losses.py +44 -45
  88. floodmodeller_api/units/sections.py +52 -51
  89. floodmodeller_api/units/structures.py +361 -531
  90. floodmodeller_api/units/units.py +27 -26
  91. floodmodeller_api/units/unsupported.py +5 -7
  92. floodmodeller_api/units/variables.py +2 -2
  93. floodmodeller_api/urban1d/_base.py +13 -17
  94. floodmodeller_api/urban1d/conduits.py +11 -21
  95. floodmodeller_api/urban1d/general_parameters.py +1 -1
  96. floodmodeller_api/urban1d/junctions.py +7 -11
  97. floodmodeller_api/urban1d/losses.py +13 -17
  98. floodmodeller_api/urban1d/outfalls.py +18 -22
  99. floodmodeller_api/urban1d/raingauges.py +5 -10
  100. floodmodeller_api/urban1d/subsections.py +5 -4
  101. floodmodeller_api/urban1d/xsections.py +14 -17
  102. floodmodeller_api/util.py +23 -6
  103. floodmodeller_api/validation/parameters.py +7 -3
  104. floodmodeller_api/validation/urban_parameters.py +1 -4
  105. floodmodeller_api/validation/validation.py +11 -5
  106. floodmodeller_api/version.py +1 -1
  107. floodmodeller_api/xml2d.py +27 -31
  108. floodmodeller_api/xml2d_template.py +1 -1
  109. floodmodeller_api/zz.py +539 -0
  110. {floodmodeller_api-0.5.0.post1.dist-info → floodmodeller_api-0.5.2.dist-info}/LICENSE.txt +1 -1
  111. {floodmodeller_api-0.5.0.post1.dist-info → floodmodeller_api-0.5.2.dist-info}/METADATA +30 -16
  112. {floodmodeller_api-0.5.0.post1.dist-info → floodmodeller_api-0.5.2.dist-info}/RECORD +116 -83
  113. {floodmodeller_api-0.5.0.post1.dist-info → floodmodeller_api-0.5.2.dist-info}/WHEEL +1 -1
  114. floodmodeller_api/test/test_zzn.py +0 -36
  115. floodmodeller_api/units/helpers.py +0 -123
  116. floodmodeller_api/zzn.py +0 -414
  117. /floodmodeller_api/test/test_data/{network_from_tabularCSV.csv → tabular_csv_outputs/network_zzn_max.csv} +0 -0
  118. {floodmodeller_api-0.5.0.post1.dist-info → floodmodeller_api-0.5.2.dist-info}/entry_points.txt +0 -0
  119. {floodmodeller_api-0.5.0.post1.dist-info → floodmodeller_api-0.5.2.dist-info}/top_level.txt +0 -0
floodmodeller_api/ief.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Flood Modeller Python API
3
- Copyright (C) 2024 Jacobs U.K. Limited
3
+ Copyright (C) 2025 Jacobs U.K. Limited
4
4
 
5
5
  This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
6
6
  as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@@ -17,7 +17,7 @@ address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London
17
17
  from __future__ import annotations
18
18
 
19
19
  import csv
20
- import os
20
+ import logging
21
21
  import subprocess
22
22
  import time
23
23
  from io import StringIO
@@ -33,19 +33,28 @@ from .diff import check_item_with_dataframe_equal
33
33
  from .ief_flags import flags
34
34
  from .logs import LF1, create_lf
35
35
  from .to_from_json import Jsonable
36
- from .util import handle_exception
37
- from .zzn import ZZN
36
+ from .util import handle_exception, is_windows
37
+ from .zz import ZZN
38
38
 
39
39
 
40
- def try_numeric(value: str) -> str | int | float:
40
+ def try_converting(value: str) -> str | int | float:
41
41
  """Attempt to parse value as float or int if valid, else return the original string"""
42
42
  try:
43
43
  return int(value)
44
44
  except ValueError:
45
- try:
46
- return float(value)
47
- except ValueError:
48
- return value
45
+ pass
46
+
47
+ try:
48
+ return float(value)
49
+ except ValueError:
50
+ pass
51
+
52
+ if not is_windows() and "\\" in value:
53
+ # backslashes aren't valid in paths
54
+ logging.info("Changing '\\' in '%s' to '/' to be valid in Linux.", value)
55
+ return value.replace("\\", "/")
56
+
57
+ return value
49
58
 
50
59
 
51
60
  class IEF(FMFile):
@@ -75,14 +84,14 @@ class IEF(FMFile):
75
84
  if ief_filepath is not None:
76
85
  FMFile.__init__(self, ief_filepath)
77
86
  self._read()
78
- self._log_path = self._get_result_filepath("lf1")
87
+ self._log_path = self._filepath.with_suffix(".lf1")
79
88
  else:
80
89
  self._create_from_blank()
81
90
 
82
91
  def _read(self):
83
92
  # Read IEF data
84
- with open(self._filepath) as ief_file:
85
- raw_data = [line.rstrip("\n") for line in ief_file.readlines()]
93
+ with open(self._filepath, encoding=self.ENCODING) as ief_file:
94
+ raw_data = [line.rstrip("\n") for line in ief_file]
86
95
  # Clean data and add as class properties
87
96
  # Create a list to store the properties which are to be saved in IEF, so as to ignore any temp properties.
88
97
  prev_comment = None
@@ -117,7 +126,7 @@ class IEF(FMFile):
117
126
  self._ief_properties.append(prop)
118
127
  else:
119
128
  # Sets the property and value as class properties so they can be edited.
120
- setattr(self, prop, try_numeric(value))
129
+ setattr(self, prop, try_converting(value))
121
130
  self._ief_properties.append(prop)
122
131
  prev_comment = None
123
132
  else:
@@ -170,7 +179,7 @@ class IEF(FMFile):
170
179
  # Add multiple EventData if present
171
180
  for idx, key in enumerate(event_data):
172
181
  if idx == event_index:
173
- ief_string += f";{key}\nEventData{eq}{str(event_data[key])}\n"
182
+ ief_string += f";{key}\nEventData{eq}{event_data[key]!s}\n"
174
183
  break
175
184
  event_index += 1
176
185
 
@@ -181,7 +190,7 @@ class IEF(FMFile):
181
190
 
182
191
  else:
183
192
  # writes property and value to ief string
184
- ief_string += f"{prop}{eq}{str(getattr(self, prop))}\n"
193
+ ief_string += f"{prop}{eq}{getattr(self, prop)!s}\n"
185
194
 
186
195
  return ief_string
187
196
 
@@ -209,7 +218,7 @@ class IEF(FMFile):
209
218
  if "=" in line:
210
219
  prop, value = line.split("=")
211
220
  # Sets the property and value as class properties so they can be edited.
212
- setattr(self, prop, try_numeric(value))
221
+ setattr(self, prop, try_converting(value))
213
222
  self._ief_properties.append(prop)
214
223
  else:
215
224
  # This should add the [] bound headers
@@ -227,8 +236,9 @@ class IEF(FMFile):
227
236
  ):
228
237
  # Check if valid flag
229
238
  if prop.upper() not in flags:
230
- print(
231
- f"Warning: '{prop}' is not a valid IEF flag, it will be ommited from the IEF\n",
239
+ logging.warning(
240
+ "'%s' is not a valid IEF flag, it will be ommited from the IEF\n",
241
+ prop,
232
242
  )
233
243
  continue
234
244
 
@@ -279,10 +289,11 @@ class IEF(FMFile):
279
289
  def _update_eventdata_info(self): # noqa: C901
280
290
  if not isinstance(self.eventdata, dict):
281
291
  # If attribute not a dict, adds the value as a single entry in list
282
- raise AttributeError(
292
+ msg = (
283
293
  "The 'EventData' attribute should be a dictionary with keys defining the event"
284
- " names and values referencing the IED files",
294
+ " names and values referencing the IED files"
285
295
  )
296
+ raise AttributeError(msg)
286
297
 
287
298
  # Number of 'EventData' flags in ief
288
299
  event_properties = self._ief_properties.count("EventData")
@@ -328,11 +339,12 @@ class IEF(FMFile):
328
339
  try:
329
340
  self.NoOfFlowTimeSeries = sum([ftp.count_series() for ftp in self.flowtimeprofiles])
330
341
  except FileNotFoundError as err:
331
- raise UserWarning(
342
+ msg = (
332
343
  "Failed to read csv referenced in flowtimeprofile, file either does not exist or is"
333
344
  "unable to be found due to relative path from IEF file. NoOfFlowTimeSeries has not"
334
- "been updated.",
335
- ) from err
345
+ "been updated."
346
+ )
347
+ raise UserWarning(msg) from err
336
348
 
337
349
  end_index = None
338
350
  start_index = (
@@ -434,6 +446,7 @@ class IEF(FMFile):
434
446
  filepath (string): Full filepath to new location for ief file (including '.ief' extension)
435
447
  """
436
448
  self._save(filepath)
449
+ self._log_path = self._filepath.with_suffix(".lf1")
437
450
 
438
451
  @handle_exception(when="simulate")
439
452
  def simulate( # noqa: C901, PLR0912, PLR0913
@@ -471,9 +484,8 @@ class IEF(FMFile):
471
484
  self._range_function = range_function
472
485
  self._range_settings = range_settings if range_settings else {}
473
486
  if self._filepath is None:
474
- raise UserWarning(
475
- "IEF must be saved to a specific filepath before simulate() can be called.",
476
- )
487
+ msg = "IEF must be saved to a specific filepath before simulate() can be called."
488
+ raise UserWarning(msg)
477
489
  if precision.upper() == "DEFAULT":
478
490
  precision = "SINGLE" # Defaults to single...
479
491
  for attr in dir(self):
@@ -489,9 +501,8 @@ class IEF(FMFile):
489
501
  else:
490
502
  _enginespath = enginespath
491
503
  if not Path(_enginespath).exists():
492
- raise Exception(
493
- f"Flood Modeller non-default engine path not found! {str(_enginespath)}",
494
- )
504
+ msg = f"Flood Modeller non-default engine path not found! {_enginespath!s}"
505
+ raise Exception(msg)
495
506
 
496
507
  if precision.upper() == "SINGLE":
497
508
  isis32_fp = str(Path(_enginespath, "ISISf32.exe"))
@@ -499,14 +510,15 @@ class IEF(FMFile):
499
510
  isis32_fp = str(Path(_enginespath, "ISISf32_DoubleP.exe"))
500
511
 
501
512
  if not Path(isis32_fp).exists():
502
- raise Exception(f"Flood Modeller engine not found! Expected location: {isis32_fp}")
513
+ msg = f"Flood Modeller engine not found! Expected location: {isis32_fp}"
514
+ raise Exception(msg)
503
515
 
504
- run_command = f'"{isis32_fp}" -sd "{self._filepath}"'
516
+ run_command = f'"{isis32_fp}" -sd "{self._filepath.resolve()}"'
505
517
 
506
518
  if method.upper() == "WAIT":
507
- print("Executing simulation...")
519
+ logging.info("Executing simulation...")
508
520
  # execute simulation
509
- process = Popen(run_command, cwd=os.path.dirname(self._filepath))
521
+ process = Popen(run_command, cwd=Path(self._filepath).parent)
510
522
 
511
523
  # progress bar based on log files
512
524
  steady = self.RunType == "Steady"
@@ -521,12 +533,12 @@ class IEF(FMFile):
521
533
 
522
534
  if result == 1 and raise_on_failure:
523
535
  raise RuntimeError(summary)
524
- print(summary)
536
+ logging.info(summary)
525
537
 
526
538
  elif method.upper() == "RETURN_PROCESS":
527
- print("Executing simulation...")
539
+ logging.info("Executing simulation...")
528
540
  # execute simulation
529
- return Popen(run_command, cwd=os.path.dirname(self._filepath))
541
+ return Popen(run_command, cwd=Path(self._filepath).parent)
530
542
 
531
543
  return None
532
544
 
@@ -553,7 +565,8 @@ class IEF(FMFile):
553
565
  result_path = self._get_result_filepath(suffix="zzn")
554
566
 
555
567
  if not result_path.exists():
556
- raise FileNotFoundError("Simulation results file (zzn) not found")
568
+ msg = "Simulation results file (zzn) not found"
569
+ raise FileNotFoundError(msg)
557
570
 
558
571
  return ZZN(result_path)
559
572
 
@@ -565,7 +578,8 @@ class IEF(FMFile):
565
578
  """
566
579
 
567
580
  if not self._log_path.exists():
568
- raise FileNotFoundError("Log file (LF1) not found")
581
+ msg = "Log file (LF1) not found"
582
+ raise FileNotFoundError(msg)
569
583
 
570
584
  steady = self.RunType == "Steady"
571
585
  return LF1(self._log_path, steady)
@@ -616,7 +630,8 @@ class IEF(FMFile):
616
630
  exy_path = self._filepath.with_suffix(".exy")
617
631
 
618
632
  if not exy_path.exists():
619
- raise FileNotFoundError("Simulation results error log (.exy) not found")
633
+ msg = "Simulation results error log (.exy) not found"
634
+ raise FileNotFoundError(msg)
620
635
 
621
636
  exy_data = pd.read_csv(exy_path, names=["node", "timestep", "severity", "code", "summary"])
622
637
  exy_data["type"] = exy_data["code"].apply(
@@ -678,9 +693,8 @@ class FlowTimeProfile(Jsonable):
678
693
  self.profile = kwargs.get("profile", "")
679
694
  self.comment = kwargs.get("comment", "")
680
695
  else:
681
- raise ValueError(
682
- "You must provide either a single raw string argument or keyword arguments.",
683
- )
696
+ msg = "You must provide either a single raw string argument or keyword arguments."
697
+ raise ValueError(msg)
684
698
 
685
699
  base_path = Path(kwargs.get("ief_filepath", ""))
686
700
  self._csvfile = (base_path / self.csv_filepath.strip('"')).resolve()
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Flood Modeller Python API
3
- Copyright (C) 2024 Jacobs U.K. Limited
3
+ Copyright (C) 2025 Jacobs U.K. Limited
4
4
 
5
5
  This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
6
6
  as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
floodmodeller_api/inp.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Flood Modeller Python API
3
- Copyright (C) 2024 Jacobs U.K. Limited
3
+ Copyright (C) 2025 Jacobs U.K. Limited
4
4
 
5
5
  This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
6
6
  as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@@ -20,7 +20,7 @@ from pathlib import Path
20
20
 
21
21
  from . import units
22
22
  from ._base import FMFile
23
- from .units.helpers import _to_str, join_n_char_ljust
23
+ from .units._helpers import join_n_char_ljust, to_str
24
24
  from .urban1d import subsections
25
25
  from .urban1d.general_parameters import DEFAULT_OPTIONS
26
26
  from .util import handle_exception
@@ -59,8 +59,8 @@ class INP(FMFile):
59
59
 
60
60
  def _read(self):
61
61
  # Read INP file
62
- with open(self._filepath) as inp_file:
63
- self._raw_data = [line.rstrip("\n") for line in inp_file.readlines()]
62
+ with open(self._filepath, encoding=self.ENCODING) as inp_file:
63
+ self._raw_data = [line.rstrip("\n") for line in inp_file]
64
64
 
65
65
  # Generate INP file structure
66
66
  self._update_inp_struct()
@@ -129,9 +129,8 @@ class INP(FMFile):
129
129
  return "\n".join(self._raw_data) + "\n"
130
130
 
131
131
  def _create_from_blank(self):
132
- raise NotImplementedError(
133
- "Creating new 1D urban models (INP files) is not yet supported by floodmodeller_api, only existing models can be read",
134
- )
132
+ msg = "Creating new 1D urban models (INP files) is not yet supported by floodmodeller_api, only existing models can be read"
133
+ raise NotImplementedError(msg)
135
134
 
136
135
  def _get_section_definitions(self):
137
136
  """Internal method used to get section definitions for each supported unit type and general parameters."""
@@ -159,7 +158,7 @@ class INP(FMFile):
159
158
  data = units.helpers.split_n_char(line, 21)
160
159
 
161
160
  # Set type to Float or Stirng, as appropirate.
162
- self.options[data[0].lower()] = _to_str(
161
+ self.options[data[0].lower()] = to_str(
163
162
  data[1],
164
163
  None,
165
164
  check_float=True,
@@ -198,8 +197,6 @@ class INP(FMFile):
198
197
  in_block = False
199
198
  unit_block = {}
200
199
  for idx, line in enumerate(self._raw_data):
201
- # TODO: Add functionality to compare first four characters only (alphanumeric) - need to consider names shorter than 4 characters, and those with _ within name
202
-
203
200
  # Check if subsection is known
204
201
  if line.upper() in subsections.ALL_SUBSECTIONS:
205
202
  if in_block is True:
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Flood Modeller Python API
3
- Copyright (C) 2024 Jacobs U.K. Limited
3
+ Copyright (C) 2025 Jacobs U.K. Limited
4
4
 
5
5
  This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
6
6
  as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@@ -17,6 +17,7 @@ address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London
17
17
  from __future__ import annotations
18
18
 
19
19
  import datetime as dt
20
+ import logging
20
21
  import time
21
22
  from typing import TYPE_CHECKING
22
23
 
@@ -55,7 +56,7 @@ class LF(FMFile):
55
56
  def __init__(
56
57
  self,
57
58
  lf_filepath: str | Path | None,
58
- data_to_extract: dict,
59
+ data_to_extract: dict[str, dict],
59
60
  steady: bool = False,
60
61
  ):
61
62
  FMFile.__init__(self, lf_filepath)
@@ -70,7 +71,7 @@ class LF(FMFile):
70
71
  def _read(self, force_reread: bool = False, suppress_final_step: bool = False):
71
72
  # Read LF file
72
73
  with open(self._filepath) as lf_file:
73
- self._raw_data = [line.rstrip("\n") for line in lf_file.readlines()]
74
+ self._raw_data = [line.rstrip("\n") for line in lf_file]
74
75
 
75
76
  # Force rereading from start of file
76
77
  if force_reread is True:
@@ -140,17 +141,12 @@ class LF(FMFile):
140
141
  def _get_index(self):
141
142
  """Finds key and dataframe for variable that is the index"""
142
143
 
143
- for key in self._data_to_extract:
144
- try:
145
- self._data_to_extract[key]["is_index"]
146
- index_key = key
147
- index_df = self._extracted_data[key].data.get_value()
148
- return index_key, index_df
149
-
150
- except KeyError:
151
- pass
144
+ for k, v in self._data_to_extract.items():
145
+ if "is_index" in v:
146
+ return k, self._extracted_data[k].data.get_value()
152
147
 
153
- raise Exception("No index variable found")
148
+ msg = "No index variable found"
149
+ raise Exception(msg)
154
150
 
155
151
  def _set_attributes(self):
156
152
  """Makes each Parser value an attribute; "last" values in dictionary"""
@@ -180,7 +176,7 @@ class LF(FMFile):
180
176
 
181
177
  delattr(self, "info")
182
178
 
183
- def to_dataframe(self, *, include_tuflow: bool = False) -> pd.DataFrame:
179
+ def to_dataframe(self, variable: str = "all", *, include_tuflow: bool = False) -> pd.DataFrame:
184
180
  """Collects parameter values that change throughout simulation into a dataframe
185
181
 
186
182
  Args:
@@ -190,20 +186,23 @@ class LF(FMFile):
190
186
  pd.DataFrame: DataFrame of log file parameters indexed by simulation time (unsteady) or network iterations (steady)
191
187
  """
192
188
 
193
- # TODO: make more like ZZN.to_dataframe
194
-
195
- data_type_all = {
189
+ lf_df_data = {
196
190
  k: getattr(self, k)
197
191
  for k, v in self._data_to_extract.items()
198
- if v["data_type"] == "all" and (include_tuflow or "tuflow" not in k)
192
+ if v["data_type"] == "all" # with entries every iteration
193
+ and (include_tuflow or "tuflow" not in k) # tuflow-related only if requested
194
+ and (variable in ("all", k, *v.get("subheaders", []))) # if it or all are requested
199
195
  }
200
196
 
201
- df = pd.concat(data_type_all, axis=1)
202
- df.columns = df.columns.droplevel()
203
-
204
- df.sort_index(inplace=True)
197
+ if lf_df_data == {}:
198
+ msg = f"No data extracted for variable '{variable}'"
199
+ raise ValueError(msg)
205
200
 
206
- return df
201
+ lf_df = pd.concat(lf_df_data, axis=1)
202
+ lf_df.columns = lf_df.columns.droplevel()
203
+ if variable != "all":
204
+ lf_df = lf_df[variable] # otherwise subheaders result in extra columns
205
+ return lf_df.sort_index()
207
206
 
208
207
  def _sync_cols(self):
209
208
  """Ensures Parser values (of type "all") have an entry each iteration"""
@@ -224,7 +223,7 @@ class LF(FMFile):
224
223
  def _print_no_lines(self):
225
224
  """Prints number of lines that have been read so far"""
226
225
 
227
- print("Last line read: " + str(self._no_lines))
226
+ logging.info("Last line read: %s", self._no_lines)
228
227
 
229
228
  def report_progress(self) -> float:
230
229
  """Returns progress for unsteady simulations
@@ -323,7 +322,7 @@ def create_lf(filepath: Path, suffix: str) -> LF1 | LF2 | None:
323
322
  """Checks for a new log file, waiting for its creation if necessary"""
324
323
 
325
324
  def _no_log_file(reason: str) -> None:
326
- print(f"No progress bar as {reason}. Simulation will continue as usual.")
325
+ logging.warning("No progress bar as %s. Simulation will continue as usual.", reason)
327
326
 
328
327
  # ensure progress bar is supported
329
328
  if suffix not in {"lf1", "lf2"}:
@@ -356,7 +355,7 @@ def create_lf(filepath: Path, suffix: str) -> LF1 | LF2 | None:
356
355
  last_modified = dt.datetime.fromtimestamp(last_modified_timestamp)
357
356
  time_diff_sec = (dt.datetime.now() - last_modified).total_seconds()
358
357
 
359
- # it's old if it's over OLD_FILE seconds old (TODO: is this robust?)
358
+ # it's old if it's over OLD_FILE seconds old
360
359
  old_log_file = time_diff_sec > OLD_FILE
361
360
 
362
361
  # timeout
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Flood Modeller Python API
3
- Copyright (C) 2024 Jacobs U.K. Limited
3
+ Copyright (C) 2025 Jacobs U.K. Limited
4
4
 
5
5
  This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
6
6
  as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@@ -64,42 +64,40 @@ class AllData(Data):
64
64
  index_key: str | None = None,
65
65
  index_df: pd.Series | None = None,
66
66
  ) -> pd.DataFrame:
67
- df = pd.DataFrame(self._value)
67
+ value_df = pd.DataFrame(self._value)
68
68
 
69
69
  # do nothing to empty dataframes
70
- if df.empty:
71
- return df
70
+ if value_df.empty:
71
+ return value_df
72
72
 
73
73
  # overall header
74
74
  if self._subheaders is None:
75
- df.rename(columns={df.columns[0]: self.header}, inplace=True)
75
+ value_df = value_df.rename(columns={value_df.columns[0]: self.header})
76
76
 
77
77
  elif index_key is not None:
78
78
  # subheaders
79
- df = df.set_axis(self._subheaders, axis=1)
79
+ value_df = value_df.set_axis(self._subheaders, axis=1)
80
80
 
81
81
  # remove duplicate of index
82
82
  # sometimes it includes extra values
83
83
  # it also has different precision
84
84
  index_duplicate = index_key + "_duplicate"
85
- if index_duplicate in df.columns:
85
+ if index_duplicate in value_df.columns:
86
86
  try:
87
- index_df = df[index_duplicate].dt.round("1s")
88
- df.drop(index_duplicate, axis=1, inplace=True)
87
+ index_df = value_df[index_duplicate].dt.round("1s")
88
+ value_df = value_df.drop(index_duplicate, axis=1)
89
89
  except AttributeError:
90
- df = df.drop(columns=index_duplicate)
90
+ value_df = value_df.drop(columns=index_duplicate)
91
91
 
92
92
  # there is no index because *this* is the index
93
93
  if index_key is None:
94
- return df
94
+ return value_df
95
95
 
96
96
  # made lf index the dataframe index
97
- df[index_key] = index_df
98
- df.dropna(inplace=True)
99
- df.drop_duplicates(subset=index_key, keep="last", inplace=True)
100
- df.set_index(index_key, inplace=True)
101
-
102
- return df
97
+ value_df[index_key] = index_df
98
+ value_df = value_df.dropna()
99
+ value_df = value_df.drop_duplicates(subset=index_key, keep="last")
100
+ return value_df.set_index(index_key)
103
101
 
104
102
 
105
103
  def data_factory(data_type: str, header: str, subheaders: list | None = None):
@@ -107,7 +105,8 @@ def data_factory(data_type: str, header: str, subheaders: list | None = None):
107
105
  return LastData(header, subheaders)
108
106
  if data_type == "all":
109
107
  return AllData(header, subheaders)
110
- raise ValueError(f'Unexpected data "{data_type}"')
108
+ msg = f'Unexpected data "{data_type}"'
109
+ raise ValueError(msg)
111
110
 
112
111
 
113
112
  class State(ABC):
@@ -140,7 +139,8 @@ class SteadyState(State):
140
139
  pass
141
140
 
142
141
  def report_progress(self):
143
- raise NotImplementedError("No progress reporting for steady simulations")
142
+ msg = "No progress reporting for steady simulations"
143
+ raise NotImplementedError(msg)
144
144
 
145
145
 
146
146
  def state_factory(steady: bool, extracted_data: Data) -> State:
@@ -264,7 +264,7 @@ class TimeDeltaSParser(Parser):
264
264
  def _process_line(self, raw: str) -> dt.timedelta:
265
265
  """Converts string S (with decimal place and "s") to timedelta"""
266
266
 
267
- s = raw.split("s")[0] # TODO: not necessary for simulation time
267
+ s = raw.split("s")[0] # not necessary for simulation time
268
268
  return dt.timedelta(seconds=float(s))
269
269
 
270
270
 
@@ -1,6 +1,6 @@
1
1
  """
2
2
  Flood Modeller Python API
3
- Copyright (C) 2024 Jacobs U.K. Limited
3
+ Copyright (C) 2025 Jacobs U.K. Limited
4
4
 
5
5
  This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
6
6
  as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
@@ -390,10 +390,6 @@ lf1_steady_data_to_extract = {
390
390
  },
391
391
  }
392
392
 
393
- # TODO for LF2:
394
- # - "start" only works for v6
395
- # - what if there are multiple domains?
396
-
397
393
  lf2_data_to_extract = {
398
394
  # start
399
395
  "version": {
@@ -1,3 +1,5 @@
1
+ from __future__ import annotations
2
+
1
3
  from typing import Any
2
4
 
3
5
  from . import DAT, IED, IEF, INP, LF1, LF2, XML2D, ZZN
@@ -14,12 +16,15 @@ from .units import (
14
16
  HTBDY,
15
17
  IIC,
16
18
  INTERPOLATE,
19
+ JUNCTION,
20
+ LATERAL,
17
21
  ORIFICE,
18
22
  OUTFALL,
19
23
  QHBDY,
20
24
  QTBDY,
21
25
  REFHBDY,
22
26
  REPLICATE,
27
+ RESERVOIR,
23
28
  RIVER,
24
29
  RNWEIR,
25
30
  SLUICE,
@@ -30,7 +35,8 @@ from .units import (
30
35
  )
31
36
  from .urban1d.conduits import CONDUIT as CONDUIT_URBAN
32
37
  from .urban1d.conduits import CONDUITS as CONDUITS_URBAN
33
- from .urban1d.junctions import JUNCTION, JUNCTIONS
38
+ from .urban1d.junctions import JUNCTION as JUNCTION_URBAN
39
+ from .urban1d.junctions import JUNCTIONS
34
40
  from .urban1d.losses import LOSS, LOSSES
35
41
  from .urban1d.outfalls import OUTFALL as OUTFALL_URBAN
36
42
  from .urban1d.outfalls import OUTFALLS as OUTFALLS_URBAN
@@ -49,7 +55,7 @@ api_class_mapping: dict[str, Any] = {
49
55
  "floodmodeller_api.zzn.ZZN": ZZN,
50
56
  "floodmodeller_api.backup.File": File,
51
57
  "floodmodeller_api.urban1d.junctions.JUNCTIONS": JUNCTIONS,
52
- "floodmodeller_api.urban1d.junctions.JUNCTION": JUNCTION,
58
+ "floodmodeller_api.urban1d.junctions.JUNCTION": JUNCTION_URBAN,
53
59
  "floodmodeller_api.urban1d.outfalls.OUTFALLS": OUTFALLS_URBAN,
54
60
  "floodmodeller_api.urban1d.outfalls.OUTFALL": OUTFALL_URBAN,
55
61
  "floodmodeller_api.urban1d.raingauges.RAINGAUGES": RAINGAUGES,
@@ -66,6 +72,9 @@ api_class_mapping: dict[str, Any] = {
66
72
  "floodmodeller_api.units.boundaries.REFHBDY": REFHBDY,
67
73
  "floodmodeller_api.units.comment.COMMENT": COMMENT,
68
74
  "floodmodeller_api.units.conduits.CONDUIT": CONDUIT,
75
+ "floodmodeller_api.units.connectors.JUNCTION": JUNCTION,
76
+ "floodmodeller_api.units.connectors.LATERAL": LATERAL,
77
+ "floodmodeller_api.units.controls.RESERVOIR": RESERVOIR,
69
78
  "floodmodeller_api.units.iic.IIC": IIC,
70
79
  "floodmodeller_api.units.losses.BLOCKAGE": BLOCKAGE,
71
80
  "floodmodeller_api.units.losses.CULVERT": CULVERT,
@@ -1,4 +1,4 @@
1
- import os
2
1
  import sys
2
+ from pathlib import Path
3
3
 
4
- sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
4
+ sys.path.insert(0, str(Path(__file__).parent.parent))
@@ -1,4 +1,3 @@
1
- import os
2
1
  from pathlib import Path
3
2
  from unittest.mock import patch
4
3
 
@@ -6,8 +5,8 @@ import pytest
6
5
 
7
6
 
8
7
  @pytest.fixture(scope="session")
9
- def test_workspace():
10
- return Path(os.path.dirname(__file__), "test_data")
8
+ def test_workspace() -> Path:
9
+ return Path(__file__).parent / "test_data"
11
10
 
12
11
 
13
12
  @pytest.fixture()
@@ -6,13 +6,13 @@ import pytest
6
6
  from floodmodeller_api.backup import BackupControl, File
7
7
 
8
8
 
9
- @pytest.fixture
9
+ @pytest.fixture()
10
10
  def backup_control():
11
11
  # Use a different directory for testing
12
12
  return BackupControl()
13
13
 
14
14
 
15
- @pytest.fixture
15
+ @pytest.fixture()
16
16
  def file(test_workspace):
17
17
  test_file = Path(test_workspace, "EX1.DAT")
18
18
  file = File(test_file)