floodmodeller-api 0.5.1__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 (86) hide show
  1. floodmodeller_api/__init__.py +10 -0
  2. floodmodeller_api/_base.py +29 -20
  3. floodmodeller_api/backup.py +12 -10
  4. floodmodeller_api/dat.py +162 -91
  5. floodmodeller_api/diff.py +1 -1
  6. floodmodeller_api/hydrology_plus/hydrology_plus_export.py +1 -1
  7. floodmodeller_api/ied.py +2 -4
  8. floodmodeller_api/ief.py +29 -17
  9. floodmodeller_api/ief_flags.py +1 -1
  10. floodmodeller_api/inp.py +4 -6
  11. floodmodeller_api/logs/lf.py +18 -12
  12. floodmodeller_api/logs/lf_helpers.py +2 -2
  13. floodmodeller_api/logs/lf_params.py +1 -5
  14. floodmodeller_api/mapping.py +9 -2
  15. floodmodeller_api/test/test_conveyance.py +9 -4
  16. floodmodeller_api/test/test_dat.py +166 -18
  17. floodmodeller_api/test/test_data/EX18_DAT_expected.json +164 -144
  18. floodmodeller_api/test/test_data/EX3_DAT_expected.json +6 -2
  19. floodmodeller_api/test/test_data/EX6_DAT_expected.json +12 -46
  20. floodmodeller_api/test/test_data/encoding_test_cp1252.dat +1081 -0
  21. floodmodeller_api/test/test_data/encoding_test_utf8.dat +1081 -0
  22. floodmodeller_api/test/test_data/integrated_bridge/AR_NoSP_NoBl_2O_NO_OneFRC.ied +33 -0
  23. floodmodeller_api/test/test_data/integrated_bridge/AR_vSP_25pc_1O.ied +32 -0
  24. floodmodeller_api/test/test_data/integrated_bridge/PL_vSP_25pc_1O.ied +34 -0
  25. floodmodeller_api/test/test_data/integrated_bridge/SBTwoFRCsStaggered.IED +32 -0
  26. floodmodeller_api/test/test_data/integrated_bridge/US_NoSP_NoBl_OR_RN.ied +28 -0
  27. floodmodeller_api/test/test_data/integrated_bridge/US_SP_NoBl_OR_frc_PT2-5_RN.ied +34 -0
  28. floodmodeller_api/test/test_data/integrated_bridge/US_fSP_NoBl_1O.ied +30 -0
  29. floodmodeller_api/test/test_data/integrated_bridge/US_nSP_NoBl_1O.ied +49 -0
  30. floodmodeller_api/test/test_data/integrated_bridge/US_vSP_NoBl_2O_Para.ied +35 -0
  31. floodmodeller_api/test/test_data/integrated_bridge.dat +40 -0
  32. floodmodeller_api/test/test_data/network.ied +2 -2
  33. floodmodeller_api/test/test_data/network_dat_expected.json +141 -243
  34. floodmodeller_api/test/test_data/network_ied_expected.json +2 -2
  35. floodmodeller_api/test/test_data/network_with_comments.ied +2 -2
  36. floodmodeller_api/test/test_ied.py +1 -1
  37. floodmodeller_api/test/test_ief.py +10 -2
  38. floodmodeller_api/test/test_integrated_bridge.py +159 -0
  39. floodmodeller_api/test/test_json.py +9 -3
  40. floodmodeller_api/test/test_logs_lf.py +45 -24
  41. floodmodeller_api/test/test_river.py +1 -1
  42. floodmodeller_api/test/test_toolbox_structure_log.py +0 -1
  43. floodmodeller_api/test/test_xml2d.py +5 -5
  44. floodmodeller_api/to_from_json.py +1 -1
  45. floodmodeller_api/tool.py +3 -5
  46. floodmodeller_api/toolbox/model_build/add_siltation_definition.py +1 -1
  47. floodmodeller_api/toolbox/model_build/structure_log/structure_log.py +12 -8
  48. floodmodeller_api/units/__init__.py +15 -0
  49. floodmodeller_api/units/_base.py +73 -10
  50. floodmodeller_api/units/_helpers.py +343 -0
  51. floodmodeller_api/units/boundaries.py +59 -71
  52. floodmodeller_api/units/comment.py +1 -1
  53. floodmodeller_api/units/conduits.py +57 -54
  54. floodmodeller_api/units/connectors.py +112 -0
  55. floodmodeller_api/units/controls.py +107 -0
  56. floodmodeller_api/units/iic.py +2 -9
  57. floodmodeller_api/units/losses.py +42 -42
  58. floodmodeller_api/units/sections.py +40 -43
  59. floodmodeller_api/units/structures.py +360 -530
  60. floodmodeller_api/units/units.py +25 -26
  61. floodmodeller_api/units/unsupported.py +5 -7
  62. floodmodeller_api/units/variables.py +2 -2
  63. floodmodeller_api/urban1d/_base.py +7 -8
  64. floodmodeller_api/urban1d/conduits.py +11 -21
  65. floodmodeller_api/urban1d/general_parameters.py +1 -1
  66. floodmodeller_api/urban1d/junctions.py +7 -11
  67. floodmodeller_api/urban1d/losses.py +13 -17
  68. floodmodeller_api/urban1d/outfalls.py +16 -21
  69. floodmodeller_api/urban1d/raingauges.py +3 -9
  70. floodmodeller_api/urban1d/subsections.py +3 -4
  71. floodmodeller_api/urban1d/xsections.py +11 -15
  72. floodmodeller_api/util.py +7 -4
  73. floodmodeller_api/validation/parameters.py +7 -3
  74. floodmodeller_api/validation/urban_parameters.py +1 -4
  75. floodmodeller_api/validation/validation.py +9 -4
  76. floodmodeller_api/version.py +1 -1
  77. floodmodeller_api/xml2d.py +9 -11
  78. floodmodeller_api/xml2d_template.py +1 -1
  79. floodmodeller_api/zz.py +7 -6
  80. {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.dist-info}/LICENSE.txt +1 -1
  81. {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.dist-info}/METADATA +11 -3
  82. {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.dist-info}/RECORD +85 -70
  83. {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.dist-info}/WHEEL +1 -1
  84. floodmodeller_api/units/helpers.py +0 -121
  85. {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.dist-info}/entry_points.txt +0 -0
  86. {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.dist-info}/top_level.txt +0 -0
@@ -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.
@@ -14,19 +14,21 @@ 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
+ import logging
18
+
17
19
  import pandas as pd
18
20
 
19
21
  from floodmodeller_api.validation import _validate_unit
20
22
 
21
23
  from ._base import Unit
22
- from .helpers import (
23
- _to_float,
24
- _to_int,
25
- _to_str,
24
+ from ._helpers import (
26
25
  join_10_char,
27
26
  join_n_char_ljust,
28
27
  split_10_char,
29
28
  split_n_char,
29
+ to_float,
30
+ to_int,
31
+ to_str,
30
32
  )
31
33
 
32
34
 
@@ -179,93 +181,94 @@ class CONDUIT(Unit):
179
181
 
180
182
  def _read(self, c_block): # noqa: PLR0915
181
183
  """Function to read a given CONDUIT block and store data as class attributes"""
182
- self._subtype = c_block[1].split(" ")[0].strip()
184
+ self._subtype = self._get_first_word(c_block[1])
183
185
  # Extends label line to be correct length before splitting to pick up blank labels
184
186
  labels = split_n_char(f"{c_block[2]:<{2*self._label_len}}", self._label_len)
185
187
  self.name = labels[0]
186
188
  self.spill = labels[1]
187
- self.comment = c_block[0].replace("CONDUIT", "").strip()
189
+ self.comment = self._remove_unit_name(c_block[0])
188
190
 
189
191
  # Read CIRCULAR type unit
190
192
  if self._subtype == "CIRCULAR":
191
193
  # Read Params
192
- self.dist_to_next = _to_float(split_10_char(c_block[3])[0])
194
+ self.dist_to_next = to_float(split_10_char(c_block[3])[0])
193
195
  self.friction_eq = c_block[4].strip()
194
196
  params = split_10_char(f"{c_block[5]:<80}")
195
- self.invert = _to_float(params[0])
196
- self.diameter = _to_float(params[1])
197
- self.use_bottom_slot = _to_str(params[2], "GLOBAL")
198
- self.bottom_slot_dist = _to_float(params[3])
199
- self.bottom_slot_depth = _to_float(params[4])
200
- self.use_top_slot = _to_str(params[5], "GLOBAL")
201
- self.top_slot_dist = _to_float(params[6])
202
- self.top_slot_depth = _to_float(params[7])
197
+ self.invert = to_float(params[0])
198
+ self.diameter = to_float(params[1])
199
+ self.use_bottom_slot = to_str(params[2], "GLOBAL")
200
+ self.bottom_slot_dist = to_float(params[3])
201
+ self.bottom_slot_depth = to_float(params[4])
202
+ self.use_top_slot = to_str(params[5], "GLOBAL")
203
+ self.top_slot_dist = to_float(params[6])
204
+ self.top_slot_depth = to_float(params[7])
203
205
  friction_params = split_10_char(f"{c_block[6]:<20}")
204
- self.friction_below_axis = _to_float(friction_params[0])
205
- self.friction_above_axis = _to_float(friction_params[1])
206
+ self.friction_below_axis = to_float(friction_params[0])
207
+ self.friction_above_axis = to_float(friction_params[1])
206
208
 
207
209
  elif self._subtype == "RECTANGULAR":
208
210
  # Read Params
209
- self.dist_to_next = _to_float(split_10_char(c_block[3])[0])
211
+ self.dist_to_next = to_float(split_10_char(c_block[3])[0])
210
212
  self.friction_eq = c_block[4].strip()
211
213
  params = split_10_char(f"{c_block[5]:<90}")
212
- self.invert = _to_float(params[0])
213
- self.width = _to_float(params[1])
214
- self.height = _to_float(params[2])
215
- self.use_bottom_slot = _to_str(params[3], "GLOBAL")
216
- self.bottom_slot_dist = _to_float(params[4])
217
- self.bottom_slot_depth = _to_float(params[5])
218
- self.use_top_slot = _to_str(params[6], "GLOBAL")
219
- self.top_slot_dist = _to_float(params[7])
220
- self.top_slot_depth = _to_float(params[8])
214
+ self.invert = to_float(params[0])
215
+ self.width = to_float(params[1])
216
+ self.height = to_float(params[2])
217
+ self.use_bottom_slot = to_str(params[3], "GLOBAL")
218
+ self.bottom_slot_dist = to_float(params[4])
219
+ self.bottom_slot_depth = to_float(params[5])
220
+ self.use_top_slot = to_str(params[6], "GLOBAL")
221
+ self.top_slot_dist = to_float(params[7])
222
+ self.top_slot_depth = to_float(params[8])
221
223
  friction_params = split_10_char(f"{c_block[6]:<30}")
222
- self.friction_on_invert = _to_float(friction_params[0])
223
- self.friction_on_walls = _to_float(friction_params[1])
224
- self.friction_on_soffit = _to_float(friction_params[2])
224
+ self.friction_on_invert = to_float(friction_params[0])
225
+ self.friction_on_walls = to_float(friction_params[1])
226
+ self.friction_on_soffit = to_float(friction_params[2])
225
227
 
226
228
  elif self._subtype in ("SPRUNG", "SPRUNGARCH"):
227
- self.dist_to_next = _to_float(split_10_char(c_block[3])[0])
228
- self.equation = _to_str(c_block[4], "MANNING")
229
+ self.dist_to_next = to_float(split_10_char(c_block[3])[0])
230
+ self.equation = to_str(c_block[4], "MANNING")
229
231
  params = split_10_char(f"{c_block[5]:<100}")
230
- self.elevation_invert = _to_float(params[0])
231
- self.width = _to_float(params[1])
232
- self.height_springing = _to_float(params[2])
233
- self.height_crown = _to_float(params[3])
234
- self.use_bottom_slot = _to_str(params[4], "GLOBAL")
235
- self.bottom_slot_dist = _to_float(params[5])
236
- self.bottom_slot_depth = _to_float(params[6])
237
- self.use_top_slot = _to_str(params[7], "GLOBAL")
238
- self.top_slot_dist = _to_float(params[8])
239
- self.top_slot_depth = _to_float(params[9])
232
+ self.elevation_invert = to_float(params[0])
233
+ self.width = to_float(params[1])
234
+ self.height_springing = to_float(params[2])
235
+ self.height_crown = to_float(params[3])
236
+ self.use_bottom_slot = to_str(params[4], "GLOBAL")
237
+ self.bottom_slot_dist = to_float(params[5])
238
+ self.bottom_slot_depth = to_float(params[6])
239
+ self.use_top_slot = to_str(params[7], "GLOBAL")
240
+ self.top_slot_dist = to_float(params[8])
241
+ self.top_slot_depth = to_float(params[9])
240
242
  friction_params = split_10_char(f"{c_block[6]:<30}")
241
- self.friction_on_invert = _to_float(friction_params[0])
242
- self.friction_on_walls = _to_float(friction_params[1])
243
- self.friction_on_soffit = _to_float(friction_params[2])
243
+ self.friction_on_invert = to_float(friction_params[0])
244
+ self.friction_on_walls = to_float(friction_params[1])
245
+ self.friction_on_soffit = to_float(friction_params[2])
244
246
 
245
247
  elif self._subtype == "SECTION":
246
- self.dist_to_next = _to_float(split_10_char(c_block[3])[0])
247
- end_index = 5 + _to_int(c_block[4])
248
+ self.dist_to_next = to_float(split_10_char(c_block[3])[0])
249
+ end_index = 5 + to_int(c_block[4])
248
250
  x = []
249
251
  y = []
250
252
  friction = []
251
253
  for i in range(5, end_index):
252
254
  row_data = split_10_char(f"{c_block[i]:<30}")
253
- x.append(_to_float(row_data[0]))
254
- y.append(_to_float(row_data[1]))
255
- friction.append(_to_float(row_data[2]))
255
+ x.append(to_float(row_data[0]))
256
+ y.append(to_float(row_data[1]))
257
+ friction.append(to_float(row_data[2]))
256
258
  self.coords = pd.DataFrame({"x": x, "y": y, "cw_friction": friction})
257
259
 
258
260
  else:
259
261
  # This else block is triggered for conduit subtypes which aren't yet supported, and just keeps the '_block' in it's raw state to write back.
260
- print(
261
- f'This Conduit sub-type: "{self._subtype}" is currently unsupported for reading/editing',
262
+ logging.warning(
263
+ "This Conduit sub-type: '%s' is currently unsupported for reading/editing",
264
+ self._subtype,
262
265
  )
263
266
  self._raw_block = c_block
264
267
 
265
268
  def _write(self):
266
269
  """Function to write a valid CONDUIT block"""
267
270
  _validate_unit(self) # Function to check the params are valid for CONDUIT unit
268
- header = "CONDUIT " + self.comment
271
+ header = self._create_header()
269
272
  labels = join_n_char_ljust(self._label_len, self.name, self.spill)
270
273
  c_block = [header, self._subtype, labels]
271
274
 
@@ -0,0 +1,112 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from floodmodeller_api.validation import _validate_unit
6
+
7
+ from ._base import Unit
8
+ from ._helpers import (
9
+ join_12_char_ljust,
10
+ read_lateral_data,
11
+ split_12_char,
12
+ to_int,
13
+ write_dataframe,
14
+ )
15
+
16
+ if TYPE_CHECKING:
17
+ import pandas as pd
18
+
19
+
20
+ class JUNCTION(Unit):
21
+ """Class to hold and process JUNCTION unit type
22
+
23
+ Args:
24
+ comment (str, optional): Comment included in unit.
25
+ subtype (str, optional): Defines the type of junction unit (*Should not be changed*)
26
+ labels (str, optional): Unlimited number of labels, the first of which is the name.
27
+
28
+ Returns:
29
+ JUNCTION: Flood Modeller JUNCTION Unit class object"""
30
+
31
+ _unit = "JUNCTION"
32
+
33
+ def _read(self, block: list[str]) -> None:
34
+ self.comment = self._remove_unit_name(block[0])
35
+ self._subtype = self._get_first_word(block[1])
36
+ self.labels = split_12_char(block[2])
37
+
38
+ self.name = self.labels[0]
39
+
40
+ def _write(self) -> list[str]:
41
+ _validate_unit(self)
42
+ return [
43
+ self._create_header(),
44
+ self.subtype, # type: ignore
45
+ join_12_char_ljust(*self.labels).rstrip(),
46
+ ]
47
+
48
+ def _create_from_blank(
49
+ self,
50
+ comment: str = "",
51
+ subtype: str = "OPEN",
52
+ labels: list[str] | None = None,
53
+ ) -> None:
54
+ self.comment = comment
55
+ self._subtype = subtype
56
+ self.labels = labels if labels is not None else []
57
+ self.name = self.labels[0]
58
+
59
+
60
+ class LATERAL(Unit):
61
+ """Class to hold and process LATERAL unit type
62
+
63
+ Args:
64
+ name (str, optional): Unit name.
65
+ comment (str, optional): Comment included in unit.
66
+ subtype (str, optional): Defines the type of lateral unit (*Should not be changed*)
67
+ weight_factor (str, optional): Corresponding weight factors or user-defined area for
68
+ each receiving unit
69
+ data (pandas.DataFrame): Dataframe object containing all the reservoir section data.
70
+ Columns are ``'Node Label', 'Custom Weight Factor', 'Use Weight Factor'``
71
+
72
+ Returns:
73
+ LATERAL: Flood Modeller LATERAL Unit class object"""
74
+
75
+ _unit = "LATERAL"
76
+
77
+ def _read(self, block: list[str]) -> None:
78
+ self.comment = self._remove_unit_name(block[0])
79
+ self.name = block[1]
80
+ self.weight_factor = block[2]
81
+ self.no_units = to_int(block[3])
82
+ self.data = read_lateral_data(block[4:])
83
+ self.labels = list(self.data["Node Label"])
84
+
85
+ def _write(self) -> list[str]:
86
+ _validate_unit(self)
87
+ return [
88
+ self._create_header(),
89
+ self.name, # type: ignore
90
+ self.weight_factor,
91
+ *write_dataframe(self.no_units, self.data, n=12),
92
+ ]
93
+
94
+ def _create_from_blank(
95
+ self,
96
+ name: str = "new_junction",
97
+ comment: str = "",
98
+ subtype: str = "OPEN",
99
+ weight_factor: str = "REACH",
100
+ data: pd.DataFrame | None = None,
101
+ ) -> None:
102
+ self.name = name
103
+ self.comment = comment
104
+ self._subtype = subtype
105
+ self.weight_factor = weight_factor
106
+
107
+ self.data = self._enforce_dataframe(
108
+ data,
109
+ ("Node Label", "Custom Weight Factor", "Use Weight Factor"),
110
+ )
111
+ self.no_units = len(self.data)
112
+ self.labels = list(self.data["Node Label"])
@@ -0,0 +1,107 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import TYPE_CHECKING
4
+
5
+ from floodmodeller_api.validation import _validate_unit
6
+
7
+ from ._base import Unit
8
+ from ._helpers import (
9
+ join_10_char,
10
+ join_12_char_ljust,
11
+ read_reservoir_data,
12
+ split_10_char,
13
+ split_12_char,
14
+ to_float,
15
+ to_int,
16
+ write_dataframe,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ import pandas as pd
21
+
22
+
23
+ class RESERVOIR(Unit):
24
+ """Class to hold and process RESERVOIR unit type
25
+
26
+ Args:
27
+ name (str, optional): Unit name.
28
+ comment (str, optional): Comment included in unit.
29
+ labels (str, optional): Unlimited number of labels - not including first label (name).
30
+ easting (float, optional): Easting coordinate of reservoir reference point (not used in computations).
31
+ northing (float, optional): Northing coordinate of reservoir reference point (not used in computations).
32
+ runoff_factor (float, optional): Rainfall runoff factor.
33
+ lateral_inflow_labels (list[str], optional): Lateral inflow labels (up to 4).
34
+ data (pandas.DataFrame): Dataframe object containing all the reservoir section data.
35
+ Columns are ``'Elevation','Plan Area'``
36
+
37
+ Returns:
38
+ RESERVOIR: Flood Modeller RESERVOIR Unit class object"""
39
+
40
+ _unit = "RESERVOIR"
41
+
42
+ def _read(self, block: list[str]) -> None:
43
+ self._revision, self.comment = self._get_revision_and_comment(block[0])
44
+
45
+ self.labels = split_12_char(block[1])
46
+ self.name = self.labels[0]
47
+
48
+ if self._revision == 1:
49
+ self.lateral_inflow_labels = split_12_char(block[2])
50
+ idx = 3
51
+
52
+ lines = split_10_char(f"{block[-1]:<30}")
53
+ self.easting = to_float(lines[0])
54
+ self.northing = to_float(lines[1])
55
+ self.runoff = to_float(lines[2])
56
+ else:
57
+ idx = 2
58
+
59
+ self.no_rows = to_int(block[idx])
60
+ start_idx = idx + 1
61
+ end_idx = start_idx + self.no_rows
62
+ self.data = read_reservoir_data(block[start_idx:end_idx])
63
+
64
+ def _write(self) -> list[str]:
65
+ _validate_unit(self)
66
+ rev1_a = (
67
+ [join_12_char_ljust(*self.lateral_inflow_labels).rstrip()]
68
+ if self._revision == 1
69
+ else []
70
+ )
71
+ rev1_b = (
72
+ [join_10_char(self.easting, self.northing, self.runoff)] if self._revision == 1 else []
73
+ )
74
+ return [
75
+ self._create_header(include_revision=self._revision is not None),
76
+ join_12_char_ljust(*self.labels).rstrip(),
77
+ *rev1_a,
78
+ *write_dataframe(self.no_rows, self.data),
79
+ *rev1_b,
80
+ ]
81
+
82
+ def _create_from_blank( # noqa: PLR0913 (need that many)
83
+ self,
84
+ name: str = "new_reservoir",
85
+ comment: str = "",
86
+ subtype: str = "OPEN",
87
+ labels: list[str] | None = None,
88
+ easting: float = 0.0,
89
+ northing: float = 0.0,
90
+ runoff: float = 0.0,
91
+ lateral_inflow_labels: list[str] | None = None,
92
+ data: pd.DataFrame | None = None,
93
+ ) -> None:
94
+ self.easting = easting
95
+ self.northing = northing
96
+ self.runoff = runoff
97
+ self.name = name
98
+ self.comment = comment
99
+ self._subtype = subtype
100
+ self._revision = 1
101
+ self.labels = labels if labels is not None else []
102
+ self.lateral_inflow_labels = (
103
+ lateral_inflow_labels if lateral_inflow_labels is not None else []
104
+ )
105
+
106
+ self.data = self._enforce_dataframe(data, ("Elevation", "Plan Area"))
107
+ self.no_rows = len(self.data)
@@ -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.
@@ -18,7 +18,7 @@ import pandas as pd
18
18
 
19
19
  from ..diff import check_item_with_dataframe_equal
20
20
  from ..to_from_json import Jsonable
21
- from .helpers import join_10_char, split_10_char
21
+ from ._helpers import join_10_char, split_10_char
22
22
 
23
23
  # Initial Conditions Class
24
24
 
@@ -65,15 +65,8 @@ class IIC(Jsonable):
65
65
  float(z),
66
66
  ],
67
67
  )
68
- # AL is this storing the values as strings?
69
68
  self.data = pd.DataFrame(data_list, columns=header)
70
- # JP Yes
71
- # AL If it does, would it worth making it store the values instead?
72
- # JP Yes I'll do that, only downside is that the updated values may not match notation
73
- # of original even if no changes. (i.e 2.0 -> 2.00 or 2. -> 2.00)
74
69
 
75
- # AL Is this only to transform the table of data into a string-like array?
76
- # JP Yes it just transforms the dataframe back into valid DAT format
77
70
  def _write(self):
78
71
  ic_block = [
79
72
  "INITIAL CONDITIONS",
@@ -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.
@@ -14,20 +14,21 @@ 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
+ import logging
18
+
17
19
  import pandas as pd
18
20
 
19
21
  from floodmodeller_api.validation import _validate_unit
20
22
 
21
23
  from ._base import Unit
22
- from .helpers import (
23
- _to_data_list,
24
- _to_float,
25
- _to_int,
26
- _to_str,
24
+ from ._helpers import (
27
25
  join_10_char,
28
26
  join_n_char_ljust,
29
27
  split_10_char,
30
28
  split_n_char,
29
+ to_data_list,
30
+ to_float,
31
+ to_str,
31
32
  )
32
33
 
33
34
 
@@ -76,8 +77,8 @@ class CULVERT(Unit):
76
77
  """Function to read a given CULVERT block and store data as class attributes"""
77
78
 
78
79
  # Extract common attributes
79
- self._subtype = block[1].split(" ")[0].strip()
80
- self.comment = block[0].replace("CULVERT", "").strip()
80
+ self._subtype = self._get_first_word(block[1])
81
+ self.comment = self._remove_unit_name(block[0])
81
82
  labels = split_n_char(f"{block[2]:<{4*self._label_len}}", self._label_len)
82
83
  self.name = labels[0]
83
84
  self.ds_label = labels[1]
@@ -91,44 +92,43 @@ class CULVERT(Unit):
91
92
 
92
93
  # Read first set of general parameters
93
94
  params = split_10_char(f"{block[3]:<60}")
94
- self.k = _to_float(params[0], 0.0)
95
- self.m = _to_float(params[1], 0.0)
96
- self.c = _to_float(params[2], 0.0)
97
- self.y = _to_float(params[3], 0.0)
98
- self.ki = _to_float(params[4], 0.0)
99
- self.type_code = _to_str(params[5], "A")
95
+ self.k = to_float(params[0], 0.0)
96
+ self.m = to_float(params[1], 0.0)
97
+ self.c = to_float(params[2], 0.0)
98
+ self.y = to_float(params[3], 0.0)
99
+ self.ki = to_float(params[4], 0.0)
100
+ self.type_code = to_str(params[5], "A")
100
101
 
101
102
  # Read trash screen and remaining general parameters
102
103
  params1 = split_10_char(f"{block[4]:<70}")
103
- self.screen_width = _to_float(params1[0], 0.0)
104
- self.bar_proportion = _to_float(params1[1], 0.0)
105
- self.debris_proportion = _to_float(params1[2], 0.0)
106
- self.loss_coefficient = _to_float(params1[3], 0.0)
107
- self.reverse_flow_mode = _to_str(params1[4], "CALCULATED", check_float=True)
108
- self.headloss_type = _to_str(params1[5], "TOTAL")
109
- self.max_screen_height = _to_float(params1[6], 0.0)
104
+ self.screen_width = to_float(params1[0], 0.0)
105
+ self.bar_proportion = to_float(params1[1], 0.0)
106
+ self.debris_proportion = to_float(params1[2], 0.0)
107
+ self.loss_coefficient = to_float(params1[3], 0.0)
108
+ self.reverse_flow_mode = to_str(params1[4], "CALCULATED", check_float=True)
109
+ self.headloss_type = to_str(params1[5], "TOTAL")
110
+ self.max_screen_height = to_float(params1[6], 0.0)
110
111
 
111
112
  elif self.subtype == "OUTLET":
112
113
  params = split_10_char(f"{block[3]:<30}")
113
- self.loss_coefficient = _to_float(params[0], 1.0)
114
- self.reverse_flow_mode = _to_str(params[1], "CALCULATED")
115
- self.headloss_type = _to_str(params[2], "TOTAL")
114
+ self.loss_coefficient = to_float(params[0], 1.0)
115
+ self.reverse_flow_mode = to_str(params[1], "CALCULATED")
116
+ self.headloss_type = to_str(params[2], "TOTAL")
116
117
 
117
118
  else:
118
- # This else block is triggered for culvert subtypes which aren't yet supported, and just keeps the '_block' in it's raw state to write back.
119
- print(
120
- f'This Culvert sub-type: "{self.subtype}" is currently unsupported for reading/editing',
119
+ # This else block is triggered for culvert subtypes which aren't yet supported, and just keeps the '_block' in its raw state to write back.
120
+ logging.warning(
121
+ "This Culvert sub-type: '%s' is currently unsupported for reading/editing",
122
+ self.subtype,
121
123
  )
122
124
  self._raw_block = block
123
125
 
124
- # TODO: Create from blank. Not supported currently as CULVERT has multiple subtypes
125
-
126
126
  def _write(self):
127
127
  """Function to write a valid CULVERT block"""
128
128
 
129
129
  _validate_unit(self)
130
130
 
131
- header = "CULVERT " + self.comment
131
+ header = self._create_header()
132
132
  labels = join_n_char_ljust(
133
133
  self._label_len,
134
134
  self.name,
@@ -192,9 +192,7 @@ class BLOCKAGE(Unit):
192
192
  """Function to read a given BLOCKAGE block and store data as class attributes"""
193
193
 
194
194
  # Extract comment and revision number
195
- b = block[0].replace("BLOCKAGE #revision#", " ").strip()
196
- self._revision = _to_int(b[0], 1)
197
- self.comment = b[1:].strip()
195
+ self._revision, self.comment = self._get_revision_and_comment(block[0])
198
196
 
199
197
  # Extract labels
200
198
  labels = split_n_char(f"{block[1]:<{5*self._label_len}}", self._label_len)
@@ -206,25 +204,27 @@ class BLOCKAGE(Unit):
206
204
 
207
205
  # Extract inlet and outlet loss coefficients
208
206
  params = split_10_char(f"{block[2]:<20}")
209
- self.inlet_loss = _to_float(params[0], 1.5)
210
- self.outlet_loss = _to_float(params[1], 1.0)
207
+ self.inlet_loss = to_float(params[0], 1.5)
208
+ self.outlet_loss = to_float(params[1], 1.0)
211
209
 
212
210
  # Extract blockage timeseries parameters
213
211
  params1 = split_10_char(f"{block[3]:<40}")
214
212
  self.nrows = int(params1[0])
215
- self.timeoffset = _to_float(params1[1])
213
+ self.timeoffset = to_float(params1[1])
216
214
 
217
- self.timeunit = _to_str(params1[2], "HOURS", check_float=True)
215
+ self.timeunit = to_str(params1[2], "HOURS", check_float=True)
218
216
  if self.timeunit == "DATE":
219
- self.timeunit = "DATES" # Parameter value updated to 'DATES' for consistency with other unit types. 'DATE' and 'DATES' both accepted for blockage unit ONLY
217
+ self.timeunit = "DATES"
218
+ # Parameter value updated to 'DATES' for consistency with other unit types.
219
+ # 'DATE' and 'DATES' both accepted for blockage unit ONLY
220
220
 
221
- self.extendmethod = _to_str(params1[3], "NOEXTEND")
221
+ self.extendmethod = to_str(params1[3], "NOEXTEND")
222
222
 
223
223
  # Extract blockage to timeseries
224
224
  data_list = (
225
- _to_data_list(block[4:], num_cols=2, date_col=0)
225
+ to_data_list(block[4:], num_cols=2, date_col=0)
226
226
  if self.timeunit == "DATES"
227
- else _to_data_list(block[4:], num_cols=2)
227
+ else to_data_list(block[4:], num_cols=2)
228
228
  ) # Enforced two columns as Flood Modeller saves old parameters when using DATES (also to avoid extra 'HOURS' bug)
229
229
 
230
230
  self.data = pd.DataFrame(data_list, columns=["Time", "Blockage"])
@@ -241,7 +241,7 @@ class BLOCKAGE(Unit):
241
241
  msg = f"Parameter error with {self!r} - blockage percentage must be between 0 and 1"
242
242
  raise ValueError(msg)
243
243
 
244
- header = f"BLOCKAGE #revision#{self._revision} {self.comment}"
244
+ header = self._create_header(include_revision=True)
245
245
  labels = join_n_char_ljust(
246
246
  self._label_len,
247
247
  self.name,