floodmodeller-api 0.4.2__py3-none-any.whl → 0.4.3__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 (178) hide show
  1. floodmodeller_api/__init__.py +8 -9
  2. floodmodeller_api/_base.py +184 -176
  3. floodmodeller_api/backup.py +273 -273
  4. floodmodeller_api/dat.py +909 -838
  5. floodmodeller_api/diff.py +136 -119
  6. floodmodeller_api/ied.py +307 -311
  7. floodmodeller_api/ief.py +647 -646
  8. floodmodeller_api/ief_flags.py +253 -253
  9. floodmodeller_api/inp.py +266 -268
  10. floodmodeller_api/libs/libifcoremd.dll +0 -0
  11. floodmodeller_api/libs/libifcoremt.so.5 +0 -0
  12. floodmodeller_api/libs/libifport.so.5 +0 -0
  13. floodmodeller_api/{libmmd.dll → libs/libimf.so} +0 -0
  14. floodmodeller_api/libs/libintlc.so.5 +0 -0
  15. floodmodeller_api/libs/libmmd.dll +0 -0
  16. floodmodeller_api/libs/libsvml.so +0 -0
  17. floodmodeller_api/libs/libzzn_read.so +0 -0
  18. floodmodeller_api/libs/zzn_read.dll +0 -0
  19. floodmodeller_api/logs/__init__.py +2 -2
  20. floodmodeller_api/logs/lf.py +320 -314
  21. floodmodeller_api/logs/lf_helpers.py +354 -346
  22. floodmodeller_api/logs/lf_params.py +643 -529
  23. floodmodeller_api/mapping.py +84 -0
  24. floodmodeller_api/test/__init__.py +4 -4
  25. floodmodeller_api/test/conftest.py +9 -8
  26. floodmodeller_api/test/test_backup.py +117 -117
  27. floodmodeller_api/test/test_dat.py +221 -92
  28. floodmodeller_api/test/test_data/All Units 4_6.DAT +1081 -1081
  29. floodmodeller_api/test/test_data/All Units 4_6.feb +1081 -1081
  30. floodmodeller_api/test/test_data/BRIDGE.DAT +926 -926
  31. floodmodeller_api/test/test_data/Culvert_Inlet_Outlet.dat +36 -36
  32. floodmodeller_api/test/test_data/Culvert_Inlet_Outlet.feb +36 -36
  33. floodmodeller_api/test/test_data/DamBreakADI.xml +52 -52
  34. floodmodeller_api/test/test_data/DamBreakFAST.xml +58 -58
  35. floodmodeller_api/test/test_data/DamBreakFAST_dy.xml +53 -53
  36. floodmodeller_api/test/test_data/DamBreakTVD.xml +55 -55
  37. floodmodeller_api/test/test_data/DefenceBreach.xml +53 -53
  38. floodmodeller_api/test/test_data/DefenceBreachFAST.xml +60 -60
  39. floodmodeller_api/test/test_data/DefenceBreachFAST_dy.xml +55 -55
  40. floodmodeller_api/test/test_data/Domain1+2_QH.xml +76 -76
  41. floodmodeller_api/test/test_data/Domain1_H.xml +41 -41
  42. floodmodeller_api/test/test_data/Domain1_Q.xml +41 -41
  43. floodmodeller_api/test/test_data/Domain1_Q_FAST.xml +48 -48
  44. floodmodeller_api/test/test_data/Domain1_Q_FAST_dy.xml +48 -48
  45. floodmodeller_api/test/test_data/Domain1_Q_xml_expected.json +263 -0
  46. floodmodeller_api/test/test_data/Domain1_W.xml +41 -41
  47. floodmodeller_api/test/test_data/EX1.DAT +321 -321
  48. floodmodeller_api/test/test_data/EX1.ext +107 -107
  49. floodmodeller_api/test/test_data/EX1.feb +320 -320
  50. floodmodeller_api/test/test_data/EX1.gxy +107 -107
  51. floodmodeller_api/test/test_data/EX17.DAT +421 -422
  52. floodmodeller_api/test/test_data/EX17.ext +213 -213
  53. floodmodeller_api/test/test_data/EX17.feb +422 -422
  54. floodmodeller_api/test/test_data/EX18.DAT +375 -375
  55. floodmodeller_api/test/test_data/EX18_DAT_expected.json +3876 -0
  56. floodmodeller_api/test/test_data/EX2.DAT +302 -302
  57. floodmodeller_api/test/test_data/EX3.DAT +926 -926
  58. floodmodeller_api/test/test_data/EX3_DAT_expected.json +16235 -0
  59. floodmodeller_api/test/test_data/EX3_IEF_expected.json +61 -0
  60. floodmodeller_api/test/test_data/EX6.DAT +2084 -2084
  61. floodmodeller_api/test/test_data/EX6.ext +532 -532
  62. floodmodeller_api/test/test_data/EX6.feb +2084 -2084
  63. floodmodeller_api/test/test_data/EX6_DAT_expected.json +31647 -0
  64. floodmodeller_api/test/test_data/Event Data Example.DAT +336 -336
  65. floodmodeller_api/test/test_data/Event Data Example.ext +107 -107
  66. floodmodeller_api/test/test_data/Event Data Example.feb +336 -336
  67. floodmodeller_api/test/test_data/Linked1D2D.xml +52 -52
  68. floodmodeller_api/test/test_data/Linked1D2DFAST.xml +53 -53
  69. floodmodeller_api/test/test_data/Linked1D2DFAST_dy.xml +48 -48
  70. floodmodeller_api/test/test_data/Linked1D2D_xml_expected.json +313 -0
  71. floodmodeller_api/test/test_data/blockage.dat +50 -50
  72. floodmodeller_api/test/test_data/blockage.ext +45 -45
  73. floodmodeller_api/test/test_data/blockage.feb +9 -9
  74. floodmodeller_api/test/test_data/blockage.gxy +71 -71
  75. floodmodeller_api/test/test_data/defaultUnits.dat +127 -127
  76. floodmodeller_api/test/test_data/defaultUnits.ext +45 -45
  77. floodmodeller_api/test/test_data/defaultUnits.feb +9 -9
  78. floodmodeller_api/test/test_data/defaultUnits.fmpx +58 -58
  79. floodmodeller_api/test/test_data/defaultUnits.gxy +85 -85
  80. floodmodeller_api/test/test_data/ex3.ief +20 -20
  81. floodmodeller_api/test/test_data/ex3.lf1 +2800 -2800
  82. floodmodeller_api/test/test_data/ex4.DAT +1374 -1374
  83. floodmodeller_api/test/test_data/ex4_changed.DAT +1374 -1374
  84. floodmodeller_api/test/test_data/example1.inp +329 -329
  85. floodmodeller_api/test/test_data/example2.inp +158 -158
  86. floodmodeller_api/test/test_data/example3.inp +297 -297
  87. floodmodeller_api/test/test_data/example4.inp +388 -388
  88. floodmodeller_api/test/test_data/example5.inp +147 -147
  89. floodmodeller_api/test/test_data/example6.inp +154 -154
  90. floodmodeller_api/test/test_data/jump.dat +176 -176
  91. floodmodeller_api/test/test_data/network.dat +1374 -1374
  92. floodmodeller_api/test/test_data/network.ext +45 -45
  93. floodmodeller_api/test/test_data/network.exy +1 -1
  94. floodmodeller_api/test/test_data/network.feb +45 -45
  95. floodmodeller_api/test/test_data/network.ied +45 -45
  96. floodmodeller_api/test/test_data/network.ief +20 -20
  97. floodmodeller_api/test/test_data/network.inp +147 -147
  98. floodmodeller_api/test/test_data/network.pxy +57 -57
  99. floodmodeller_api/test/test_data/network.zzd +122 -122
  100. floodmodeller_api/test/test_data/network_dat_expected.json +21837 -0
  101. floodmodeller_api/test/test_data/network_from_tabularCSV.csv +87 -87
  102. floodmodeller_api/test/test_data/network_ied_expected.json +287 -0
  103. floodmodeller_api/test/test_data/rnweir.dat +9 -9
  104. floodmodeller_api/test/test_data/rnweir.ext +45 -45
  105. floodmodeller_api/test/test_data/rnweir.feb +9 -9
  106. floodmodeller_api/test/test_data/rnweir.gxy +45 -45
  107. floodmodeller_api/test/test_data/rnweir_default.dat +74 -74
  108. floodmodeller_api/test/test_data/rnweir_default.ext +45 -45
  109. floodmodeller_api/test/test_data/rnweir_default.feb +9 -9
  110. floodmodeller_api/test/test_data/rnweir_default.fmpx +58 -58
  111. floodmodeller_api/test/test_data/rnweir_default.gxy +53 -53
  112. floodmodeller_api/test/test_data/unit checks.dat +16 -16
  113. floodmodeller_api/test/test_ied.py +29 -29
  114. floodmodeller_api/test/test_ief.py +125 -24
  115. floodmodeller_api/test/test_inp.py +47 -48
  116. floodmodeller_api/test/test_json.py +114 -0
  117. floodmodeller_api/test/test_logs_lf.py +48 -51
  118. floodmodeller_api/test/test_tool.py +165 -154
  119. floodmodeller_api/test/test_toolbox_structure_log.py +234 -239
  120. floodmodeller_api/test/test_xml2d.py +151 -156
  121. floodmodeller_api/test/test_zzn.py +36 -34
  122. floodmodeller_api/to_from_json.py +218 -0
  123. floodmodeller_api/tool.py +332 -330
  124. floodmodeller_api/toolbox/__init__.py +5 -5
  125. floodmodeller_api/toolbox/example_tool.py +45 -45
  126. floodmodeller_api/toolbox/model_build/__init__.py +2 -2
  127. floodmodeller_api/toolbox/model_build/add_siltation_definition.py +100 -94
  128. floodmodeller_api/toolbox/model_build/structure_log/__init__.py +1 -1
  129. floodmodeller_api/toolbox/model_build/structure_log/structure_log.py +287 -289
  130. floodmodeller_api/toolbox/model_build/structure_log_definition.py +76 -72
  131. floodmodeller_api/units/__init__.py +10 -10
  132. floodmodeller_api/units/_base.py +214 -209
  133. floodmodeller_api/units/boundaries.py +467 -469
  134. floodmodeller_api/units/comment.py +52 -55
  135. floodmodeller_api/units/conduits.py +382 -403
  136. floodmodeller_api/units/helpers.py +123 -132
  137. floodmodeller_api/units/iic.py +107 -101
  138. floodmodeller_api/units/losses.py +305 -308
  139. floodmodeller_api/units/sections.py +444 -445
  140. floodmodeller_api/units/structures.py +1690 -1684
  141. floodmodeller_api/units/units.py +93 -102
  142. floodmodeller_api/units/unsupported.py +44 -44
  143. floodmodeller_api/units/variables.py +87 -89
  144. floodmodeller_api/urban1d/__init__.py +11 -11
  145. floodmodeller_api/urban1d/_base.py +188 -177
  146. floodmodeller_api/urban1d/conduits.py +93 -85
  147. floodmodeller_api/urban1d/general_parameters.py +58 -58
  148. floodmodeller_api/urban1d/junctions.py +81 -79
  149. floodmodeller_api/urban1d/losses.py +81 -74
  150. floodmodeller_api/urban1d/outfalls.py +114 -107
  151. floodmodeller_api/urban1d/raingauges.py +111 -108
  152. floodmodeller_api/urban1d/subsections.py +92 -93
  153. floodmodeller_api/urban1d/xsections.py +147 -141
  154. floodmodeller_api/util.py +77 -21
  155. floodmodeller_api/validation/parameters.py +660 -660
  156. floodmodeller_api/validation/urban_parameters.py +388 -404
  157. floodmodeller_api/validation/validation.py +110 -112
  158. floodmodeller_api/version.py +1 -1
  159. floodmodeller_api/xml2d.py +688 -684
  160. floodmodeller_api/xml2d_template.py +37 -37
  161. floodmodeller_api/zzn.py +387 -365
  162. {floodmodeller_api-0.4.2.dist-info → floodmodeller_api-0.4.3.dist-info}/LICENSE.txt +13 -13
  163. {floodmodeller_api-0.4.2.dist-info → floodmodeller_api-0.4.3.dist-info}/METADATA +82 -82
  164. floodmodeller_api-0.4.3.dist-info/RECORD +179 -0
  165. {floodmodeller_api-0.4.2.dist-info → floodmodeller_api-0.4.3.dist-info}/WHEEL +1 -1
  166. floodmodeller_api-0.4.3.dist-info/entry_points.txt +3 -0
  167. floodmodeller_api/libifcoremd.dll +0 -0
  168. floodmodeller_api/test/test_data/EX3.bmp +0 -0
  169. floodmodeller_api/test/test_data/test_output.csv +0 -87
  170. floodmodeller_api/zzn_read.dll +0 -0
  171. floodmodeller_api-0.4.2.data/scripts/fmapi-add_siltation.bat +0 -2
  172. floodmodeller_api-0.4.2.data/scripts/fmapi-add_siltation.py +0 -3
  173. floodmodeller_api-0.4.2.data/scripts/fmapi-structure_log.bat +0 -2
  174. floodmodeller_api-0.4.2.data/scripts/fmapi-structure_log.py +0 -3
  175. floodmodeller_api-0.4.2.data/scripts/fmapi-toolbox.bat +0 -2
  176. floodmodeller_api-0.4.2.data/scripts/fmapi-toolbox.py +0 -41
  177. floodmodeller_api-0.4.2.dist-info/RECORD +0 -169
  178. {floodmodeller_api-0.4.2.dist-info → floodmodeller_api-0.4.3.dist-info}/top_level.txt +0 -0
@@ -1,314 +1,320 @@
1
- """
2
- Flood Modeller Python API
3
- Copyright (C) 2023 Jacobs U.K. Limited
4
-
5
- This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
6
- as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
7
-
8
- This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
9
- of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
10
-
11
- You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
12
-
13
- If you have any query about this program or this License, please contact us at support@floodmodeller.com or write to the following
14
- address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London, SE1 2QG, United Kingdom.
15
- """
16
-
17
- from pathlib import Path
18
- from typing import Optional, Union
19
-
20
- import pandas as pd
21
-
22
- from .._base import FMFile
23
- from .lf_helpers import state_factory
24
- from .lf_params import lf1_steady_data_to_extract, lf1_unsteady_data_to_extract, lf2_data_to_extract
25
-
26
-
27
- class LF(FMFile):
28
- """Reads and processes Flood Modeller log file
29
-
30
- Args:
31
- lf1_filepath (str): Full filepath to model log file
32
- data_to_extract (dict): Dictionary defining each line type to parse
33
- steady (bool): True if for a steady-state simulation
34
-
35
- Output:
36
- Initiates 'LF' class object
37
- """
38
-
39
- def __init__(
40
- self,
41
- lf_filepath: Optional[Union[str, Path]],
42
- data_to_extract: dict,
43
- steady: bool = False,
44
- ):
45
- try:
46
- self._filepath = lf_filepath
47
- FMFile.__init__(self)
48
-
49
- self._data_to_extract = data_to_extract
50
- self._init_counters()
51
- self._init_parsers()
52
- self._state = state_factory(steady, self._extracted_data)
53
-
54
- self._read()
55
-
56
- except Exception as e:
57
- self._handle_exception(e, when="read")
58
-
59
- def _read(self, force_reread: bool = False, suppress_final_step: bool = False):
60
- # Read LF file
61
- with open(self._filepath, "r") as lf_file:
62
- self._raw_data = [line.rstrip("\n") for line in lf_file.readlines()]
63
-
64
- # Force rereading from start of file
65
- if force_reread is True:
66
- self._del_attributes()
67
- self._init_counters()
68
- self._init_parsers()
69
-
70
- # Process file
71
- self._update_data()
72
-
73
- if not suppress_final_step:
74
- self._set_attributes()
75
-
76
- def read(self, force_reread: bool = False, suppress_final_step: bool = False) -> None:
77
- """Reads log file
78
-
79
- Args:
80
- force_reread (bool): If False, starts reading from where it stopped last time. If True, starts reading from the start of the file.
81
- suppress_final_step (bool): If False, dataframes and dictionary are not created as attributes.
82
-
83
- """
84
-
85
- self._read(force_reread, suppress_final_step)
86
-
87
- def _init_counters(self):
88
- """Initialises counters that keep track of file during simulation"""
89
-
90
- self._no_lines = 0 # number of lines that have been read so far
91
- self._no_iters = 0 # number of iterations so far
92
-
93
- def _init_parsers(self):
94
- """Creates dictionary of Parser objects for each entry in data_to_extract"""
95
-
96
- self._extracted_data = {}
97
-
98
- for key in self._data_to_extract:
99
- subdictionary = self._data_to_extract[key]
100
- subdictionary_class = subdictionary["class"]
101
- subdictionary_kwargs = {k: v for k, v in subdictionary.items() if k != "class"}
102
- subdictionary_kwargs["name"] = key
103
- self._extracted_data[key] = subdictionary_class(**subdictionary_kwargs)
104
-
105
- def _update_data(self):
106
- """Updates value of each Parser object based on raw data"""
107
-
108
- # self._print_no_lines()
109
-
110
- # loop through lines that haven't already been read
111
- raw_lines = self._raw_data[self._no_lines :]
112
- for raw_line in raw_lines:
113
- # loop through parser types
114
- for key in self._data_to_extract:
115
- parser = self._extracted_data[key]
116
-
117
- # lines which start with prefix
118
- if raw_line.startswith(parser.prefix):
119
- # store everything after prefix
120
- end_of_line = raw_line.split(parser.prefix)[1].lstrip()
121
- parser.process_line(end_of_line)
122
-
123
- # index marks the end of an iteration
124
- if parser.is_index is True:
125
- self._sync_cols()
126
- self._no_iters += 1
127
-
128
- # update counter
129
- self._no_lines += 1
130
-
131
- # self._print_no_lines()
132
-
133
- def _get_index(self):
134
- """Finds key and dataframe for variable that is the index"""
135
-
136
- for key in self._data_to_extract:
137
- try:
138
- self._data_to_extract[key]["is_index"]
139
- index_key = key
140
- index_df = self._extracted_data[key].data.get_value()
141
- return index_key, index_df
142
-
143
- except KeyError:
144
- pass
145
-
146
- raise Exception("No index variable found")
147
-
148
- def _set_attributes(self):
149
- """Makes each Parser value an attribute; "last" values in dictionary"""
150
-
151
- index_key, index_df = self._get_index()
152
-
153
- info = {}
154
-
155
- for key in self._data_to_extract:
156
- data_type = self._data_to_extract[key]["data_type"]
157
- value = self._extracted_data[key].data.get_value(index_key, index_df)
158
-
159
- if data_type == "all":
160
- setattr(self, key, value)
161
- elif data_type == "last" and value is not None:
162
- info[key] = value
163
-
164
- self.info = info
165
-
166
- def _del_attributes(self):
167
- """Deletes each Parser value direct attribute of LF"""
168
-
169
- for key in self._data_to_extract:
170
- data_type = self._data_to_extract[key]["data_type"]
171
- if data_type == "all":
172
- delattr(self, key)
173
-
174
- delattr(self, "info")
175
-
176
- def to_dataframe(self) -> pd.DataFrame:
177
- """Collects parameter values that change throughout simulation into a dataframe
178
-
179
- Returns:
180
- pd.DataFrame: DataFrame of log file parameters indexed by simulation time (unsteady) or network iterations (steady)
181
- """
182
-
183
- # TODO: make more like ZZN.to_dataframe
184
-
185
- data_type_all = {
186
- k: getattr(self, k) for k, v in self._data_to_extract.items() if v["data_type"] == "all"
187
- }
188
-
189
- df = pd.concat(data_type_all, axis=1)
190
- df.columns = df.columns.droplevel()
191
-
192
- df.sort_index(inplace=True)
193
-
194
- return df
195
-
196
- def _sync_cols(self):
197
- """Ensures Parser values (of type "all") have an entry each iteration"""
198
-
199
- # loop through parser types
200
- for key in self._data_to_extract:
201
- parser = self._extracted_data[key]
202
-
203
- # sync parser types that are not the index
204
- if parser.is_index is False:
205
- # if their number of values is not in sync
206
- if parser.data_type == "all" and parser.data.no_values < (
207
- self._no_iters + int(parser.before_index)
208
- ):
209
- # append nan to the list
210
- parser.data.update(parser._nan)
211
-
212
- def _print_no_lines(self):
213
- """Prints number of lines that have been read so far"""
214
-
215
- print("Last line read: " + str(self._no_lines))
216
-
217
- def report_progress(self) -> float:
218
- """Returns progress for unsteady simulations
219
-
220
- Returns:
221
- float: Last progress percentage recorded in log file
222
- """
223
-
224
- return self._state.report_progress()
225
-
226
-
227
- class LF1(LF):
228
- """Reads and processes Flood Modeller 1D log file '.lf1'
229
-
230
- Args:
231
- lf1_filepath (str): Full filepath to model lf1 file
232
- steady (bool): True for steady-state simulations
233
-
234
- **Attributes (unsteady)**
235
-
236
- Args:
237
- info (dict): Parameters with one value per simulation
238
- mass_error (pandas.DataFrame): Mass error
239
- timestep (pandas.DataFrame): Timestep
240
- elapsed (pandas.DataFrame): Elapsed
241
- simulated (pandas.DataFrame): Simulated
242
- iterations (pandas.DataFrame): PlotI1
243
- convergence (pandas.DataFrame): PlotC1
244
- flow (pandas.DataFrame): PlotF1
245
-
246
- **Attributes (steady)**
247
-
248
- Args:
249
- info (dict): Parameters with one value per simulation
250
- network_iteration (pandas.DataFrame): Network iteration
251
- largest_change_in_split_from_last_iteration (pandas.DataFrame): Largest change in split from last iteration
252
-
253
- Output:
254
- Initiates 'LF1' class object
255
- """
256
-
257
- _filetype: str = "LF1"
258
- _suffix: str = ".lf1"
259
-
260
- def __init__(self, lf_filepath: Optional[Union[str, Path]], steady: bool = False):
261
- if steady is False:
262
- data_to_extract = lf1_unsteady_data_to_extract
263
- else:
264
- data_to_extract = lf1_steady_data_to_extract
265
-
266
- super().__init__(lf_filepath, data_to_extract, steady)
267
-
268
-
269
- class LF2(LF):
270
- """Reads and processes Flood Modeller 1D log file '.lf2'
271
-
272
- Args:
273
- lf2_filepath (str): Full filepath to model lf2 file
274
-
275
- **Attributes**
276
-
277
- Args:
278
- info (dict): Parameters with one value per simulation
279
- simulated (pandas.DataFrame): Simulated
280
- wet_cells (pandas.DataFrame): Wet cells
281
- 2D_boundary_inflow (pandas.DataFrame): 2D boundary inflow
282
- 2D_boundary_outflow (pandas.DataFrame): 2D boundary outflow
283
- 1D_link_flow (pandas.DataFrame): 1D link flow
284
- change_in_volume (pandas.DataFrame): Change in volume
285
- volume (pandas.DataFrame): Volume
286
- inst_mass_err (pandas.DataFrame): Inst mass error
287
- mass_error (pandas.DataFrame): Mass error
288
- largest_cr (pandas.DataFrame): Largest Cr
289
- elapsed (pandas.DataFrame): Elapsed
290
-
291
- Output:
292
- Initiates 'LF2' class object
293
- """
294
-
295
- _filetype: str = "LF2"
296
- _suffix: str = ".lf2"
297
-
298
- def __init__(self, lf_filepath: Optional[Union[str, Path]]):
299
- data_to_extract = {
300
- **lf1_unsteady_data_to_extract,
301
- **lf2_data_to_extract,
302
- }
303
-
304
- super().__init__(lf_filepath, data_to_extract, steady=False)
305
-
306
-
307
- def lf_factory(filepath: str, suffix: str, steady: bool) -> LF:
308
- if suffix == "lf1":
309
- return LF1(filepath, steady)
310
- elif suffix == "lf2":
311
- return LF2(filepath)
312
- else:
313
- flow_type = "steady" if steady else "unsteady"
314
- raise ValueError(f"Unexpected log file type {suffix} for {flow_type} flow")
1
+ """
2
+ Flood Modeller Python API
3
+ Copyright (C) 2024 Jacobs U.K. Limited
4
+
5
+ This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License
6
+ as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
7
+
8
+ This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
9
+ of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
10
+
11
+ You should have received a copy of the GNU General Public License along with this program. If not, see https://www.gnu.org/licenses/.
12
+
13
+ If you have any query about this program or this License, please contact us at support@floodmodeller.com or write to the following
14
+ address: Jacobs UK Limited, Flood Modeller, Cottons Centre, Cottons Lane, London, SE1 2QG, United Kingdom.
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import TYPE_CHECKING
20
+
21
+ import pandas as pd
22
+
23
+ from .._base import FMFile
24
+ from .lf_helpers import state_factory
25
+ from .lf_params import lf1_steady_data_to_extract, lf1_unsteady_data_to_extract, lf2_data_to_extract
26
+
27
+ if TYPE_CHECKING:
28
+ from pathlib import Path
29
+
30
+
31
+ class LF(FMFile):
32
+ """Reads and processes Flood Modeller log file
33
+
34
+ Args:
35
+ lf1_filepath (str): Full filepath to model log file
36
+ data_to_extract (dict): Dictionary defining each line type to parse
37
+ steady (bool): True if for a steady-state simulation
38
+
39
+ Output:
40
+ Initiates 'LF' class object
41
+ """
42
+
43
+ def __init__(
44
+ self,
45
+ lf_filepath: str | Path | None,
46
+ data_to_extract: dict,
47
+ steady: bool = False,
48
+ ):
49
+ try:
50
+ FMFile.__init__(self, lf_filepath)
51
+
52
+ self._data_to_extract = data_to_extract
53
+ self._init_counters()
54
+ self._init_parsers()
55
+ self._state = state_factory(steady, self._extracted_data)
56
+
57
+ self._read()
58
+
59
+ except Exception as e:
60
+ self._handle_exception(e, when="read")
61
+
62
+ def _read(self, force_reread: bool = False, suppress_final_step: bool = False):
63
+ # Read LF file
64
+ with open(self._filepath) as lf_file:
65
+ self._raw_data = [line.rstrip("\n") for line in lf_file.readlines()]
66
+
67
+ # Force rereading from start of file
68
+ if force_reread is True:
69
+ self._del_attributes()
70
+ self._init_counters()
71
+ self._init_parsers()
72
+
73
+ # Process file
74
+ self._update_data()
75
+
76
+ if not suppress_final_step:
77
+ self._set_attributes()
78
+
79
+ def read(self, force_reread: bool = False, suppress_final_step: bool = False) -> None:
80
+ """Reads log file
81
+
82
+ Args:
83
+ force_reread (bool): If False, starts reading from where it stopped last time. If True, starts reading from the start of the file.
84
+ suppress_final_step (bool): If False, dataframes and dictionary are not created as attributes.
85
+
86
+ """
87
+
88
+ self._read(force_reread, suppress_final_step)
89
+
90
+ def _init_counters(self):
91
+ """Initialises counters that keep track of file during simulation"""
92
+
93
+ self._no_lines = 0 # number of lines that have been read so far
94
+ self._no_iters = 0 # number of iterations so far
95
+
96
+ def _init_parsers(self):
97
+ """Creates dictionary of Parser objects for each entry in data_to_extract"""
98
+
99
+ self._extracted_data = {}
100
+
101
+ for key in self._data_to_extract:
102
+ subdictionary = self._data_to_extract[key]
103
+ subdictionary_class = subdictionary["class"]
104
+ subdictionary_kwargs = {k: v for k, v in subdictionary.items() if k != "class"}
105
+ subdictionary_kwargs["name"] = key
106
+ self._extracted_data[key] = subdictionary_class(**subdictionary_kwargs)
107
+
108
+ def _update_data(self):
109
+ """Updates value of each Parser object based on raw data"""
110
+
111
+ # loop through lines that haven't already been read
112
+ raw_lines = self._raw_data[self._no_lines :]
113
+ for raw_line in raw_lines:
114
+ # loop through parser types
115
+ for key in self._data_to_extract:
116
+ parser = self._extracted_data[key]
117
+
118
+ # lines which start with prefix
119
+ if raw_line.startswith(parser.prefix):
120
+ # store everything after prefix
121
+ end_of_line = raw_line.split(parser.prefix)[1].lstrip()
122
+ parser.process_line(end_of_line)
123
+
124
+ # index marks the end of an iteration
125
+ if parser.is_index is True:
126
+ self._sync_cols()
127
+ self._no_iters += 1
128
+
129
+ # update counter
130
+ self._no_lines += 1
131
+
132
+ def _get_index(self):
133
+ """Finds key and dataframe for variable that is the index"""
134
+
135
+ for key in self._data_to_extract:
136
+ try:
137
+ self._data_to_extract[key]["is_index"]
138
+ index_key = key
139
+ index_df = self._extracted_data[key].data.get_value()
140
+ return index_key, index_df
141
+
142
+ except KeyError:
143
+ pass
144
+
145
+ raise Exception("No index variable found")
146
+
147
+ def _set_attributes(self):
148
+ """Makes each Parser value an attribute; "last" values in dictionary"""
149
+
150
+ index_key, index_df = self._get_index()
151
+
152
+ info = {}
153
+
154
+ for key in self._data_to_extract:
155
+ data_type = self._data_to_extract[key]["data_type"]
156
+ value = self._extracted_data[key].data.get_value(index_key, index_df)
157
+
158
+ if data_type == "all":
159
+ setattr(self, key, value)
160
+ elif data_type == "last" and value is not None:
161
+ info[key] = value
162
+
163
+ self.info = info
164
+
165
+ def _del_attributes(self):
166
+ """Deletes each Parser value direct attribute of LF"""
167
+
168
+ for key in self._data_to_extract:
169
+ data_type = self._data_to_extract[key]["data_type"]
170
+ if data_type == "all":
171
+ delattr(self, key)
172
+
173
+ delattr(self, "info")
174
+
175
+ def to_dataframe(self, *, include_tuflow: bool = False) -> pd.DataFrame:
176
+ """Collects parameter values that change throughout simulation into a dataframe
177
+
178
+ Args:
179
+ include_tuflow (bool): Include diagnostics for linked TUFLOW models
180
+
181
+ Returns:
182
+ pd.DataFrame: DataFrame of log file parameters indexed by simulation time (unsteady) or network iterations (steady)
183
+ """
184
+
185
+ # TODO: make more like ZZN.to_dataframe
186
+
187
+ data_type_all = {
188
+ k: getattr(self, k)
189
+ for k, v in self._data_to_extract.items()
190
+ if v["data_type"] == "all" and (include_tuflow or "tuflow" not in k)
191
+ }
192
+
193
+ df = pd.concat(data_type_all, axis=1)
194
+ df.columns = df.columns.droplevel()
195
+
196
+ df.sort_index(inplace=True)
197
+
198
+ return df
199
+
200
+ def _sync_cols(self):
201
+ """Ensures Parser values (of type "all") have an entry each iteration"""
202
+
203
+ # loop through parser types
204
+ for key in self._data_to_extract:
205
+ parser = self._extracted_data[key]
206
+
207
+ # sync parser types that are not the index
208
+ if (
209
+ parser.is_index is False # if their number of values is not in sync
210
+ and parser.data_type == "all"
211
+ and parser.data.no_values < (self._no_iters + int(parser.before_index))
212
+ ):
213
+ # append nan to the list
214
+ parser.data.update(parser._nan)
215
+
216
+ def _print_no_lines(self):
217
+ """Prints number of lines that have been read so far"""
218
+
219
+ print("Last line read: " + str(self._no_lines))
220
+
221
+ def report_progress(self) -> float:
222
+ """Returns progress for unsteady simulations
223
+
224
+ Returns:
225
+ float: Last progress percentage recorded in log file
226
+ """
227
+
228
+ return self._state.report_progress()
229
+
230
+
231
+ class LF1(LF):
232
+ """Reads and processes Flood Modeller 1D log file '.lf1'
233
+
234
+ Args:
235
+ lf1_filepath (str): Full filepath to model lf1 file
236
+ steady (bool): True for steady-state simulations
237
+
238
+ **Attributes (unsteady)**
239
+
240
+ Args:
241
+ info (dict): Parameters with one value per simulation
242
+ mass_error (pandas.DataFrame): Mass error
243
+ timestep (pandas.DataFrame): Timestep
244
+ elapsed (pandas.DataFrame): Elapsed
245
+ tuflow_vol (pandas.DataFrame): TUFLOW HPC Vol
246
+ tuflow_n_wet (pandas.DataFrame): TUFLOW HPC nWet
247
+ tuflow_dt (pandas.DataFrame): TUFLOW HPC dt
248
+ simulated (pandas.DataFrame): Simulated
249
+ iterations (pandas.DataFrame): PlotI1
250
+ convergence (pandas.DataFrame): PlotC1
251
+ flow (pandas.DataFrame): PlotF1
252
+
253
+ **Attributes (steady)**
254
+
255
+ Args:
256
+ info (dict): Parameters with one value per simulation
257
+ network_iteration (pandas.DataFrame): Network iteration
258
+ largest_change_in_split_from_last_iteration (pandas.DataFrame): Largest change in split from last iteration
259
+
260
+ Output:
261
+ Initiates 'LF1' class object
262
+ """
263
+
264
+ _filetype: str = "LF1"
265
+ _suffix: str = ".lf1"
266
+
267
+ def __init__(self, lf_filepath: str | Path | None, steady: bool = False):
268
+ if steady is False:
269
+ data_to_extract = lf1_unsteady_data_to_extract
270
+ else:
271
+ data_to_extract = lf1_steady_data_to_extract
272
+
273
+ super().__init__(lf_filepath, data_to_extract, steady)
274
+
275
+
276
+ class LF2(LF):
277
+ """Reads and processes Flood Modeller 1D log file '.lf2'
278
+
279
+ Args:
280
+ lf2_filepath (str): Full filepath to model lf2 file
281
+
282
+ **Attributes**
283
+
284
+ Args:
285
+ info (dict): Parameters with one value per simulation
286
+ simulated (pandas.DataFrame): Simulated
287
+ wet_cells (pandas.DataFrame): Wet cells
288
+ 2D_boundary_inflow (pandas.DataFrame): 2D boundary inflow
289
+ 2D_boundary_outflow (pandas.DataFrame): 2D boundary outflow
290
+ 1D_link_flow (pandas.DataFrame): 1D link flow
291
+ change_in_volume (pandas.DataFrame): Change in volume
292
+ volume (pandas.DataFrame): Volume
293
+ inst_mass_err (pandas.DataFrame): Inst mass error
294
+ mass_error (pandas.DataFrame): Mass error
295
+ largest_cr (pandas.DataFrame): Largest Cr
296
+ elapsed (pandas.DataFrame): Elapsed
297
+
298
+ Output:
299
+ Initiates 'LF2' class object
300
+ """
301
+
302
+ _filetype: str = "LF2"
303
+ _suffix: str = ".lf2"
304
+
305
+ def __init__(self, lf_filepath: str | Path | None):
306
+ data_to_extract = {
307
+ **lf1_unsteady_data_to_extract,
308
+ **lf2_data_to_extract,
309
+ }
310
+
311
+ super().__init__(lf_filepath, data_to_extract, steady=False)
312
+
313
+
314
+ def lf_factory(filepath: str, suffix: str, steady: bool) -> LF:
315
+ if suffix == "lf1":
316
+ return LF1(filepath, steady)
317
+ if suffix == "lf2":
318
+ return LF2(filepath)
319
+ flow_type = "steady" if steady else "unsteady"
320
+ raise ValueError(f"Unexpected log file type {suffix} for {flow_type} flow")