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
@@ -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,14 +20,13 @@ from floodmodeller_api.validation import _validate_unit
20
20
  from floodmodeller_api.validation.parameters import parameter_options
21
21
 
22
22
  from ._base import Unit
23
- from .helpers import (
24
- _to_data_list,
25
- _to_float,
26
- _to_int,
27
- _to_str,
23
+ from ._helpers import (
28
24
  join_10_char,
29
25
  join_n_char_ljust,
30
26
  split_10_char,
27
+ to_data_list,
28
+ to_float,
29
+ to_str,
31
30
  )
32
31
 
33
32
 
@@ -82,13 +81,6 @@ class QTBDY(Unit):
82
81
  }.items():
83
82
  setattr(self, param, val)
84
83
 
85
- # AL Since this is most likely used when building a model,
86
- # AL it would be nice to have a "name generator" to create
87
- # AL a unique name with each call (ie new_qtbdy_12345 then new_qtbdy_02508)
88
- # JP Yes this is a good idea, although I'm not sure how it would be best implemented
89
- # since any two instances of the class being initialised would be unaware of each other?
90
- # There is always the option to pass a name when constrcuting the class which may be better
91
-
92
84
  self.data = (
93
85
  data
94
86
  if isinstance(data, pd.Series)
@@ -98,21 +90,21 @@ class QTBDY(Unit):
98
90
  def _read(self, qtbdy_block):
99
91
  """Function to read a given QTBDY block and store data as class attributes"""
100
92
  self.name = qtbdy_block[1][: self._label_len].strip()
101
- self.comment = qtbdy_block[0].replace("QTBDY", "").strip()
93
+ self.comment = self._remove_unit_name(qtbdy_block[0])
102
94
  qtbdy_params = split_10_char(f"{qtbdy_block[2]:<90}")
103
95
  self.nrows = int(qtbdy_params[0])
104
- self.timeoffset = _to_float(qtbdy_params[1])
105
- self._something = _to_float(qtbdy_params[2])
106
- self.timeunit = _to_str(qtbdy_params[3], "HOURS", check_float=True)
107
- self.extendmethod = _to_str(qtbdy_params[4], "EXTEND")
108
- self.interpmethod = _to_str(qtbdy_params[5], "LINEAR")
109
- self.flowmultiplier = _to_float(qtbdy_params[6])
110
- self.minflow = _to_float(qtbdy_params[7])
111
- self.allow_override = _to_str(qtbdy_params[8], "OVERRIDE") # ''/OVERRIDE or NOOVERRIDE
96
+ self.timeoffset = to_float(qtbdy_params[1])
97
+ self._something = to_float(qtbdy_params[2])
98
+ self.timeunit = to_str(qtbdy_params[3], "HOURS", check_float=True)
99
+ self.extendmethod = to_str(qtbdy_params[4], "EXTEND")
100
+ self.interpmethod = to_str(qtbdy_params[5], "LINEAR")
101
+ self.flowmultiplier = to_float(qtbdy_params[6])
102
+ self.minflow = to_float(qtbdy_params[7])
103
+ self.allow_override = to_str(qtbdy_params[8], "OVERRIDE") # ''/OVERRIDE or NOOVERRIDE
112
104
  data_list = (
113
- _to_data_list(qtbdy_block[3:], date_col=1)
105
+ to_data_list(qtbdy_block[3:], date_col=1)
114
106
  if self.timeunit == "DATES"
115
- else _to_data_list(qtbdy_block[3:])
107
+ else to_data_list(qtbdy_block[3:])
116
108
  )
117
109
 
118
110
  self.data = pd.DataFrame(data_list, columns=["Flow", "Time"])
@@ -122,7 +114,7 @@ class QTBDY(Unit):
122
114
  def _write(self):
123
115
  """Function to write a valid QTBDY block"""
124
116
  _validate_unit(self) # Function to check the params are valid for QTBDY
125
- header = "QTBDY " + self.comment
117
+ header = self._create_header()
126
118
  name = self.name[: self._label_len]
127
119
  self.nrows = len(self.data)
128
120
 
@@ -195,18 +187,18 @@ class HTBDY(Unit):
195
187
  def _read(self, htbdy_block):
196
188
  """Function to read a given HTBDY block and store data as class attributes"""
197
189
  self.name = htbdy_block[1][: self._label_len].strip()
198
- self.comment = htbdy_block[0].replace("HTBDY", "").strip()
190
+ self.comment = self._remove_unit_name(htbdy_block[0])
199
191
  htbdy_params = split_10_char(f"{htbdy_block[2]:<50}")
200
192
  self.nrows = int(htbdy_params[0])
201
- self._something = _to_str(htbdy_params[1], "")
202
- self.timeunit = _to_str(htbdy_params[2], "HOURS", check_float=True)
203
- self.extendmethod = _to_str(htbdy_params[3], "EXTEND")
204
- self.interpmethod = _to_str(htbdy_params[4], "LINEAR")
193
+ self._something = to_str(htbdy_params[1], "")
194
+ self.timeunit = to_str(htbdy_params[2], "HOURS", check_float=True)
195
+ self.extendmethod = to_str(htbdy_params[3], "EXTEND")
196
+ self.interpmethod = to_str(htbdy_params[4], "LINEAR")
205
197
 
206
198
  data_list = (
207
- _to_data_list(htbdy_block[3:], date_col=1)
199
+ to_data_list(htbdy_block[3:], date_col=1)
208
200
  if self.timeunit == "DATES"
209
- else _to_data_list(htbdy_block[3:])
201
+ else to_data_list(htbdy_block[3:])
210
202
  )
211
203
 
212
204
  self.data = pd.DataFrame(data_list, columns=["Stage", "Time"])
@@ -221,7 +213,7 @@ class HTBDY(Unit):
221
213
  def _write(self):
222
214
  """Function to write a valid HTBDY block"""
223
215
  _validate_unit(self) # Function to check the params are valid for HTBDY
224
- header = "HTBDY " + self.comment
216
+ header = self._create_header()
225
217
  name = self.name
226
218
  self.nrows = len(self.data)
227
219
 
@@ -273,12 +265,12 @@ class QHBDY(Unit):
273
265
  def _read(self, qhbdy_block):
274
266
  """Function to read a given QHBDY block and store data as class attributes"""
275
267
  self.name = qhbdy_block[1][: self._label_len].strip()
276
- self.comment = qhbdy_block[0].replace("QHBDY", "").strip()
268
+ self.comment = self._remove_unit_name(qhbdy_block[0])
277
269
  qhbdy_params = split_10_char(f"{qhbdy_block[2]:<30}")
278
270
  self.nrows = int(qhbdy_params[0])
279
- self.interpmethod = _to_str(qhbdy_params[2], "LINEAR")
271
+ self.interpmethod = to_str(qhbdy_params[2], "LINEAR")
280
272
 
281
- data_list = _to_data_list(qhbdy_block[3:])
273
+ data_list = to_data_list(qhbdy_block[3:])
282
274
 
283
275
  self.data = pd.DataFrame(data_list, columns=["Flow", "Stage"])
284
276
  self.data = self.data.set_index("Stage")
@@ -287,7 +279,7 @@ class QHBDY(Unit):
287
279
  def _write(self):
288
280
  """Function to write a valid QHBDY block"""
289
281
  _validate_unit(self) # Function to check the params are valid for QHBDY
290
- header = "QHBDY " + self.comment
282
+ header = self._create_header()
291
283
  name = self.name
292
284
  self.nrows = len(self.data)
293
285
 
@@ -348,43 +340,40 @@ class REFHBDY(Unit):
348
340
  """Function to read a given REFHBDY block and store data as class attributes"""
349
341
  # line 1 & 2
350
342
  # Extract comment and revision number
351
- b = refhbdy_block[0].replace("REFHBDY #revision#", "").strip()
352
- self._revision = _to_int(b[0], 1)
353
- self.comment = b[1:].strip()
343
+ self._revision, self.comment = self._get_revision_and_comment(refhbdy_block[0])
354
344
  self.name = refhbdy_block[1][: self._label_len].strip()
355
345
 
356
346
  # line 3
357
347
  refhbdy_params1 = split_10_char(refhbdy_block[2])
358
- # TODO: work out what this is
359
- self._something = _to_float(refhbdy_params1[0])
348
+ self._unknown_param_1 = to_float(refhbdy_params1[0])
360
349
  self.easting = int(float(refhbdy_params1[1]))
361
350
  self.northing = int(float(refhbdy_params1[2]))
362
351
 
363
352
  # line 4
364
353
  refhbdy_opts = split_10_char(f"{refhbdy_block[3]:<90}")
365
- self.time_delay = _to_float(refhbdy_opts[0])
354
+ self.time_delay = to_float(refhbdy_opts[0])
366
355
  # SD / timestep must be odd interval
367
- self.timestep = _to_float(refhbdy_opts[1])
356
+ self.timestep = to_float(refhbdy_opts[1])
368
357
  # '' : Full hydrograph, 'pfonly' : peak flow, 'bfonly' : baseflow only
369
358
  self.sim_type = refhbdy_opts[2]
370
- self.scale_method = _to_str(refhbdy_opts[3], "SCALEFACT") # PEAKVALUE or SCALEFACT
371
- self.scale_value = _to_float(refhbdy_opts[4], 1.0)
372
- self.boundary_type = _to_str(refhbdy_opts[5], "HYDROGRAPH") # HYDROGRAPH or HYETOGRAPH
373
- self.scale_type = _to_str(refhbdy_opts[6], "FULL") # FULL or RUNOFF
374
- self.minflow = _to_float(refhbdy_opts[7])
359
+ self.scale_method = to_str(refhbdy_opts[3], "SCALEFACT") # PEAKVALUE or SCALEFACT
360
+ self.scale_value = to_float(refhbdy_opts[4], 1.0)
361
+ self.boundary_type = to_str(refhbdy_opts[5], "HYDROGRAPH") # HYDROGRAPH or HYETOGRAPH
362
+ self.scale_type = to_str(refhbdy_opts[6], "FULL") # FULL or RUNOFF
363
+ self.minflow = to_float(refhbdy_opts[7])
375
364
  self.allow_override = refhbdy_opts[8] # ''/OVERRIDE or NOOVERRIDE
376
365
 
377
366
  # line 5
378
367
  refhbdy_params2 = split_10_char(f"{refhbdy_block[4]:<60}")
379
- self.area = _to_float(refhbdy_params2[0])
368
+ self.area = to_float(refhbdy_params2[0])
380
369
  try:
381
370
  # Maintain SAAR as integer if already is, else use float
382
371
  self.saar = int(refhbdy_params2[1])
383
372
  except ValueError:
384
373
  self.saar = float(refhbdy_params2[1])
385
- self.urbext = _to_float(refhbdy_params2[2])
386
- self.season = _to_str(refhbdy_params2[3], "DEFAULT") # DEFAULT, SUMMER or WINTER
387
- self.calc_source = _to_str(refhbdy_params2[4], "DLL") # DLL or REPORT
374
+ self.urbext = to_float(refhbdy_params2[2])
375
+ self.season = to_str(refhbdy_params2[3], "DEFAULT") # DEFAULT, SUMMER or WINTER
376
+ self.calc_source = to_str(refhbdy_params2[4], "DLL") # DLL or REPORT
388
377
  self.use_urban_subdivisions = refhbdy_params2[5] != ""
389
378
  if self.use_urban_subdivisions:
390
379
  # Just keeping this raw for now as unlikely to be used.
@@ -399,37 +388,36 @@ class REFHBDY(Unit):
399
388
 
400
389
  # line 6
401
390
  rainfall_params1 = split_10_char(rainfall_params1)
402
- self.storm_area = _to_float(rainfall_params1[0])
403
- self.storm_duration = _to_float(rainfall_params1[1])
404
- # TODO: work out what this is
405
- self._something2 = _to_float(rainfall_params1[2])
391
+ self.storm_area = to_float(rainfall_params1[0])
392
+ self.storm_duration = to_float(rainfall_params1[1])
393
+ self._unknown_param_2 = to_float(rainfall_params1[2])
406
394
 
407
395
  # line 7
408
396
  self.rainfall_comment = rainfall_params2[20:]
409
397
  rainfall_params2 = split_10_char(rainfall_params2[:20])
410
398
  self.arf_method = rainfall_params2[1]
411
- self._something3 = rainfall_params2[0] # TODO: work out what this is
399
+ self._unknown_param_3 = rainfall_params2[0]
412
400
 
413
401
  # line 8
414
402
  rainfall_params3 = split_10_char(rainfall_params3)
415
- self.observed_rainfall_depth = _to_float(rainfall_params3[0])
416
- self.return_period = _to_float(rainfall_params3[1])
417
- self.arf = _to_float(rainfall_params3[2])
418
- self.ddf_c = _to_float(rainfall_params3[3])
419
- self.ddf_d1 = _to_float(rainfall_params3[4])
420
- self.ddf_d2 = _to_float(rainfall_params3[5])
421
- self.ddf_d3 = _to_float(rainfall_params3[6])
422
- self.ddf_e = _to_float(rainfall_params3[7])
423
- self.ddf_f = _to_float(rainfall_params3[8])
403
+ self.observed_rainfall_depth = to_float(rainfall_params3[0])
404
+ self.return_period = to_float(rainfall_params3[1])
405
+ self.arf = to_float(rainfall_params3[2])
406
+ self.ddf_c = to_float(rainfall_params3[3])
407
+ self.ddf_d1 = to_float(rainfall_params3[4])
408
+ self.ddf_d2 = to_float(rainfall_params3[5])
409
+ self.ddf_d3 = to_float(rainfall_params3[6])
410
+ self.ddf_e = to_float(rainfall_params3[7])
411
+ self.ddf_f = to_float(rainfall_params3[8])
424
412
 
425
413
  def _write(self):
426
414
  """Function to write a valid REFHBDY block"""
427
415
  _validate_unit(self) # Function to check the params are valid for QTBDY
428
- header = f"REFHBDY #revision#{self._revision} {self.comment}"
416
+ header = self._create_header(include_revision=True)
429
417
  name = self.name[: self._label_len]
430
418
 
431
419
  refhbdy_block = [header, name]
432
- line3 = join_10_char(self._something, self.easting, self.northing)
420
+ line3 = join_10_char(self._unknown_param_1, self.easting, self.northing)
433
421
  self.sim_type = (
434
422
  "" if self.sim_type.upper() == "FULL" else self.sim_type
435
423
  ) # Allow 'full' as an option
@@ -454,8 +442,8 @@ class REFHBDY(Unit):
454
442
  if self.use_urban_subdivisions:
455
443
  refhbdy_block.extend(self._urban_refh_data)
456
444
 
457
- line6 = join_10_char(self.storm_area, self.storm_duration, self._something2)
458
- line7 = join_10_char(self._something3, self.arf_method) + self.rainfall_comment
445
+ line6 = join_10_char(self.storm_area, self.storm_duration, self._unknown_param_2)
446
+ line7 = join_10_char(self._unknown_param_3, self.arf_method) + self.rainfall_comment
459
447
  line8 = join_10_char(
460
448
  self.observed_rainfall_depth,
461
449
  self.return_period,
@@ -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.
@@ -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)