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,14 +1,57 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
1
4
  import csv
5
+ import logging
6
+ from typing import TYPE_CHECKING
2
7
 
3
8
  from floodmodeller_api import DAT
4
9
 
10
+ if TYPE_CHECKING:
11
+ from floodmodeller_api._base import Unit
12
+ from floodmodeller_api.units import (
13
+ BRIDGE,
14
+ CONDUIT,
15
+ ORIFICE,
16
+ REPLICATE,
17
+ RNWEIR,
18
+ SLUICE,
19
+ SPILL,
20
+ WEIR,
21
+ )
22
+
23
+
24
+ def extract_attrs(object_input, keys):
25
+ """Extract attributes and their values from a given object and return as a dict.
26
+
27
+ No handling for where attributes don't exist but can ignore for when this is an issue, shouldn't be an issue here
28
+ """
29
+ return {key: getattr(object_input, key) for key in keys}
30
+
31
+
32
+ def serialise_keys(old_dict):
33
+ """Exchange tuple keys in dict for string keys.
34
+
35
+ Dictionary keys must be unique; but in FM the node label isn't always unique
36
+ however the (label,type) pair will always be unique; so we have to use that as our key.
37
+ json doesn't like tuples as keys so we have to convert them first if we need to serialise our dictionary output.
38
+
39
+ Replaces tuple keys with string versions of themselves, wrapped in ()
40
+
41
+ """
42
+ return {f"({','.join(key)})": value for key, value in old_dict.items()}
43
+
5
44
 
6
45
  class StructureLogBuilder:
7
- def __init__(self, input_path, output_path) -> None:
46
+ def __init__(self, input_path: str | None = None, output_path: str | None = None) -> None:
8
47
  self.dat_file_path = input_path
9
48
  self.csv_output_path = output_path
49
+ self._conduit_chains: dict[str | None, list[str | None]] = {}
50
+ self._already_in_chain: set[str | None] = set()
51
+ self.unit_store: dict[(str, str)] = {}
52
+ self._replicate_mimics: dict[str | None, str | None] = {}
10
53
 
11
- def _add_fields(self):
54
+ def _add_fields(self, writer) -> None:
12
55
  field = [
13
56
  "Unit Name",
14
57
  "Unit Type",
@@ -19,237 +62,452 @@ class StructureLogBuilder:
19
62
  "Weir Coefficient",
20
63
  "Culvert Inlet/Outlet Loss",
21
64
  ]
22
- self._writer.writerow(field)
65
+ writer.writerow(field)
23
66
 
24
- def _conduit_data(self, conduit):
25
- length = 0.0
26
- inlet = ""
27
- outlet = ""
28
- previous = self._dat.prev(conduit)
67
+ def _conduit_data(self, conduit: CONDUIT | REPLICATE) -> tuple[dict, Unit | None]:
68
+ conduit_data = {"length": conduit.dist_to_next}
69
+ # modified conduit crawler script
70
+ add_to_conduit_stack = None
71
+
72
+ # check if the previous node is an inlet
73
+ previous = self.dat.prev(conduit)
29
74
  if hasattr(previous, "subtype") and previous.subtype == "INLET":
30
- inlet = previous.ki
75
+ conduit_data["inlet"] = previous.ki
76
+
31
77
  current_conduit = conduit
32
- while current_conduit.dist_to_next != 0:
33
- length += current_conduit.dist_to_next
34
- current_conduit = self._dat.next(current_conduit)
35
- next_conduit = self._dat.next(current_conduit)
36
- if hasattr(next_conduit, "subtype") and next_conduit.subtype == "OUTLET":
37
- outlet = next_conduit.loss_coefficient
38
- return [length, inlet, outlet]
39
-
40
- def _culvert_loss_data(self, inlet, outlet):
41
- culvert_loss = ""
42
- if inlet != "" and outlet != "":
43
- culvert_loss = f"Ki: {inlet}, Ko: {outlet}"
44
- elif inlet != "":
45
- culvert_loss = f"Ki: {inlet}"
46
- elif outlet != "":
47
- culvert_loss = f"Ko: {outlet}"
48
- return culvert_loss
49
-
50
- def _circular_data(self, conduit, length):
51
- dimensions = f"dia: {conduit.diameter:.2f} x l: {length:.2f}"
52
- all_mannings = [
78
+
79
+ if current_conduit.name not in self._already_in_chain:
80
+ # if this conduit isn't part of a chain already, then it must be part of a new chain
81
+ total_length = 0
82
+ chain = []
83
+ while True:
84
+ self._already_in_chain.add(current_conduit.name)
85
+ chain.append(current_conduit.name)
86
+
87
+ if current_conduit._unit not in ("CONDUIT", "REPLICATE"):
88
+ # This occurs in cases where a conduit chain doesnt 'legally' end.
89
+ break
90
+
91
+ if current_conduit.dist_to_next == 0:
92
+ break
93
+
94
+ total_length += current_conduit.dist_to_next
95
+ current_conduit = self.dat.next(current_conduit)
96
+
97
+ self._conduit_chains[conduit.name] = chain.copy()
98
+ conduit_data["total_length"] = total_length
99
+
100
+ next_conduit = self.dat.next(conduit)
101
+ if next_conduit is not None:
102
+ if hasattr(next_conduit, "subtype") and next_conduit.subtype == "OUTLET":
103
+ conduit_data["outlet"] = next_conduit.loss_coefficient
104
+
105
+ if next_conduit._unit == "REPLICATE":
106
+ # for replicates, pass down the label of the unit it's copying from.
107
+ if current_conduit._unit == "REPLICATE":
108
+ self._replicate_mimics[next_conduit.name] = self._replicate_mimics[
109
+ current_conduit.name
110
+ ]
111
+ else:
112
+ self._replicate_mimics[next_conduit.name] = current_conduit.name
113
+
114
+ # Replicates aren't in the DAT.structures dict so we need to add them manually.
115
+ add_to_conduit_stack = copy.deepcopy(next_conduit)
116
+
117
+ return conduit_data, add_to_conduit_stack
118
+
119
+ def _circular_data(self, conduit: CONDUIT) -> dict:
120
+ dimensions = extract_attrs(conduit, {"diameter", "invert"})
121
+ all_friction = [
53
122
  conduit.friction_above_axis,
54
123
  conduit.friction_below_axis,
55
124
  ]
56
- mannings_set = {min(all_mannings), max(all_mannings)}
57
- if len(mannings_set) == 1:
58
- friction = f"Mannings: {mannings_set.pop()}"
59
- else:
60
- friction = f"Mannings: [min: {mannings_set.pop()}, max: {mannings_set.pop()}]"
125
+ friction_set = sorted(set(all_friction))
126
+ friction = {
127
+ "friction_eq": conduit.friction_eq,
128
+ "friction_set": friction_set,
129
+ "all_friction": all_friction,
130
+ }
61
131
 
62
- return [friction, dimensions]
132
+ return {"dimensions": dimensions, "friction": friction}
63
133
 
64
- def _sprungarch_data(self, conduit, length):
65
- dimensions = f"(Springing: {conduit.height_crown:.2f}, Crown: {conduit.height_springing:.2f}) x w: {conduit.width:.2f} x l: {length:.2f}"
66
- all_mannings = [
134
+ def _sprungarch_data(self, conduit: CONDUIT) -> dict:
135
+ dimensions = extract_attrs(
136
+ conduit,
137
+ {"width", "height_springing", "height_crown", "elevation_invert"},
138
+ )
139
+ # invert attribute is different for these, making homogenous
140
+ dimensions["invert"] = dimensions.pop("elevation_invert")
141
+ all_friction = [
67
142
  conduit.friction_on_invert,
68
143
  conduit.friction_on_soffit,
69
144
  conduit.friction_on_walls,
70
145
  ]
71
- mannings_set = {min(all_mannings), max(all_mannings)}
72
- if len(mannings_set) == 1:
73
- friction = f"Mannings: {mannings_set.pop()}"
74
- else:
75
- friction = f"Mannings: [min: {mannings_set.pop()}, max: {mannings_set.pop()}]"
146
+ friction_set = sorted(set(all_friction))
147
+ friction = {
148
+ "friction_eq": conduit.equation,
149
+ "friction_set": friction_set,
150
+ "all_friction": all_friction,
151
+ }
152
+ return {"dimensions": dimensions, "friction": friction}
76
153
 
77
- return [friction, dimensions]
78
-
79
- def _rectangular_data(self, conduit, length):
80
- dimensions = f"h: {conduit.height:.2f} x w: {conduit.width:.2f} x l: {length:.2f}"
81
- all_mannings = [
154
+ def _rectangular_data(self, conduit):
155
+ dimensions = extract_attrs(conduit, {"width", "height", "invert"})
156
+ all_friction = [
82
157
  conduit.friction_on_invert,
83
158
  conduit.friction_on_soffit,
84
159
  conduit.friction_on_walls,
85
160
  ]
86
- mannings_set = {min(all_mannings), max(all_mannings)}
87
- if len(mannings_set) == 1:
88
- friction = f"Mannings: {mannings_set.pop()}"
89
- else:
90
- friction = f"Mannings: [min: {mannings_set.pop()}, max: {mannings_set.pop()}]"
161
+ friction_set = sorted(set(all_friction))
162
+ friction = {
163
+ "friction_eq": conduit.friction_eq,
164
+ "friction_set": friction_set,
165
+ "all_friction": all_friction,
166
+ }
167
+
168
+ return {"dimensions": dimensions, "friction": friction}
91
169
 
92
- return [friction, dimensions]
170
+ def _section_data(self, conduit: CONDUIT) -> dict:
171
+ # Symmetrical conduits
172
+ # these have a lot of weirdness in terms of data validity and FM will rearrange data that it thinks is wrong
173
+ # so it's mostly worth just trusting the modeller here
93
174
 
94
- def _section_data(self, conduit, length):
95
175
  x_list = conduit.coords.x.tolist()
96
- width = (max(x_list) - min(x_list)) * 2
97
176
  y_list = conduit.coords.y.tolist()
177
+ all_friction = conduit.coords.cw_friction.tolist()
178
+ # want to serialise coords so converting out of dataframe
179
+ width = max(x_list) * 2
98
180
  height = max(y_list) - min(y_list)
99
- # currently this means that height goes to the top of the spike,
100
- # it is only meant to go up to the height of the majority of the area
101
- dimensions = f"h: {height:.2f} x w: {width:.2f} x l: {length:.2f}"
102
- all_cw_frictions = conduit.coords.cw_friction.tolist()
103
- cw_frictions_set = {min(all_cw_frictions), max(all_cw_frictions)}
104
- if len(cw_frictions_set) == 1:
105
- friction = f"Colebrook-White: {cw_frictions_set.pop()}"
106
- else:
107
- friction = (
108
- f"Colebrook-White: [min: {cw_frictions_set.pop()}, max: {cw_frictions_set.pop()}]"
109
- )
181
+ elevation_invert = min(y_list)
182
+ dimensions = {
183
+ "width": width,
184
+ "height": height,
185
+ "invert": elevation_invert,
186
+ "section_data": {"x": x_list, "y": y_list},
187
+ }
110
188
 
111
- return [friction, dimensions]
189
+ all_friction = conduit.coords.cw_friction.tolist()
190
+ friction_set = sorted(set(all_friction))
191
+ friction = {
192
+ "friction_eq": "COLEBROOK-WHITE",
193
+ "friction_set": friction_set,
194
+ "all_friction": all_friction,
195
+ }
112
196
 
113
- def _sprung_data(self, conduit, length):
114
- dimensions = f"(Springing: {conduit.height_crown:.2f}, Crown: {conduit.height_springing:.2f}) x w: {conduit.width:.2f} x l: {length:.2f}"
115
- all_mannings = [
197
+ return {"dimensions": dimensions, "friction": friction}
198
+
199
+ def _sprung_data(self, conduit: CONDUIT) -> dict:
200
+ dimensions = extract_attrs(
201
+ conduit,
202
+ {"width", "height_springing", "height_crown", "elevation_invert"},
203
+ )
204
+ # invert attribute is different for these, making homogenous
205
+ dimensions["invert"] = dimensions.pop("elevation_invert")
206
+ all_friction = [
116
207
  conduit.friction_on_invert,
117
208
  conduit.friction_on_soffit,
118
209
  conduit.friction_on_walls,
119
210
  ]
120
- mannings_set = {min(all_mannings), max(all_mannings)}
121
- if len(mannings_set) == 1:
122
- friction = f"Mannings: {mannings_set.pop()}"
123
- else:
124
- friction = f"Mannings: [min: {mannings_set.pop()}, max: {mannings_set.pop()}]"
125
-
126
- return [friction, dimensions]
127
-
128
- def _add_conduits(self):
129
- for conduit in self._dat.conduits.values():
130
- if conduit.subtype not in [
131
- "CIRCULAR",
132
- "SPRUNGARCH",
133
- "RECTANGULAR",
134
- "SECTION",
135
- "SPRUNG",
211
+ friction_set = sorted(set(all_friction))
212
+ friction = {
213
+ "friction_eq": conduit.equation,
214
+ "friction_set": friction_set,
215
+ "all_friction": all_friction,
216
+ }
217
+
218
+ return {"dimensions": dimensions, "friction": friction}
219
+
220
+ def _replicate_data(self, replicate: REPLICATE) -> dict:
221
+ dimensions = {
222
+ "bed_level_drop": replicate.bed_level_drop,
223
+ "mimic": self._replicate_mimics[replicate.name],
224
+ }
225
+
226
+ return {"dimensions": dimensions}
227
+
228
+ def add_conduits(self):
229
+ conduit_stack = copy.deepcopy(list(self.dat.conduits.values()))
230
+
231
+ # this is a stack/while-loop because I want to add units to it as we go, to detail inline replicate units
232
+ while len(conduit_stack) > 0:
233
+ conduit = conduit_stack.pop(0)
234
+ self.unit_store[(conduit.name, conduit._unit)] = {
235
+ "name": conduit.name,
236
+ "type": conduit._unit,
237
+ "subtype": conduit.subtype,
238
+ "comment": conduit.comment,
239
+ }
240
+ if (conduit._unit, conduit.subtype) not in [
241
+ ("CONDUIT", "CIRCULAR"),
242
+ ("CONDUIT", "SPRUNGARCH"),
243
+ ("CONDUIT", "RECTANGULAR"),
244
+ ("CONDUIT", "SECTION"),
245
+ ("CONDUIT", "SPRUNG"),
246
+ ("REPLICATE", None),
136
247
  ]:
137
- print(f"Conduit subtype: {conduit.subtype} not currently supported")
248
+ logging.warning(
249
+ "Conduit subtype: %s not currently supported in structure log",
250
+ conduit.subtype,
251
+ )
138
252
  self._write(conduit.name, conduit._unit, conduit.subtype)
139
253
  continue
140
- conduit_data = self._conduit_data(conduit)
141
- length = conduit_data[0]
142
- inlet = conduit_data[1]
143
- outlet = conduit_data[2]
254
+ conduit_dict, add_to_conduit_stack = self._conduit_data(conduit)
255
+ self.unit_store[(conduit.name, conduit._unit)]["conduit_data"] = conduit_dict
256
+ # now use individual functions to get friction and dimensional data in a way that is appropriate for each conduit type
257
+ match (conduit._unit, conduit.subtype):
258
+ case ("CONDUIT", "CIRCULAR"):
259
+ self.unit_store[(conduit.name, conduit._unit)] |= self._circular_data(conduit)
260
+ case ("CONDUIT", "SPRUNGARCH"):
261
+ self.unit_store[(conduit.name, conduit._unit)] |= self._sprungarch_data(conduit)
262
+ case ("CONDUIT", "RECTANGULAR"):
263
+ self.unit_store[(conduit.name, conduit._unit)] |= self._rectangular_data(conduit) # fmt: skip
264
+ case ("CONDUIT", "SECTION"):
265
+ self.unit_store[(conduit.name, conduit._unit)] |= self._section_data(conduit)
266
+ case ("CONDUIT", "SPRUNG"):
267
+ self.unit_store[(conduit.name, conduit._unit)] |= self._sprung_data(conduit)
268
+ case ("REPLICATE", None):
269
+ self.unit_store[(conduit.name, conduit._unit)] |= self._replicate_data(conduit)
270
+ case _:
271
+ pass
144
272
 
145
- if length == 0:
146
- continue
147
-
148
- culvert_loss = self._culvert_loss_data(inlet, outlet)
149
- friction = ""
150
- dimensions = ""
151
- if conduit.subtype == "CIRCULAR":
152
- circular_data = self._circular_data(conduit, length)
153
- friction = circular_data[0]
154
- dimensions = circular_data[1]
155
- elif conduit.subtype == "SPRUNGARCH":
156
- sprungarch_data = self._sprungarch_data(conduit, length)
157
- friction = sprungarch_data[0]
158
- dimensions = sprungarch_data[1]
159
- elif conduit.subtype == "RECTANGULAR":
160
- rectangular_data = self._rectangular_data(conduit, length)
161
- friction = rectangular_data[0]
162
- dimensions = rectangular_data[1]
163
- elif conduit.subtype == "SECTION":
164
- section_data = self._section_data(conduit, length)
165
- friction = section_data[0]
166
- dimensions = section_data[1]
167
- elif conduit.subtype == "SPRUNG":
168
- sprung_data = self._sprung_data(conduit, length)
169
- friction = sprung_data[0]
170
- dimensions = sprung_data[1]
171
-
172
- self._write(
173
- conduit.name,
174
- conduit._unit,
175
- conduit.subtype,
176
- conduit.comment,
177
- friction,
178
- dimensions,
179
- "",
180
- culvert_loss,
181
- )
273
+ if add_to_conduit_stack is not None:
274
+ conduit_stack.insert(0, add_to_conduit_stack)
182
275
 
183
- def _orifice_dimensions(self, structure):
276
+ def _orifice_dimensions(self, structure: ORIFICE) -> dict:
277
+ dimensions = {
278
+ "shape": structure.shape,
279
+ "invert": structure.invert,
280
+ "soffit": structure.soffit,
281
+ "bore_area": structure.bore_area,
282
+ }
283
+ dimensions["height"] = structure.soffit - structure.invert
184
284
  if structure.shape == "RECTANGLE":
185
- height = structure.soffit - structure.invert
186
- width = structure.bore_area / height
187
- dimensions = f"h: {height:.2f} x w: {width:.2f}"
285
+ dimensions["width"] = structure.bore_area / dimensions["height"]
286
+ dimensions["bore_area"] = structure.bore_area
188
287
  elif structure.shape == "CIRCULAR":
189
- diameter = structure.soffit - structure.invert
190
- dimensions = f"dia: {diameter:.2f}"
191
- return dimensions
288
+ dimensions["width"] = dimensions["height"]
289
+ # calcuate bore area from given diameter
290
+ dimensions["bore_area"] = (dimensions["height"] ** 2) * (3.1415 * 0.25)
291
+ return {"dimensions": dimensions}
192
292
 
193
- def _spill_data(self, structure):
194
- elevation = min(structure.data.Y.tolist())
293
+ def _spill_data(self, structure: SPILL) -> dict:
195
294
  x_list = structure.data.X.tolist()
196
- width = max(x_list) - min(x_list)
197
- dimensions = f"Elevation: {elevation:.2f} x w: {width:.2f}"
198
- weir_coefficient = structure.weir_coefficient
199
- return [dimensions, weir_coefficient]
200
-
201
- def _bridge_data(self, structure):
202
- all_mannings = structure.section_data["Mannings n"].tolist()
203
- mannings_set = {min(all_mannings), max(all_mannings)}
204
- if len(mannings_set) == 1:
205
- friction = f"Mannings: {mannings_set.pop()}"
206
- else:
207
- friction = f"Mannings: [min: {mannings_set.pop()}, max: {mannings_set.pop()}]"
208
- height = structure.opening_data.values[0][3] - min(structure.section_data.Y.tolist())
209
- width = structure.opening_data.values[0][1] - structure.opening_data.values[0][0]
210
- dimensions = f"h: {height:.2f} x w: {width:.2f}"
211
- return [friction, dimensions]
212
-
213
- def _add_structures(self):
214
- for structure in self._dat.structures.values():
215
- friction = ""
216
- dimensions = ""
217
- weir_coefficient = ""
295
+ y_list = structure.data.Y.tolist()
296
+ dimensions = {
297
+ "invert": min(y_list),
298
+ "width": max(x_list) - min(x_list),
299
+ "weir_coefficient": structure.weir_coefficient,
300
+ "section_data": {"x": x_list, "y": y_list},
301
+ }
302
+ return {"dimensions": dimensions}
303
+
304
+ def _bridge_data(self, structure: BRIDGE) -> dict:
305
+ section_df = structure.section_data
306
+ all_friction = section_df["Mannings n"].tolist()
307
+ friction_set = sorted(set(all_friction))
308
+ friction = {"friction_set": friction_set, "all_friction": all_friction}
309
+
310
+ orifice_data = extract_attrs(
311
+ structure,
312
+ {
313
+ "orifice_flow",
314
+ "orifice_lower_transition_dist",
315
+ "orifice_upper_transition_dist",
316
+ "orifice_discharge_coefficient",
317
+ },
318
+ )
319
+ orifice_data["transition_width"] = (
320
+ structure.orifice_upper_transition_dist + structure.orifice_lower_transition_dist
321
+ )
322
+
323
+ opening_data = []
324
+ # if it has openings, not sure why it wouldn't but worth checking.
325
+ if structure.opening_nrows > 0:
326
+ opening_df = structure.opening_data
327
+ # this isn't 'proper' for a df but there should be few rows and we're doing special stuff
328
+ for _, row in opening_df.iterrows():
329
+ opening = {}
330
+ opening["width"] = row["Finish"] - row["Start"]
331
+
332
+ temp_df = section_df.loc[
333
+ (row["Start"] <= section_df["X"]) & (section_df["X"] <= row["Finish"])
334
+ ]
335
+ opening["bed_minimum"] = temp_df["Y"].min()
336
+
337
+ opening["soffit_level"] = row["Soffit Level"]
338
+ opening["opening_height"] = opening["soffit_level"] - opening["bed_minimum"]
339
+
340
+ opening_data.append(opening)
341
+
342
+ culvert_data = []
343
+ if hasattr(structure, "culvert_data") and structure.culvert_data.shape[0] > 1:
344
+ for _, row in structure.culvert_data.iterrows():
345
+ culvert = {
346
+ "invert": row["Invert"],
347
+ "soffit": row["Soffit"],
348
+ "bore_area": "Section Area",
349
+ }
350
+ culvert["height"] = culvert["soffit"] - culvert["invert"]
351
+ culvert["average_width"] = culvert["bore_area"] / culvert["height"]
352
+ culvert_data.append(culvert)
353
+
354
+ return {
355
+ "opening_data": opening_data,
356
+ "friction": friction,
357
+ "orifice_data": orifice_data,
358
+ "culvert_data": culvert_data,
359
+ }
360
+
361
+ def _sluice_data(self, structure: SLUICE) -> dict:
362
+ dimensions = extract_attrs(structure, {"crest_elevation", "weir_breadth", "weir_length"})
363
+
364
+ return {"dimensions": dimensions}
365
+
366
+ def _weir_data(self, structure: RNWEIR | WEIR) -> dict:
367
+ dimensions = extract_attrs(structure, {"weir_elevation", "weir_breadth"})
368
+
369
+ if structure._unit == "RNWEIR":
370
+ dimensions |= extract_attrs(
371
+ structure,
372
+ {"weir_length", "upstream_crest_height", "downstream_crest_height"},
373
+ )
374
+
375
+ return {"dimensions": dimensions}
376
+
377
+ def add_structures(self):
378
+ for structure in self.dat.structures.values():
379
+ self.unit_store[(structure.name, structure._unit)] = {
380
+ "name": structure.name,
381
+ "type": structure._unit,
382
+ "subtype": structure.subtype,
383
+ "comment": structure.comment,
384
+ }
218
385
  if structure._unit == "ORIFICE":
219
- dimensions = self._orifice_dimensions(structure)
386
+ self.unit_store[(structure.name, structure._unit)] |= self._orifice_dimensions(
387
+ structure,
388
+ )
220
389
  elif structure._unit == "SPILL":
221
- spill_data = self._spill_data(structure)
222
- dimensions = spill_data[0]
223
- weir_coefficient = spill_data[1]
390
+ self.unit_store[(structure.name, structure._unit)] |= self._spill_data(structure)
224
391
  elif structure._unit == "SLUICE":
225
- dimensions = f"Crest Elevation: {structure.crest_elevation:.2f} x w: {structure.weir_breadth:.2f} x l: {structure.weir_length:.2f}"
226
- elif structure._unit == "RNWEIR":
227
- dimensions = f"Crest Elevation: {structure.weir_elevation:.2f} x w: {structure.weir_breadth:.2f} x l: {structure.weir_length:.2f}"
228
- elif structure._unit == "WEIR":
229
- dimensions = f"Crest Elevation: {structure.weir_elevation:.2f} x w: {structure.weir_breadth:.2f}"
392
+ self.unit_store[(structure.name, structure._unit)] |= self._sluice_data(structure)
393
+ elif structure._unit in {"WEIR", "RNWEIR"}:
394
+ self.unit_store[(structure.name, structure._unit)] |= self._weir_data(structure)
230
395
  # Need weir coefficient (the velocity attribute??)
231
396
  elif structure._unit == "BRIDGE":
232
- bridge_data = self._bridge_data(structure)
233
- friction = bridge_data[0]
234
- dimensions = bridge_data[1]
397
+ self.unit_store[(structure.name, structure._unit)] |= self._bridge_data(structure)
235
398
  else:
236
- print(f"Structure: {structure._unit} not currently supported in structure log")
399
+ logging.warning(
400
+ "Structure: %s not currently supported in structure log",
401
+ structure._unit,
402
+ )
237
403
  self._write(structure.name, structure._unit, structure.subtype)
238
404
  continue
239
405
 
240
- self._write(
241
- structure.name,
242
- structure._unit,
243
- structure.subtype,
244
- structure.comment,
245
- friction,
246
- dimensions,
247
- weir_coefficient,
248
- "",
249
- )
406
+ def _format_friction(self, unit_dict):
407
+ text = ""
408
+
409
+ if "friction" not in unit_dict:
410
+ return ""
411
+
412
+ try:
413
+ match unit_dict["friction"]["friction_eq"]:
414
+ case "MANNING":
415
+ text += "Mannings: "
416
+ case "COLEBROOK-WHITE":
417
+ text += "Colebrook-White: "
418
+ except KeyError:
419
+ text += "Mannings: "
420
+
421
+ friction_set = unit_dict["friction"]["friction_set"]
422
+ if len(friction_set) == 1:
423
+ text += f"{friction_set[0]:.3f}"
424
+ else:
425
+ text += f"[min: {friction_set[0]:.3f}, max: {friction_set[-1]:.3f}]"
426
+
427
+ return text
428
+
429
+ def _format_bridge_dimensions(self, unit_dict):
430
+ if len(unit_dict["opening_data"]) == 1:
431
+ opening = unit_dict["opening_data"][0]
432
+ height = opening["opening_height"]
433
+ width = opening["width"]
434
+ return f"h: {height:.2f} x w: {width:.2f}"
435
+
436
+ text = ""
437
+
438
+ for n, opening in enumerate(unit_dict["opening_data"]):
439
+ height = opening["opening_height"]
440
+ width = opening["width"]
441
+
442
+ text += f"Opening {n+1}: h: {height:.2f} x w: {width:.2f} "
443
+
444
+ return text.rstrip()
445
+
446
+ def _format_orifice_dimensions(self, unit_dict):
447
+ height = unit_dict["dimensions"]["height"]
448
+ width = unit_dict["dimensions"]["width"]
449
+ match unit_dict["dimensions"]["shape"]:
450
+ case "RECTANGLE":
451
+ return f"h: {height:.2f} x w: {width:.2f}"
452
+ case "CIRCULAR":
453
+ return f"dia: {width:.2f}"
454
+
455
+ def _format_spill_dimensions(self, unit_dict):
456
+ invert = unit_dict["dimensions"]["invert"]
457
+ width = unit_dict["dimensions"]["width"]
458
+
459
+ return f"Elevation: {invert:.2f} x w: {width:.2f}"
460
+
461
+ def _format_weir_dimensions(self, unit_dict):
462
+ text = ""
463
+ elevation = unit_dict["dimensions"]["weir_elevation"]
464
+ breadth = unit_dict["dimensions"]["weir_breadth"]
465
+
466
+ text += f"Crest Elevation: {elevation:.2f} x w: {breadth:.2f}"
467
+
468
+ if "weir_length" in unit_dict["dimensions"]:
469
+ length = unit_dict["dimensions"]["weir_length"]
470
+ text += f" x l: {length:.2f}"
471
+ return text
472
+
473
+ def _format_sluice_dimensions(self, unit_dict):
474
+ elevation = unit_dict["dimensions"]["crest_elevation"]
475
+ breadth = unit_dict["dimensions"]["weir_breadth"]
476
+ length = unit_dict["dimensions"]["weir_length"]
477
+
478
+ return f"Crest Elevation: {elevation:.2f} x w: {breadth:.2f} x l: {length:.2f}"
479
+
480
+ def _format_conduit_dimensions(self, unit_dict):
481
+ text = ""
482
+ culvert_loss = ""
483
+ match unit_dict["subtype"]:
484
+ case "CIRCULAR":
485
+ text += f'dia: {unit_dict["dimensions"]["diameter"]:.2f} x l: {unit_dict["conduit_data"]["length"]:.2f}'
486
+ case "SPRUNGARCH" | "SPRUNG":
487
+ text += f'(Springing: {unit_dict["dimensions"]["height_springing"]:.2f}, Crown: {unit_dict["dimensions"]["height_crown"]:.2f}) x w: {unit_dict["dimensions"]["width"]:.2f} x l: {unit_dict["conduit_data"]["length"]:.2f}'
488
+ case "RECTANGULAR":
489
+ text += f'h: {unit_dict["dimensions"]["height"]:.2f} x w: {unit_dict["dimensions"]["width"]:.2f} x l: {unit_dict["conduit_data"]["length"]:.2f}'
490
+ case "SECTION":
491
+ text += f'h: {unit_dict["dimensions"]["height"]:.2f} x w: {unit_dict["dimensions"]["width"]:.2f} x l: {unit_dict["conduit_data"]["length"]:.2f}'
492
+ case _:
493
+ return "", ""
494
+
495
+ if "total_length" in unit_dict["conduit_data"]:
496
+ text += f' (Total conduit length: {unit_dict["conduit_data"]["total_length"]:.2f})'
497
+
498
+ if "inlet" in unit_dict["conduit_data"]:
499
+ culvert_loss += f'Ki: {unit_dict["conduit_data"]["inlet"]}, '
500
+
501
+ if "outlet" in unit_dict["conduit_data"]:
502
+ culvert_loss += f'Ko: {unit_dict["conduit_data"]["outlet"]}, '
503
+
504
+ culvert_loss = culvert_loss.rstrip(", ")
505
+
506
+ return text, culvert_loss
250
507
 
251
508
  def _write( # noqa: PLR0913
252
509
  self,
510
+ writer,
253
511
  name,
254
512
  unit,
255
513
  subtype,
@@ -259,7 +517,7 @@ class StructureLogBuilder:
259
517
  weir_coefficient="",
260
518
  culvert_loss="",
261
519
  ):
262
- self._writer.writerow(
520
+ writer.writerow(
263
521
  [
264
522
  name,
265
523
  unit,
@@ -272,16 +530,64 @@ class StructureLogBuilder:
272
530
  ],
273
531
  )
274
532
 
275
- def create(self):
276
- # Read in the .dat file
277
- self._dat = DAT(self.dat_file_path)
533
+ def write_csv_output(self, file):
534
+ """
535
+ Take the current state of the instance (unit_store etc) and write it to the specified output file.
536
+ """
278
537
 
279
- # Create a new .csv file
280
- with open(self.csv_output_path, "w", newline="") as file:
281
- self._writer = csv.writer(file)
538
+ writer = csv.writer(file)
539
+
540
+ self._add_fields(writer)
541
+
542
+ for unit_dict in self.unit_store.values():
543
+ name = unit_dict["name"]
544
+ unit_type = unit_dict["type"]
545
+ subtype = unit_dict["subtype"]
546
+ comment = unit_dict["comment"]
282
547
 
283
- self._add_fields()
548
+ friction = self._format_friction(unit_dict)
284
549
 
285
- self._add_conduits()
550
+ culvert_loss = ""
286
551
 
287
- self._add_structures()
552
+ match unit_type:
553
+ case "BRIDGE":
554
+ dimensions = self._format_bridge_dimensions(unit_dict)
555
+ case "ORIFICE":
556
+ dimensions = self._format_orifice_dimensions(unit_dict)
557
+ case "WEIR" | "RNWEIR":
558
+ dimensions = self._format_weir_dimensions(unit_dict)
559
+ case "SLUICE":
560
+ dimensions = self._format_sluice_dimensions(unit_dict)
561
+ case "SPILL":
562
+ dimensions = self._format_spill_dimensions(unit_dict)
563
+ case "CONDUIT":
564
+ dimensions, culvert_loss = self._format_conduit_dimensions(unit_dict)
565
+ case _:
566
+ dimensions = ""
567
+
568
+ try:
569
+ weir_coefficient = unit_dict["dimensions"]["weir_coefficient"]
570
+ except KeyError:
571
+ weir_coefficient = ""
572
+
573
+ self._write(
574
+ writer,
575
+ name,
576
+ unit_type,
577
+ subtype,
578
+ comment,
579
+ friction,
580
+ dimensions,
581
+ weir_coefficient,
582
+ culvert_loss,
583
+ )
584
+
585
+ def create(self):
586
+ """
587
+ When using the toolbox wrapper or commandline entry point, this is the entrypoint to the structure logger code
588
+ """
589
+ self.dat = DAT(self.dat_file_path)
590
+ self.add_conduits()
591
+ self.add_structures()
592
+ with open(self.csv_output_path, "w", newline="") as file:
593
+ self.write_csv_output(file)