floodmodeller-api 0.5.0__py3-none-any.whl → 0.5.1__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 (80) hide show
  1. floodmodeller_api/__init__.py +1 -1
  2. floodmodeller_api/_base.py +26 -16
  3. floodmodeller_api/backup.py +3 -2
  4. floodmodeller_api/dat.py +29 -30
  5. floodmodeller_api/diff.py +3 -3
  6. floodmodeller_api/hydrology_plus/hydrology_plus_export.py +14 -13
  7. floodmodeller_api/ied.py +6 -6
  8. floodmodeller_api/ief.py +27 -25
  9. floodmodeller_api/inp.py +3 -4
  10. floodmodeller_api/logs/lf.py +9 -16
  11. floodmodeller_api/logs/lf_helpers.py +18 -18
  12. floodmodeller_api/mapping.py +2 -0
  13. floodmodeller_api/test/__init__.py +2 -2
  14. floodmodeller_api/test/conftest.py +2 -3
  15. floodmodeller_api/test/test_backup.py +2 -2
  16. floodmodeller_api/test/test_conveyance.py +4 -3
  17. floodmodeller_api/test/test_dat.py +2 -2
  18. floodmodeller_api/test/test_data/structure_logs/EX17_expected.csv +4 -0
  19. floodmodeller_api/test/test_data/structure_logs/EX17_expected.json +69 -0
  20. floodmodeller_api/test/test_data/structure_logs/EX18_expected.csv +20 -0
  21. floodmodeller_api/test/test_data/structure_logs/EX18_expected.json +292 -0
  22. floodmodeller_api/test/test_data/structure_logs/EX6_expected.csv +4 -0
  23. floodmodeller_api/test/test_data/structure_logs/EX6_expected.json +35 -0
  24. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_flow.csv +182 -0
  25. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_fr.csv +182 -0
  26. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_mode.csv +182 -0
  27. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_stage.csv +182 -0
  28. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_state.csv +182 -0
  29. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzn_velocity.csv +182 -0
  30. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_left_fp_h.csv +182 -0
  31. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_left_fp_mode.csv +182 -0
  32. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_link_inflow.csv +182 -0
  33. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_max.csv +87 -0
  34. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_right_fp_h.csv +182 -0
  35. floodmodeller_api/test/test_data/tabular_csv_outputs/network_zzx_right_fp_mode.csv +182 -0
  36. floodmodeller_api/test/test_flowtimeprofile.py +2 -2
  37. floodmodeller_api/test/test_hydrology_plus_export.py +4 -2
  38. floodmodeller_api/test/test_ied.py +2 -2
  39. floodmodeller_api/test/test_ief.py +2 -2
  40. floodmodeller_api/test/test_inp.py +2 -2
  41. floodmodeller_api/test/test_json.py +5 -10
  42. floodmodeller_api/test/test_logs_lf.py +6 -6
  43. floodmodeller_api/test/test_read_file.py +1 -0
  44. floodmodeller_api/test/test_river.py +79 -2
  45. floodmodeller_api/test/test_tool.py +8 -5
  46. floodmodeller_api/test/test_toolbox_structure_log.py +149 -158
  47. floodmodeller_api/test/test_xml2d.py +9 -11
  48. floodmodeller_api/test/test_zz.py +143 -0
  49. floodmodeller_api/to_from_json.py +8 -8
  50. floodmodeller_api/tool.py +12 -6
  51. floodmodeller_api/toolbox/example_tool.py +5 -1
  52. floodmodeller_api/toolbox/model_build/add_siltation_definition.py +12 -8
  53. floodmodeller_api/toolbox/model_build/structure_log/structure_log.py +498 -196
  54. floodmodeller_api/toolbox/model_build/structure_log_definition.py +5 -1
  55. floodmodeller_api/units/_base.py +14 -10
  56. floodmodeller_api/units/conveyance.py +10 -8
  57. floodmodeller_api/units/helpers.py +1 -3
  58. floodmodeller_api/units/losses.py +2 -3
  59. floodmodeller_api/units/sections.py +15 -11
  60. floodmodeller_api/units/structures.py +9 -9
  61. floodmodeller_api/units/units.py +2 -0
  62. floodmodeller_api/urban1d/_base.py +6 -9
  63. floodmodeller_api/urban1d/outfalls.py +2 -1
  64. floodmodeller_api/urban1d/raingauges.py +2 -1
  65. floodmodeller_api/urban1d/subsections.py +2 -0
  66. floodmodeller_api/urban1d/xsections.py +3 -2
  67. floodmodeller_api/util.py +16 -2
  68. floodmodeller_api/validation/validation.py +2 -1
  69. floodmodeller_api/version.py +1 -1
  70. floodmodeller_api/xml2d.py +18 -20
  71. floodmodeller_api/zz.py +538 -0
  72. {floodmodeller_api-0.5.0.dist-info → floodmodeller_api-0.5.1.dist-info}/METADATA +20 -14
  73. {floodmodeller_api-0.5.0.dist-info → floodmodeller_api-0.5.1.dist-info}/RECORD +78 -60
  74. {floodmodeller_api-0.5.0.dist-info → floodmodeller_api-0.5.1.dist-info}/WHEEL +1 -1
  75. floodmodeller_api/test/test_zzn.py +0 -36
  76. floodmodeller_api/zzn.py +0 -414
  77. /floodmodeller_api/test/test_data/{network_from_tabularCSV.csv → tabular_csv_outputs/network_zzn_max.csv} +0 -0
  78. {floodmodeller_api-0.5.0.dist-info → floodmodeller_api-0.5.1.dist-info}/LICENSE.txt +0 -0
  79. {floodmodeller_api-0.5.0.dist-info → floodmodeller_api-0.5.1.dist-info}/entry_points.txt +0 -0
  80. {floodmodeller_api-0.5.0.dist-info → floodmodeller_api-0.5.1.dist-info}/top_level.txt +0 -0
@@ -1,3 +1,7 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import ClassVar
4
+
1
5
  from floodmodeller_api.tool import FMTool, Parameter
2
6
 
3
7
  from .structure_log import StructureLogBuilder
@@ -50,7 +54,7 @@ class StructureLog(FMTool):
50
54
 
51
55
  name = "Structure Log"
52
56
  description = "Creates a structure log"
53
- parameters = [
57
+ parameters: ClassVar[list[Parameter]] = [
54
58
  Parameter(
55
59
  name="input_path",
56
60
  dtype=str,
@@ -40,7 +40,11 @@ class Unit(Jsonable):
40
40
  self._create_from_blank(**kwargs)
41
41
 
42
42
  @property
43
- def name(self):
43
+ def unit(self) -> str:
44
+ return self._unit
45
+
46
+ @property
47
+ def name(self) -> str | None:
44
48
  return self._name
45
49
 
46
50
  @name.setter
@@ -48,20 +52,21 @@ class Unit(Jsonable):
48
52
  try:
49
53
  new_name = str(new_name)
50
54
  if " " in new_name:
51
- raise Exception(
52
- f'Cannot set unit name to "{new_name}" as it contains one or more spaces',
53
- )
55
+ msg = f'Cannot set unit name to "{new_name}" as it contains one or more spaces'
56
+ raise Exception(msg)
54
57
  self._name = new_name
55
58
  except Exception as e:
56
- raise Exception(f'Failed to set unit name to "{new_name}" due to error: {e}') from e
59
+ msg = f'Failed to set unit name to "{new_name}" due to error: {e}'
60
+ raise Exception(msg) from e
57
61
 
58
62
  @property
59
- def subtype(self):
63
+ def subtype(self) -> str | None:
60
64
  return self._subtype
61
65
 
62
66
  @subtype.setter
63
67
  def subtype(self, new_value):
64
- raise ValueError("You cannot change the subtype of a unit once it has been instantiated")
68
+ msg = "You cannot change the subtype of a unit once it has been instantiated"
69
+ raise ValueError(msg)
65
70
 
66
71
  def __repr__(self):
67
72
  if self._subtype is None:
@@ -71,9 +76,8 @@ class Unit(Jsonable):
71
76
  )
72
77
 
73
78
  def _create_from_blank(self):
74
- raise NotImplementedError(
75
- f"Creating new {self._unit} units is not yet supported by floodmodeller_api, only existing units can be read",
76
- )
79
+ msg = f"Creating new {self._unit} units is not yet supported by floodmodeller_api, only existing units can be read"
80
+ raise NotImplementedError(msg)
77
81
 
78
82
  def __str__(self):
79
83
  return "\n".join(self._write())
@@ -72,11 +72,12 @@ def calculate_cross_section_conveyance(
72
72
  total_length = np.where(in_panel_and_section, length, 0).sum(axis=1)
73
73
  total_mannings = np.where(in_panel_and_section, mannings, 0).sum(axis=1)
74
74
 
75
- conveyance += np.where(
76
- total_length >= MINIMUM_PERIMETER_THRESHOLD,
77
- total_area ** (5 / 3) * total_length ** (1 / 3) / (total_mannings * rpl_panel),
78
- 0,
79
- )
75
+ with np.errstate(divide="ignore", invalid="ignore"):
76
+ conveyance += np.where(
77
+ total_length >= MINIMUM_PERIMETER_THRESHOLD,
78
+ total_area ** (5 / 3) * total_length ** (1 / 3) / (total_mannings * rpl_panel),
79
+ 0,
80
+ )
80
81
 
81
82
  return pd.Series(conveyance, index=water_levels)
82
83
 
@@ -116,9 +117,10 @@ def calculate_geometry(
116
117
  is_submerged_on_right = (h1 <= 0) & (h2 > 0)
117
118
  conditions = [is_submerged, is_submerged_on_left, is_submerged_on_right]
118
119
 
119
- # needed for partially submerged sections
120
- dx_left = dx * h1 / (h1 - h2)
121
- dx_right = dx * h2 / (h2 - h1)
120
+ with np.errstate(divide="ignore", invalid="ignore"):
121
+ # needed for partially submerged sections
122
+ dx_left = dx * h1 / (h1 - h2)
123
+ dx_right = dx * h2 / (h2 - h1)
122
124
 
123
125
  area = np.select(
124
126
  conditions,
@@ -116,8 +116,6 @@ def _to_data_list(block: list[str], num_cols: int | None = None, date_col: int |
116
116
  else:
117
117
  row_split = [_to_float(itm) for itm in row_split]
118
118
 
119
- row_list = []
120
- for var in row_split:
121
- row_list.append(var)
119
+ row_list = list(row_split)
122
120
  data_list.append(row_list)
123
121
  return data_list
@@ -238,9 +238,8 @@ class BLOCKAGE(Unit):
238
238
 
239
239
  # Custom validation for blockage percentage
240
240
  if self.data.max() > 1 or self.data.min() < 0:
241
- raise ValueError(
242
- f"Parameter error with {repr(self)} - blockage percentage must be between 0 and 1",
243
- )
241
+ msg = f"Parameter error with {self!r} - blockage percentage must be between 0 and 1"
242
+ raise ValueError(msg)
244
243
 
245
244
  header = f"BLOCKAGE #revision#{self._revision} {self.comment}"
246
245
  labels = join_n_char_ljust(
@@ -14,6 +14,10 @@ If you have any query about this program or this License, please contact us at s
14
14
  address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London, SE1 2QG, United Kingdom.
15
15
  """
16
16
 
17
+ from __future__ import annotations
18
+
19
+ from typing import ClassVar
20
+
17
21
  import pandas as pd
18
22
 
19
23
  from floodmodeller_api.validation import _validate_unit
@@ -54,7 +58,7 @@ class RIVER(Unit):
54
58
  """
55
59
 
56
60
  _unit = "RIVER"
57
- _required_columns = [
61
+ _required_columns: ClassVar[list[str]] = [
58
62
  "X",
59
63
  "Y",
60
64
  "Mannings n",
@@ -204,7 +208,7 @@ class RIVER(Unit):
204
208
  # Manual so slope can have more sf
205
209
  params = f'{self.dist_to_next:>10.3f}{"":>10}{self.slope:>10.6f}{self.density:>10.3f}'
206
210
  self.nrows = len(self._data)
207
- riv_block = [header, self.subtype, labels, params, f"{str(self.nrows):>10}"]
211
+ riv_block = [header, self.subtype, labels, params, f"{self.nrows!s:>10}"]
208
212
 
209
213
  riv_data = []
210
214
  for (
@@ -257,11 +261,11 @@ class RIVER(Unit):
257
261
  @data.setter
258
262
  def data(self, new_df: pd.DataFrame) -> None:
259
263
  if not isinstance(new_df, pd.DataFrame):
260
- raise ValueError(
261
- "The updated data table for a cross section must be a pandas DataFrame.",
262
- )
263
- if new_df.columns != self._required_columns:
264
- raise ValueError(f"The DataFrame must only contain columns: {self._required_columns}")
264
+ msg = "The updated data table for a cross section must be a pandas DataFrame."
265
+ raise ValueError(msg)
266
+ if list(map(str.lower, new_df.columns)) != list(map(str.lower, self._required_columns)):
267
+ msg = f"The DataFrame must only contain columns: {self._required_columns}"
268
+ raise ValueError(msg)
265
269
  self._data = new_df
266
270
 
267
271
  @property
@@ -325,11 +329,11 @@ class RIVER(Unit):
325
329
  @active_data.setter
326
330
  def active_data(self, new_df: pd.DataFrame) -> None:
327
331
  if not isinstance(new_df, pd.DataFrame):
328
- raise ValueError(
329
- "The updated data table for a cross section must be a pandas DataFrame.",
330
- )
332
+ msg = "The updated data table for a cross section must be a pandas DataFrame."
333
+ raise ValueError(msg)
331
334
  if new_df.columns.to_list() != self._required_columns:
332
- raise ValueError(f"The DataFrame must only contain columns: {self._required_columns}")
335
+ msg = f"The DataFrame must only contain columns: {self._required_columns}"
336
+ raise ValueError(msg)
333
337
 
334
338
  # Ensure activation markers are present
335
339
  new_df = new_df.copy()
@@ -376,7 +376,7 @@ class BRIDGE(Unit):
376
376
  self.orifice_discharge_coefficient,
377
377
  )
378
378
  self.section_nrows = len(self.section_data)
379
- br_block.extend(["MANNING", params, f"{str(self.section_nrows):>10}"])
379
+ br_block.extend(["MANNING", params, f"{self.section_nrows!s:>10}"])
380
380
 
381
381
  section_data = []
382
382
  for _, x, y, n, embankments in self.section_data.itertuples():
@@ -388,7 +388,7 @@ class BRIDGE(Unit):
388
388
  br_block.extend(section_data)
389
389
 
390
390
  self.opening_nrows = len(self.opening_data)
391
- br_block.append(f"{str(self.opening_nrows):>10}")
391
+ br_block.append(f"{self.opening_nrows!s:>10}")
392
392
  opening_data = []
393
393
  for _, start, finish, spring, soffit in self.opening_data.itertuples():
394
394
  row = join_10_char(start, finish, spring, soffit)
@@ -424,10 +424,10 @@ class BRIDGE(Unit):
424
424
  [
425
425
  "MANNING",
426
426
  params,
427
- f"{str(self.abutment_type):>10}",
427
+ f"{self.abutment_type!s:>10}",
428
428
  pier_params,
429
429
  self.abutment_alignment,
430
- f"{str(self.section_nrows):>10}",
430
+ f"{self.section_nrows!s:>10}",
431
431
  ],
432
432
  )
433
433
 
@@ -440,7 +440,7 @@ class BRIDGE(Unit):
440
440
  br_block.extend(section_data)
441
441
 
442
442
  self.opening_nrows = len(self.opening_data)
443
- br_block.append(f"{str(self.opening_nrows):>10}")
443
+ br_block.append(f"{self.opening_nrows!s:>10}")
444
444
  opening_data = []
445
445
  for _, start, finish, spring, soffit in self.opening_data.itertuples():
446
446
  row = join_10_char(start, finish, spring, soffit)
@@ -448,7 +448,7 @@ class BRIDGE(Unit):
448
448
  br_block.extend(opening_data)
449
449
 
450
450
  self.culvert_nrows = len(self.culvert_data)
451
- br_block.append(f"{str(self.culvert_nrows):>10}")
451
+ br_block.append(f"{self.culvert_nrows!s:>10}")
452
452
  culvert_data = []
453
453
  for (
454
454
  _,
@@ -481,7 +481,7 @@ class BRIDGE(Unit):
481
481
  "YARNELL",
482
482
  params,
483
483
  additional_params,
484
- f"{str(self.us_section_nrows):>10}",
484
+ f"{self.us_section_nrows!s:>10}",
485
485
  ],
486
486
  )
487
487
 
@@ -495,7 +495,7 @@ class BRIDGE(Unit):
495
495
  br_block.extend(us_section_data)
496
496
 
497
497
  self.ds_section_nrows = len(self.ds_section_data)
498
- br_block.append(f"{str(self.ds_section_nrows):>10}")
498
+ br_block.append(f"{self.ds_section_nrows!s:>10}")
499
499
  ds_section_data = []
500
500
  for _, x, y, n, embankments, top_level in self.ds_section_data.itertuples():
501
501
  # Adding extra 10 spaces before embankment flag
@@ -506,7 +506,7 @@ class BRIDGE(Unit):
506
506
  br_block.extend(ds_section_data)
507
507
 
508
508
  self.pier_locs_nrows = len(self.pier_locs_data)
509
- br_block.append(f"{str(self.pier_locs_nrows):>10}")
509
+ br_block.append(f"{self.pier_locs_nrows!s:>10}")
510
510
  pier_locs_data = []
511
511
  for (
512
512
  _,
@@ -14,6 +14,8 @@ If you have any query about this program or this License, please contact us at s
14
14
  address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London, SE1 2QG, United Kingdom.
15
15
  """
16
16
 
17
+ from __future__ import annotations
18
+
17
19
  from typing import TypedDict
18
20
 
19
21
 
@@ -51,9 +51,8 @@ class UrbanUnit(Jsonable):
51
51
  return None
52
52
 
53
53
  def _create_from_blank(self):
54
- raise NotImplementedError(
55
- f"Creating new {self._unit} units is not yet supported by floodmodeller_api, only existing units can be read",
56
- )
54
+ msg = f"Creating new {self._unit} units is not yet supported by floodmodeller_api, only existing units can be read"
55
+ raise NotImplementedError(msg)
57
56
 
58
57
  def __str__(self):
59
58
  return self._write()
@@ -110,9 +109,8 @@ class UrbanSubsection(Jsonable):
110
109
  return f"<floodmodeller_api UrbanSubsection Class: {self._attribute}>"
111
110
 
112
111
  def _create_from_blank(self):
113
- raise NotImplementedError(
114
- f"Creating new {self._name} subsections is not yet supported by floodmodeller_api, only existing subsections can be read",
115
- )
112
+ msg = f"Creating new {self._name} subsections is not yet supported by floodmodeller_api, only existing subsections can be read"
113
+ raise NotImplementedError(msg)
116
114
 
117
115
  def __str__(self):
118
116
  return "\n".join(self._write())
@@ -155,9 +153,8 @@ class UrbanSubsection(Jsonable):
155
153
  # Miss-match found
156
154
  # check that it is not an existing label in units
157
155
  if unit.name in units:
158
- raise Exception(
159
- f'Error: Cannot update label "{name}" to "{unit.name}" beacuase "{unit.name}" already exists in the {self._attribute} subsection',
160
- )
156
+ msg = f'Error: Cannot update label "{name}" to "{unit.name}" beacuase "{unit.name}" already exists in the {self._attribute} subsection'
157
+ raise Exception(msg)
161
158
 
162
159
  units[unit.name] = unit
163
160
  del units[name]
@@ -102,7 +102,8 @@ class OUTFALL(UrbanUnit):
102
102
  params2 = join_n_char_ljust(15, self.tseries, self.gated, self.routeto)
103
103
 
104
104
  else:
105
- raise RuntimeError(f"{self.type} not supported")
105
+ msg = f"{self.type} not supported"
106
+ raise RuntimeError(msg)
106
107
 
107
108
  return params1 + params2
108
109
 
@@ -97,7 +97,8 @@ class RAINGAUGE(UrbanUnit):
97
97
  params2 = join_n_char_ljust(15, self.filename, self.station, self.units)
98
98
 
99
99
  else:
100
- raise RuntimeError(f"{self.data_option} not supported")
100
+ msg = f"{self.data_option} not supported"
101
+ raise RuntimeError(msg)
101
102
 
102
103
  return params1 + params2
103
104
 
@@ -14,6 +14,8 @@ If you have any query about this program or this License, please contact us at s
14
14
  address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London, SE1 2QG, United Kingdom.
15
15
  """
16
16
 
17
+ from __future__ import annotations
18
+
17
19
  from typing import Type, TypedDict
18
20
 
19
21
  from ._base import UrbanSubsection
@@ -31,7 +31,7 @@ class XSECTION(UrbanUnit):
31
31
  geom3 (float): auxiliary parameter (width, side slopes, etc.) as listed in Table D-1. (required, applicable to shape types)
32
32
  geom4 (float):auxiliary parameter (width, side slopes, etc.) as listed in Table D-1. (required, applicable to shape types)
33
33
  barrels (float): Barrels type number of barrels (i.e., number of parallel pipes of equal size, slope, and roughness) associated with a conduit of shape type , or "CUSTOM" type (optional, default is 1).
34
- culvert (int): Culvert code number from Table A.10 for the conduits inlet geometry if it is a culvert subject to possible inlet flow control. Only an option for shape type (leave blank otherwise) (optional, default is "").
34
+ culvert (int): Culvert code number from Table A.10 for the conduit's inlet geometry if it is a culvert subject to possible inlet flow control. Only an option for shape type (leave blank otherwise) (optional, default is "").
35
35
  curve (str): Curve name of a Shape Curve in the [CURVES] section that defines how width varies with depth. (optional, applicable to shape types only)
36
36
  tsect (str): Name of an entry in the [TRANSECTS] section that describes the cross-section geometry of an irregular channel. (required, applicable to "IREGUALAR types only)
37
37
 
@@ -109,7 +109,8 @@ class XSECTION(UrbanUnit):
109
109
  params2 = join_n_char_ljust(15, self.shape, self.tsect)
110
110
 
111
111
  else:
112
- raise RuntimeError(f"{self.shape} not supported")
112
+ msg = f"{self.shape} not supported"
113
+ raise RuntimeError(msg)
113
114
 
114
115
  return params1 + params2
115
116
 
floodmodeller_api/util.py CHANGED
@@ -58,7 +58,7 @@ def read_file(filepath: str | Path) -> FMFile:
58
58
 
59
59
 
60
60
  """
61
- from . import DAT, IED, IEF, INP, LF1, LF2, XML2D, ZZN
61
+ from . import DAT, IED, IEF, INP, LF1, LF2, XML2D, ZZN, ZZX
62
62
  from .hydrology_plus import HydrologyPlusExport
63
63
 
64
64
  suffix_to_class = {
@@ -67,6 +67,7 @@ def read_file(filepath: str | Path) -> FMFile:
67
67
  ".ied": IED,
68
68
  ".xml": XML2D,
69
69
  ".zzn": ZZN,
70
+ ".zzx": ZZX,
70
71
  ".inp": INP,
71
72
  ".lf1": LF1,
72
73
  ".lf2": LF2,
@@ -77,13 +78,26 @@ def read_file(filepath: str | Path) -> FMFile:
77
78
  if api_class:
78
79
  return api_class(filepath)
79
80
 
80
- raise ValueError(f"Unsupported file type: {filepath.suffix}")
81
+ msg = f"Unsupported file type: {filepath.suffix}"
82
+ raise ValueError(msg)
81
83
 
82
84
 
83
85
  def is_windows() -> bool:
84
86
  return sys.platform.startswith("win")
85
87
 
86
88
 
89
+ def get_associated_file(original_file: Path, new_suffix: str) -> Path:
90
+ new_file = original_file.with_suffix(new_suffix)
91
+ if not new_file.exists():
92
+ msg = (
93
+ f"Error: Could not find associated {new_suffix} file."
94
+ f" Ensure that the {original_file.suffix} results"
95
+ f" have an associated {new_suffix} file with matching name."
96
+ )
97
+ raise FileNotFoundError(msg)
98
+ return new_file
99
+
100
+
87
101
  def handle_exception(when: str) -> Callable:
88
102
  """Decorator factory to wrap a method with exception handling."""
89
103
 
@@ -41,7 +41,8 @@ def _validate_unit(unit, urban=False):
41
41
  if not value[0]
42
42
  ],
43
43
  )
44
- raise ValueError(f"One or more parameters in {repr(unit)} are invalid:\n {errors}")
44
+ msg = f"One or more parameters in {unit!r} are invalid:\n {errors}"
45
+ raise ValueError(msg)
45
46
 
46
47
 
47
48
  def _validate_parameter(param, value): # noqa: C901, PLR0911, PLR0912
@@ -1 +1 @@
1
- __version__ = "0.5.0"
1
+ __version__ = "0.5.1"
@@ -17,7 +17,6 @@ address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London
17
17
  from __future__ import annotations
18
18
 
19
19
  import io
20
- import os
21
20
  import time
22
21
  from copy import deepcopy
23
22
  from pathlib import Path
@@ -205,12 +204,9 @@ class XML2D(FMFile):
205
204
 
206
205
  def _validate(self):
207
206
  try:
208
- self._xsdschema.assert_(self._xmltree)
207
+ self._xsdschema.assert_(self._xmltree) # noqa: PT009
209
208
  except AssertionError as err:
210
- msg = (
211
- f"XML Validation Error for {repr(self)}:\n"
212
- f" {err.args[0].replace(self._ns, '')}"
213
- )
209
+ msg = f"XML Validation Error for {self!r}:\n {err.args[0].replace(self._ns, '')}"
214
210
  raise ValueError(msg) from err
215
211
 
216
212
  def _recursive_update_xml( # noqa: C901, PLR0912
@@ -224,7 +220,8 @@ class XML2D(FMFile):
224
220
 
225
221
  for key, item in new_dict.items():
226
222
  if key in self._multi_value_keys and not isinstance(item, list):
227
- raise Exception(f"Element: '{key}' must be added as list")
223
+ msg = f"Element: '{key}' must be added as list"
224
+ raise Exception(msg)
228
225
  if parent_key == "ROOT":
229
226
  parent = self._xmltree.getroot()
230
227
  else:
@@ -294,7 +291,8 @@ class XML2D(FMFile):
294
291
  from_list=False,
295
292
  ):
296
293
  if add_key in self._multi_value_keys and not isinstance(add_item, list) and not from_list:
297
- raise Exception(f"Element: '{add_key}' must be added as list")
294
+ msg = f"Element: '{add_key}' must be added as list"
295
+ raise Exception(msg)
298
296
  if isinstance(add_item, dict):
299
297
  new_element = etree.SubElement(parent, f"{self._ns}{add_key}")
300
298
  for key, item in add_item.items():
@@ -509,12 +507,11 @@ class XML2D(FMFile):
509
507
  self.range_settings = range_settings if range_settings else {}
510
508
 
511
509
  if self._filepath is None:
512
- raise UserWarning(
513
- "xml2D must be saved to a specific filepath before simulate() can be called.",
514
- )
510
+ msg = "xml2D must be saved to a specific filepath before simulate() can be called."
511
+ raise UserWarning(msg)
515
512
  if precision.upper() == "DEFAULT":
516
513
  precision = "SINGLE" # defaults to single precision
517
- for _, domain in self.domains.items():
514
+ for domain in self.domains.values():
518
515
  if domain["run_data"].get("double_precision") == "required":
519
516
  precision = "DOUBLE"
520
517
  break
@@ -525,14 +522,13 @@ class XML2D(FMFile):
525
522
  else:
526
523
  _enginespath = enginespath
527
524
  if not Path(_enginespath).exists():
528
- raise Exception(
529
- f"Flood Modeller non-default engine path not found! {str(_enginespath)}",
530
- )
525
+ msg = f"Flood Modeller non-default engine path not found! {_enginespath!s}"
526
+ raise Exception(msg)
531
527
 
532
528
  # checking if all schemes used are fast, if so will use FAST.exe
533
529
  # TODO: Add in option to choose to use or not to use if you can
534
530
  is_fast = True
535
- for _, domain in self.domains.items():
531
+ for domain in self.domains.values():
536
532
  if domain["run_data"]["scheme"] != "FAST":
537
533
  is_fast = False
538
534
  break
@@ -545,7 +541,8 @@ class XML2D(FMFile):
545
541
  isis2d_fp = str(Path(_enginespath, "ISIS2d_DP.exe"))
546
542
 
547
543
  if not Path(isis2d_fp).exists():
548
- raise Exception(f"Flood Modeller engine not found! Expected location: {isis2d_fp}")
544
+ msg = f"Flood Modeller engine not found! Expected location: {isis2d_fp}"
545
+ raise Exception(msg)
549
546
 
550
547
  console_output = console_output.lower()
551
548
  run_command = (
@@ -556,7 +553,7 @@ class XML2D(FMFile):
556
553
  if method.upper() == "WAIT":
557
554
  print("Executing simulation ... ")
558
555
  # execute simulation
559
- process = Popen(run_command, cwd=os.path.dirname(self._filepath), stdout=stdout)
556
+ process = Popen(run_command, cwd=Path(self._filepath).parent, stdout=stdout)
560
557
 
561
558
  # progress bar based on log files:
562
559
  if console_output == "simple":
@@ -573,7 +570,7 @@ class XML2D(FMFile):
573
570
  elif method.upper() == "RETURN_PROCESS":
574
571
  print("Executing simulation ...")
575
572
  # execute simulation
576
- return Popen(run_command, cwd=os.path.dirname(self._filepath), stdout=stdout)
573
+ return Popen(run_command, cwd=Path(self._filepath).parent, stdout=stdout)
577
574
 
578
575
  return None
579
576
 
@@ -584,7 +581,8 @@ class XML2D(FMFile):
584
581
  floodmodeller_api.LF2 class object
585
582
  """
586
583
  if not self._log_path.exists():
587
- raise FileNotFoundError("Log file (LF2) not found")
584
+ msg = "Log file (LF2) not found"
585
+ raise FileNotFoundError(msg)
588
586
 
589
587
  return LF2(self._log_path)
590
588