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