floodmodeller-api 0.5.1__py3-none-any.whl → 0.5.2.post1__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 +161 -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 +10 -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.post1.dist-info}/LICENSE.txt +1 -1
  81. {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.post1.dist-info}/METADATA +11 -3
  82. {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.post1.dist-info}/RECORD +85 -70
  83. {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.post1.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.post1.dist-info}/entry_points.txt +0 -0
  86. {floodmodeller_api-0.5.1.dist-info → floodmodeller_api-0.5.2.post1.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,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 csv
20
+ import logging
20
21
  import subprocess
21
22
  import time
22
23
  from io import StringIO
@@ -32,19 +33,28 @@ from .diff import check_item_with_dataframe_equal
32
33
  from .ief_flags import flags
33
34
  from .logs import LF1, create_lf
34
35
  from .to_from_json import Jsonable
35
- from .util import handle_exception
36
+ from .util import handle_exception, is_windows
36
37
  from .zz import ZZN
37
38
 
38
39
 
39
- def try_numeric(value: str) -> str | int | float:
40
+ def try_converting(value: str) -> str | int | float:
40
41
  """Attempt to parse value as float or int if valid, else return the original string"""
41
42
  try:
42
43
  return int(value)
43
44
  except ValueError:
44
- try:
45
- return float(value)
46
- except ValueError:
47
- 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
48
58
 
49
59
 
50
60
  class IEF(FMFile):
@@ -74,13 +84,13 @@ class IEF(FMFile):
74
84
  if ief_filepath is not None:
75
85
  FMFile.__init__(self, ief_filepath)
76
86
  self._read()
77
- self._log_path = self._get_result_filepath("lf1")
87
+ self._log_path = self._filepath.with_suffix(".lf1")
78
88
  else:
79
89
  self._create_from_blank()
80
90
 
81
91
  def _read(self):
82
92
  # Read IEF data
83
- with open(self._filepath) as ief_file:
93
+ with open(self._filepath, encoding=self.ENCODING) as ief_file:
84
94
  raw_data = [line.rstrip("\n") for line in ief_file]
85
95
  # Clean data and add as class properties
86
96
  # Create a list to store the properties which are to be saved in IEF, so as to ignore any temp properties.
@@ -116,7 +126,7 @@ class IEF(FMFile):
116
126
  self._ief_properties.append(prop)
117
127
  else:
118
128
  # Sets the property and value as class properties so they can be edited.
119
- setattr(self, prop, try_numeric(value))
129
+ setattr(self, prop, try_converting(value))
120
130
  self._ief_properties.append(prop)
121
131
  prev_comment = None
122
132
  else:
@@ -208,7 +218,7 @@ class IEF(FMFile):
208
218
  if "=" in line:
209
219
  prop, value = line.split("=")
210
220
  # Sets the property and value as class properties so they can be edited.
211
- setattr(self, prop, try_numeric(value))
221
+ setattr(self, prop, try_converting(value))
212
222
  self._ief_properties.append(prop)
213
223
  else:
214
224
  # This should add the [] bound headers
@@ -226,8 +236,9 @@ class IEF(FMFile):
226
236
  ):
227
237
  # Check if valid flag
228
238
  if prop.upper() not in flags:
229
- print(
230
- 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,
231
242
  )
232
243
  continue
233
244
 
@@ -435,6 +446,7 @@ class IEF(FMFile):
435
446
  filepath (string): Full filepath to new location for ief file (including '.ief' extension)
436
447
  """
437
448
  self._save(filepath)
449
+ self._log_path = self._filepath.with_suffix(".lf1")
438
450
 
439
451
  @handle_exception(when="simulate")
440
452
  def simulate( # noqa: C901, PLR0912, PLR0913
@@ -501,10 +513,10 @@ class IEF(FMFile):
501
513
  msg = f"Flood Modeller engine not found! Expected location: {isis32_fp}"
502
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
521
  process = Popen(run_command, cwd=Path(self._filepath).parent)
510
522
 
@@ -521,10 +533,10 @@ 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
541
  return Popen(run_command, cwd=Path(self._filepath).parent)
530
542
 
@@ -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,7 +59,7 @@ class INP(FMFile):
59
59
 
60
60
  def _read(self):
61
61
  # Read INP file
62
- with open(self._filepath) as inp_file:
62
+ with open(self._filepath, encoding=self.ENCODING) as inp_file:
63
63
  self._raw_data = [line.rstrip("\n") for line in inp_file]
64
64
 
65
65
  # Generate INP file structure
@@ -158,7 +158,7 @@ class INP(FMFile):
158
158
  data = units.helpers.split_n_char(line, 21)
159
159
 
160
160
  # Set type to Float or Stirng, as appropirate.
161
- self.options[data[0].lower()] = _to_str(
161
+ self.options[data[0].lower()] = to_str(
162
162
  data[1],
163
163
  None,
164
164
  check_float=True,
@@ -197,8 +197,6 @@ class INP(FMFile):
197
197
  in_block = False
198
198
  unit_block = {}
199
199
  for idx, line in enumerate(self._raw_data):
200
- # TODO: Add functionality to compare first four characters only (alphanumeric) - need to consider names shorter than 4 characters, and those with _ within name
201
-
202
200
  # Check if subsection is known
203
201
  if line.upper() in subsections.ALL_SUBSECTIONS:
204
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)
@@ -175,7 +176,7 @@ class LF(FMFile):
175
176
 
176
177
  delattr(self, "info")
177
178
 
178
- def to_dataframe(self, *, include_tuflow: bool = False) -> pd.DataFrame:
179
+ def to_dataframe(self, variable: str = "all", *, include_tuflow: bool = False) -> pd.DataFrame:
179
180
  """Collects parameter values that change throughout simulation into a dataframe
180
181
 
181
182
  Args:
@@ -185,17 +186,22 @@ class LF(FMFile):
185
186
  pd.DataFrame: DataFrame of log file parameters indexed by simulation time (unsteady) or network iterations (steady)
186
187
  """
187
188
 
188
- # TODO: make more like ZZN.to_dataframe
189
-
190
- data_type_all = {
189
+ lf_df_data = {
191
190
  k: getattr(self, k)
192
191
  for k, v in self._data_to_extract.items()
193
- 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
194
195
  }
195
196
 
196
- lf_df = pd.concat(data_type_all, axis=1)
197
- lf_df.columns = lf_df.columns.droplevel()
197
+ if lf_df_data == {}:
198
+ msg = f"No data extracted for variable '{variable}'"
199
+ raise ValueError(msg)
198
200
 
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
199
205
  return lf_df.sort_index()
200
206
 
201
207
  def _sync_cols(self):
@@ -217,7 +223,7 @@ class LF(FMFile):
217
223
  def _print_no_lines(self):
218
224
  """Prints number of lines that have been read so far"""
219
225
 
220
- print("Last line read: " + str(self._no_lines))
226
+ logging.info("Last line read: %s", self._no_lines)
221
227
 
222
228
  def report_progress(self) -> float:
223
229
  """Returns progress for unsteady simulations
@@ -316,7 +322,7 @@ def create_lf(filepath: Path, suffix: str) -> LF1 | LF2 | None:
316
322
  """Checks for a new log file, waiting for its creation if necessary"""
317
323
 
318
324
  def _no_log_file(reason: str) -> None:
319
- 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)
320
326
 
321
327
  # ensure progress bar is supported
322
328
  if suffix not in {"lf1", "lf2"}:
@@ -349,7 +355,7 @@ def create_lf(filepath: Path, suffix: str) -> LF1 | LF2 | None:
349
355
  last_modified = dt.datetime.fromtimestamp(last_modified_timestamp)
350
356
  time_diff_sec = (dt.datetime.now() - last_modified).total_seconds()
351
357
 
352
- # 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
353
359
  old_log_file = time_diff_sec > OLD_FILE
354
360
 
355
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.
@@ -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": {
@@ -16,12 +16,15 @@ from .units import (
16
16
  HTBDY,
17
17
  IIC,
18
18
  INTERPOLATE,
19
+ JUNCTION,
20
+ LATERAL,
19
21
  ORIFICE,
20
22
  OUTFALL,
21
23
  QHBDY,
22
24
  QTBDY,
23
25
  REFHBDY,
24
26
  REPLICATE,
27
+ RESERVOIR,
25
28
  RIVER,
26
29
  RNWEIR,
27
30
  SLUICE,
@@ -32,7 +35,8 @@ from .units import (
32
35
  )
33
36
  from .urban1d.conduits import CONDUIT as CONDUIT_URBAN
34
37
  from .urban1d.conduits import CONDUITS as CONDUITS_URBAN
35
- from .urban1d.junctions import JUNCTION, JUNCTIONS
38
+ from .urban1d.junctions import JUNCTION as JUNCTION_URBAN
39
+ from .urban1d.junctions import JUNCTIONS
36
40
  from .urban1d.losses import LOSS, LOSSES
37
41
  from .urban1d.outfalls import OUTFALL as OUTFALL_URBAN
38
42
  from .urban1d.outfalls import OUTFALLS as OUTFALLS_URBAN
@@ -51,7 +55,7 @@ api_class_mapping: dict[str, Any] = {
51
55
  "floodmodeller_api.zzn.ZZN": ZZN,
52
56
  "floodmodeller_api.backup.File": File,
53
57
  "floodmodeller_api.urban1d.junctions.JUNCTIONS": JUNCTIONS,
54
- "floodmodeller_api.urban1d.junctions.JUNCTION": JUNCTION,
58
+ "floodmodeller_api.urban1d.junctions.JUNCTION": JUNCTION_URBAN,
55
59
  "floodmodeller_api.urban1d.outfalls.OUTFALLS": OUTFALLS_URBAN,
56
60
  "floodmodeller_api.urban1d.outfalls.OUTFALL": OUTFALL_URBAN,
57
61
  "floodmodeller_api.urban1d.raingauges.RAINGAUGES": RAINGAUGES,
@@ -68,6 +72,9 @@ api_class_mapping: dict[str, Any] = {
68
72
  "floodmodeller_api.units.boundaries.REFHBDY": REFHBDY,
69
73
  "floodmodeller_api.units.comment.COMMENT": COMMENT,
70
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,
71
78
  "floodmodeller_api.units.iic.IIC": IIC,
72
79
  "floodmodeller_api.units.losses.BLOCKAGE": BLOCKAGE,
73
80
  "floodmodeller_api.units.losses.CULVERT": CULVERT,
@@ -57,7 +57,9 @@ def from_gui(test_workspace: Path):
57
57
  def test_results_close_to_gui(section: str, dat: DAT, from_gui: pd.DataFrame):
58
58
  threshold = 6
59
59
 
60
- actual = dat.sections[section].conveyance
60
+ actual = dat.sections[section].conveyance # type: ignore
61
+ # ignored because we know that these all have type RIVER
62
+
61
63
  expected = (
62
64
  from_gui.set_index(f"{section}_stage")[f"{section}_conveyance"].dropna().drop_duplicates()
63
65
  )
@@ -74,14 +76,17 @@ def test_results_close_to_gui(section: str, dat: DAT, from_gui: pd.DataFrame):
74
76
 
75
77
  @pytest.mark.parametrize("section", ["a", "a2", "b", "b2", "c", "d", "d2", "e", "e2", "e3"])
76
78
  def test_results_match_gui_at_shared_points(section: str, dat: DAT, from_gui: pd.DataFrame):
77
- tolerance = 1e-2 # 0.001
78
- actual = dat.sections[section].conveyance
79
+ tolerance = 1e-2 # 0.01
80
+
81
+ actual = dat.sections[section].conveyance # type: ignore
82
+ # ignored because we know that these all have type RIVER
83
+
79
84
  expected = (
80
85
  from_gui.set_index(f"{section}_stage")[f"{section}_conveyance"].dropna().drop_duplicates()
81
86
  )
82
87
  shared_index = sorted(set(actual.index).intersection(expected.index))
83
88
  diff = expected[shared_index] - actual[shared_index]
84
- assert (abs(diff) < tolerance).all() # asserts all conveyance values within 0.001 difference
89
+ assert (abs(diff) < tolerance).all() # asserts all conveyance values within 0.01 difference
85
90
 
86
91
 
87
92
  def test_calculate_geometry():
@@ -1,10 +1,11 @@
1
+ import logging
1
2
  from pathlib import Path
2
3
  from unittest.mock import patch
3
4
 
4
5
  import pytest
5
6
 
6
7
  from floodmodeller_api import DAT
7
- from floodmodeller_api.units import QTBDY
8
+ from floodmodeller_api.units import JUNCTION, LATERAL, QTBDY, RESERVOIR
8
9
  from floodmodeller_api.util import FloodModellerAPIError
9
10
 
10
11
 
@@ -41,13 +42,6 @@ def dat_ex6(test_workspace):
41
42
  yield dat
42
43
 
43
44
 
44
- def test_dat_str_not_changed_by_write(dat_fp, data_before):
45
- # TODO: Update this test - it isn't really testing anything since the behaviour of the fixture is exactly the same
46
- """DAT: Test str representation equal to dat file with no changes"""
47
- dat = DAT(dat_fp)
48
- assert dat._write() == data_before
49
-
50
-
51
45
  def test_changing_section_and_dist_works(dat_fp, data_before):
52
46
  """DAT: Test changing and reverting section name and dist to next makes no changes"""
53
47
  dat = DAT(dat_fp)
@@ -77,17 +71,17 @@ def test_changing_and_reverting_qtbdy_hydrograph_works(dat_fp, data_before):
77
71
  assert dat._write() == data_before
78
72
 
79
73
 
80
- def test_dat_read_doesnt_change_data(test_workspace, tmpdir):
74
+ def test_dat_read_doesnt_change_data(test_workspace, tmp_path):
81
75
  """DAT: Check all '.dat' files in folder by reading the _write() output into a new DAT instance and checking it stays the same."""
82
76
  for datfile in Path(test_workspace).glob("*.dat"):
83
77
  dat = DAT(datfile)
84
78
  first_output = dat._write()
85
- new_path = Path(tmpdir) / "tmp.dat"
79
+ new_path = tmp_path / "tmp.dat"
86
80
  dat.save(new_path)
87
81
  second_dat = DAT(new_path)
88
- assert dat == second_dat # Checks equivalence on the class itself
82
+ assert dat == second_dat, f"dat objects not equal for {datfile=}"
89
83
  second_output = second_dat._write()
90
- assert first_output == second_output
84
+ assert first_output == second_output, f"dat outputs not equal for {datfile=}"
91
85
 
92
86
 
93
87
  def test_insert_unit_before(units, dat_ex6):
@@ -201,12 +195,14 @@ def test_remove_unit(dat_ex3):
201
195
  assert (prev_dat_struct_len - len(dat_ex3._dat_struct)) == 1
202
196
 
203
197
 
204
- def test_diff(test_workspace, capsys):
205
- dat_ex4 = DAT(Path(test_workspace, "ex4.DAT"))
206
- dat_ex4_changed = DAT(Path(test_workspace, "ex4_changed.DAT"))
207
- dat_ex4.diff(dat_ex4_changed)
208
- assert capsys.readouterr().out == (
209
- "Files not equivalent, 12 difference(s) found:\n"
198
+ def test_diff(test_workspace, caplog):
199
+ with caplog.at_level(logging.INFO):
200
+ dat_ex4 = DAT(Path(test_workspace, "ex4.DAT"))
201
+ dat_ex4_changed = DAT(Path(test_workspace, "ex4_changed.DAT"))
202
+ dat_ex4.diff(dat_ex4_changed)
203
+
204
+ assert caplog.text == (
205
+ "INFO root:_base.py:135 Files not equivalent, 12 difference(s) found:\n"
210
206
  " DAT->structures->MILLAu->RNWEIR..MILLAu->upstream_crest_height: 1.07 != 1.37\n"
211
207
  " DAT->structures->MILLBu->RNWEIR..MILLBu->upstream_crest_height: 0.43 != 0.73\n"
212
208
  " DAT->structures->ROAD1->RNWEIR..ROAD1->upstream_crest_height: 2.02 != 2.32\n"
@@ -220,3 +216,155 @@ def test_diff(test_workspace, capsys):
220
216
  " DAT->_all_units->itm[61]->RNWEIR..CSRD01u->upstream_crest_height: 0.81 != 1.11\n"
221
217
  " DAT->_all_units->itm[73]->RNWEIR..FOOTa->upstream_crest_height: 2.47 != 2.77\n"
222
218
  )
219
+
220
+
221
+ def test_valid_network(test_workspace: Path):
222
+ """Test against network derived manually."""
223
+ dat = DAT(test_workspace / "network.dat")
224
+ actual_nodes, actual_edges = dat.get_network()
225
+
226
+ expected_edges = [
227
+ ("FSSR16BDY_resin", "RIVER_resin"),
228
+ ("QTBDY_CS26", "RIVER_CS26"),
229
+ ("RIVER_CS26", "RIVER_CS25"),
230
+ ("RIVER_CS25", "RIVER_CS24"),
231
+ ("RIVER_CS25", "SPILL_RD25Su"),
232
+ ("RIVER_CS24", "RIVER_CS23"),
233
+ ("RIVER_CS24", "SPILL_RD24Su"),
234
+ ("RIVER_CS23", "RIVER_CS22"),
235
+ ("RIVER_CS23", "SPILL_RD23Su"),
236
+ ("RIVER_CS22", "RIVER_CS21"),
237
+ ("RIVER_CS22", "SPILL_RD22Su"),
238
+ ("RIVER_CS21", "RIVER_CS20"),
239
+ ("RIVER_CS21", "SPILL_RD21Su"),
240
+ ("RIVER_CS20", "RIVER_CS19"),
241
+ ("RIVER_CS20", "SPILL_RD20Su"),
242
+ ("RIVER_CS19", "RIVER_CS18"),
243
+ ("RIVER_CS19", "SPILL_RD19Su"),
244
+ ("RIVER_CS18", "RIVER_RESOUT2a"),
245
+ ("RIVER_CS18", "SPILL_RD18Su"),
246
+ ("RIVER_RESOUT2a", "RIVER_RESOUT2b"),
247
+ ("RIVER_RESOUT2b", "JUNCTION_RESOUT2b"),
248
+ ("JUNCTION_RESOUT2b", "RIVER_RESOUT2u"),
249
+ ("JUNCTION_RESOUT2b", "RIVER_RESOUT2d"),
250
+ ("RIVER_resin", "RIVER_CSRD25"),
251
+ ("RIVER_CSRD25", "RIVER_CSRD24"),
252
+ ("RIVER_CSRD25", "SPILL_RD25Su"),
253
+ ("RIVER_CSRD24", "RIVER_CSRD23"),
254
+ ("RIVER_CSRD24", "SPILL_RD24Su"),
255
+ ("RIVER_CSRD23", "RIVER_CSRD22"),
256
+ ("RIVER_CSRD23", "SPILL_RD23Su"),
257
+ ("RIVER_CSRD22", "RIVER_CSRD21"),
258
+ ("RIVER_CSRD22", "SPILL_RD22Su"),
259
+ ("RIVER_CSRD21", "RIVER_CSRD20"),
260
+ ("RIVER_CSRD21", "SPILL_RD21Su"),
261
+ ("RIVER_CSRD20", "RIVER_CSRD19"),
262
+ ("RIVER_CSRD20", "SPILL_RD20Su"),
263
+ ("RIVER_CSRD19", "RIVER_CSRD18"),
264
+ ("RIVER_CSRD19", "SPILL_RD19Su"),
265
+ ("RIVER_CSRD18", "RIVER_CSRD17"),
266
+ ("RIVER_CSRD18", "SPILL_RD18Su"),
267
+ ("RIVER_CSRD17", "RIVER_CSRD16"),
268
+ ("RIVER_CSRD16", "RIVER_CSRD15"),
269
+ ("RIVER_CSRD15", "RIVER_CSRD14u"),
270
+ ("RIVER_CSRD14u", "JUNCTION_CSRD14u"),
271
+ ("JUNCTION_CSRD14u", "RNWEIR_MILLBu"),
272
+ ("JUNCTION_CSRD14u", "RNWEIR_MILLAu"),
273
+ ("RNWEIR_MILLAu", "RIVER_MILLAd"),
274
+ ("RIVER_MILLAd", "RIVER_RESOUT2u"),
275
+ ("RIVER_RESOUT2d", "RIVER_CSRD13"),
276
+ ("RIVER_CSRD13", "RIVER_CSRD12u"),
277
+ ("RIVER_CSRD12u", "JUNCTION_CSRD12u"),
278
+ ("RNWEIR_MILLBu", "JUNCTION_CSRD12u"),
279
+ ("JUNCTION_CSRD12u", "RIVER_CSRD12d"),
280
+ ("RIVER_CSRD12d", "RIVER_CSRD10"),
281
+ ("RIVER_CSRD10", "RIVER_CSRD09"),
282
+ ("RIVER_CSRD09u", "RIVER_CSRD09a"),
283
+ ("RIVER_CSRD09", "JUNCTION_CSRD09"),
284
+ ("RIVER_CSRD09u", "JUNCTION_CSRD09"),
285
+ ("JUNCTION_CSRD09", "RNWEIR_ROAD1"),
286
+ ("RIVER_CSRD09a", "BERNOULLI_CSRD09a"),
287
+ ("BERNOULLI_CSRD09a", "RIVER_CSRD08u"),
288
+ ("RIVER_CSRD08u", "RIVER_CSRD08a"),
289
+ ("RIVER_CSRD08a", "JUNCTION_CSRD08a"),
290
+ ("JUNCTION_CSRD08a", "RIVER_CSRD08"),
291
+ ("RNWEIR_ROAD1", "JUNCTION_CSRD08a"),
292
+ ("RIVER_CSRD08", "RIVER_CSRD07"),
293
+ ("RIVER_CSRD07", "RIVER_CSRD06"),
294
+ ("RIVER_CSRD06", "RIVER_CSRD05"),
295
+ ("RIVER_CSRD05", "RIVER_CSRD04"),
296
+ ("RIVER_CSRD04", "RIVER_CSRD03"),
297
+ ("RIVER_CSRD03", "RIVER_CSRD02"),
298
+ ("RIVER_CSRD02", "RIVER_CSRD02d"),
299
+ ("RIVER_CSRD02d", "JUNCTION_CSRD02d"),
300
+ ("JUNCTION_CSRD02d", "RNWEIR_RAILRDu"),
301
+ ("JUNCTION_CSRD02d", "BERNOULLI_RAILBRu"),
302
+ ("BERNOULLI_RAILBRu", "JUNCTION_RAILBRd"),
303
+ ("RNWEIR_RAILRDu", "JUNCTION_RAILBRd"),
304
+ ("JUNCTION_RAILBRd", "RIVER_CSRD01a"),
305
+ ("RIVER_CSRD01a", "RIVER_CSRD01u"),
306
+ ("RIVER_CSRD01u", "RNWEIR_CSRD01u"),
307
+ ("RNWEIR_CSRD01u", "RIVER_CSRD01d"),
308
+ ("RIVER_CSRD01d", "RIVER_CSRD01"),
309
+ ("RIVER_CSRD01", "INTERPOLATE_DS.001"),
310
+ ("INTERPOLATE_DS.001", "INTERPOLATE_DS.002"),
311
+ ("INTERPOLATE_DS.002", "INTERPOLATE_DS.003"),
312
+ ("INTERPOLATE_DS.003", "INTERPOLATE_DS.004"),
313
+ ("INTERPOLATE_DS.004", "INTERPOLATE_DS.005"),
314
+ ("INTERPOLATE_DS.005", "INTERPOLATE_DS.006"),
315
+ ("INTERPOLATE_DS.006", "RIVER_DS2"),
316
+ ("RIVER_DS2", "JUNCTION_DS2"),
317
+ ("JUNCTION_DS2", "RNWEIR_FOOTa"),
318
+ ("JUNCTION_DS2", "BERNOULLI_FOOTBRu"),
319
+ ("RNWEIR_FOOTa", "JUNCTION_FOOTb"),
320
+ ("BERNOULLI_FOOTBRu", "JUNCTION_FOOTb"),
321
+ ("JUNCTION_FOOTb", "RIVER_DS3"),
322
+ ("RIVER_DS3", "RIVER_DS4"),
323
+ ("RIVER_DS4", "QHBDY_DS4"),
324
+ ]
325
+
326
+ actual = {tuple(x.unique_name for x in y) for y in actual_edges}
327
+ expected = set(expected_edges)
328
+ assert expected == actual
329
+ assert len(actual_nodes) == 86
330
+
331
+
332
+ def test_invalid_network(test_workspace: Path):
333
+ """Test dat file that cannot be made into a valid network."""
334
+ dat = DAT(test_workspace / "All Units 4_6.DAT")
335
+ with pytest.raises(RuntimeError):
336
+ dat.get_network()
337
+
338
+
339
+ def test_create_and_insert_connectors():
340
+ dat = DAT()
341
+ junction = JUNCTION(comment="hi", labels=["A", "B"])
342
+ lateral = LATERAL(name="lat", comment="bye")
343
+ reservoir = RESERVOIR(
344
+ easting=0,
345
+ northing=0,
346
+ runoff=0,
347
+ name="res",
348
+ comment="hello",
349
+ lateral_inflow_labels=["C", "D"],
350
+ )
351
+ dat.insert_units([junction, lateral, reservoir], add_at=-1)
352
+ assert dat.connectors == {"A": junction, "lat": lateral}
353
+ assert dat.controls == {"res": reservoir}
354
+
355
+
356
+ @pytest.mark.parametrize(
357
+ ("dat_str", "label"),
358
+ [
359
+ ("encoding_test_utf8.dat", "d\xc3\xa5rek"), # because it's initially saved as utf8
360
+ ("encoding_test_cp1252.dat", "d\xe5rek"),
361
+ ],
362
+ )
363
+ def test_encoding(test_workspace: Path, dat_str: str, label: str, tmp_path: Path):
364
+ dat_read = DAT(test_workspace / dat_str)
365
+ new_path = tmp_path / "tmp_encoding.dat"
366
+ dat_read.save(new_path)
367
+ dat_write = DAT(new_path)
368
+
369
+ assert label in dat_read.sections
370
+ assert label in dat_write.sections # remains as \xc3\xa5 even for utf8