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
floodmodeller_api/ied.py CHANGED
@@ -1,311 +1,307 @@
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
- from . import units
21
- from ._base import FMFile
22
-
23
-
24
- class IED(FMFile):
25
- """Reads and write Flood Modeller event data format '.ied'
26
- Args:
27
- ied_filepath (str, optional): Full filepath to ied file. If not specified, a new IED class will be created.
28
-
29
- Output:
30
- Initiates 'IED' class object
31
-
32
- Raises:
33
- TypeError: Raised if ied_filepath does not point to a .ied file
34
- FileNotFoundError: Raised if ied_filepath points to a file which does not exist
35
- """
36
-
37
- _filetype: str = "IED"
38
- _suffix: str = ".ied"
39
-
40
- def __init__(self, ied_filepath: Optional[Union[str, Path]] = None):
41
- try:
42
- self._filepath = ied_filepath
43
- if self._filepath is not None:
44
- FMFile.__init__(self)
45
-
46
- self._read()
47
-
48
- else:
49
- # No filepath specified, create new 'blank' IED in memory
50
- self._ied_struct = []
51
- self._raw_data = []
52
-
53
- self._get_unit_definitions()
54
-
55
- except Exception as e:
56
- self._handle_exception(e, when="read")
57
-
58
- def _read(self):
59
- # Read IED data
60
- with open(self._filepath, "r") as ied_file:
61
- self._raw_data = [line.rstrip("\n") for line in ied_file.readlines()]
62
-
63
- # Generate IED structure
64
- self._update_ied_struct()
65
-
66
- def _write(self) -> str: # noqa: C901
67
- """Returns string representation of the current IED data"""
68
- try:
69
- block_shift = 0
70
- existing_units = {
71
- "boundaries": [],
72
- "structures": [],
73
- "sections": [],
74
- "conduits": [],
75
- "losses": [],
76
- }
77
-
78
- for block in self._ied_struct:
79
- # Check for all supported boundary types
80
- if block["Type"] in units.SUPPORTED_UNIT_TYPES:
81
- unit_data = self._raw_data[
82
- block["start"] + block_shift : block["end"] + 1 + block_shift
83
- ]
84
- prev_block_len = len(unit_data)
85
- if units.SUPPORTED_UNIT_TYPES[block["Type"]]["has_subtype"]:
86
- unit_name = unit_data[2][:12].strip()
87
- else:
88
- unit_name = unit_data[1][:12].strip()
89
-
90
- # Get unit object
91
- unit_group = getattr(self, units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"])
92
- if unit_name in unit_group:
93
- # block still exists
94
- new_unit_data = unit_group[unit_name]._write()
95
- existing_units[units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"]].append(
96
- unit_name
97
- )
98
- else:
99
- # Bdy block has been deleted
100
- new_unit_data = []
101
-
102
- new_block_len = len(new_unit_data)
103
- self._raw_data[
104
- block["start"] + block_shift : block["end"] + 1 + block_shift
105
- ] = new_unit_data
106
- # adjust block shift for change in number of lines in bdy block
107
- block_shift += new_block_len - prev_block_len
108
-
109
- # Add any new units
110
- for group_name, _units in existing_units.items():
111
- for name, unit in getattr(self, group_name).items():
112
- if name not in _units:
113
- # Newly added unit
114
- # Ensure that the 'name' attribute matches name key in boundaries
115
- self._raw_data.extend(unit._write())
116
-
117
- # Update ied_struct
118
- self._update_ied_struct()
119
-
120
- # Update unit names
121
- for unit_group, unit_group_name in [
122
- (self.boundaries, "boundaries"),
123
- (self.sections, "sections"),
124
- (self.structures, "structures"),
125
- (self.conduits, "conduits"),
126
- (self.losses, "losses"),
127
- ]:
128
- for name, unit in unit_group.copy().items():
129
- if name != unit.name:
130
- # Check if new name already exists as a label
131
- if unit.name in unit_group:
132
- raise Exception(
133
- f'Error: Cannot update label "{name}" to "{unit.name}" because "{unit.name}" already exists in the Network {unit_group_name} group'
134
- )
135
- unit_group[unit.name] = unit
136
- del unit_group[name]
137
-
138
- ied_string = ""
139
- for line in self._raw_data:
140
- ied_string += line + "\n"
141
-
142
- return ied_string
143
-
144
- except Exception as e:
145
- self._handle_exception(e, when="write")
146
-
147
- def _get_unit_definitions(self):
148
- # Get unit definitions
149
- self.sections = {}
150
- self.boundaries = {}
151
- self.structures = {}
152
- self.conduits = {}
153
- self.losses = {}
154
- self._unsupported = {}
155
- self._all_units = []
156
- for block in self._ied_struct:
157
- unit_data = self._raw_data[block["start"] : block["end"] + 1]
158
- # Check for all supported boundary types, starting just with QTBDY type
159
- if block["Type"] in units.SUPPORTED_UNIT_TYPES:
160
- # Check to see whether unit type has associated subtypes so that unit name can be correctly assigned
161
- if units.SUPPORTED_UNIT_TYPES[block["Type"]]["has_subtype"]:
162
- # Takes first 12 characters as name
163
- unit_name = unit_data[2][:12].strip()
164
- else:
165
- unit_name = unit_data[1][:12].strip()
166
-
167
- # Create instance of unit and add to relevant group
168
- unit_group = getattr(self, units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"])
169
- if unit_name in unit_group:
170
- raise Exception(
171
- f'Duplicate label ({unit_name}) encountered within category: {units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"]}'
172
- )
173
- else:
174
- unit_group[unit_name] = eval(f'units.{block["Type"]}({unit_data})')
175
-
176
- self._all_units.append(unit_group[unit_name])
177
-
178
- elif block["Type"] in units.UNSUPPORTED_UNIT_TYPES:
179
- # Check to see whether unit type has associated subtypes so that unit name can be correctly assigned
180
- if units.UNSUPPORTED_UNIT_TYPES[block["Type"]]["has_subtype"]:
181
- # Takes first 12 characters as name
182
- unit_name = unit_data[2][:12].strip()
183
- subtype = True
184
- else:
185
- unit_name = unit_data[1][:12].strip()
186
- subtype = False
187
-
188
- # _label_len = _to_int(params[5], 12) # label length
189
- self._unsupported[f"{unit_name} ({block['Type']})"] = units.UNSUPPORTED(
190
- unit_data,
191
- 12,
192
- unit_name=unit_name,
193
- unit_type=block["Type"],
194
- subtype=subtype,
195
- )
196
- self._all_units.append(self._unsupported[f"{unit_name} ({block['Type']})"])
197
-
198
- print()
199
-
200
- def _update_ied_struct(self): # noqa: C901
201
- # Generate IED structure
202
- ied_struct = []
203
- in_block = False
204
- bdy_block = {}
205
- comment_n = None
206
- in_comment = False
207
-
208
- for idx, line in enumerate(self._raw_data):
209
- # Deal with comment blocks explicitly as they could contain unit keywords
210
- if in_comment and comment_n is None:
211
- comment_n = int(line.strip())
212
- continue
213
- elif in_comment:
214
- comment_n -= 1
215
- if comment_n == 0:
216
- bdy_block["end"] = idx # add ending index
217
- # append existing bdy block to the ied_struct
218
- ied_struct.append(bdy_block)
219
- bdy_block = {} # reset bdy block
220
- in_comment = False
221
- in_block = False
222
- comment_n = None
223
- continue
224
- else:
225
- continue # move onto next line as still in comment block
226
-
227
- if line == "COMMENT":
228
- in_comment = True
229
- if in_block is True:
230
- bdy_block["end"] = idx - 1 # add ending index
231
- # append existing bdy block to the ied_struct
232
- ied_struct.append(bdy_block)
233
- bdy_block = {} # reset bdy block
234
- # start new block for COMMENT
235
- bdy_block["Type"] = line.split(" ")[0]
236
- bdy_block["start"] = idx # add starting index
237
- continue
238
-
239
- if len(line.split(" ")[0]) > 1:
240
- if line.split(" ")[0] in units.ALL_UNIT_TYPES:
241
- if in_block is True:
242
- bdy_block["end"] = idx - 1 # add ending index
243
- # append existing bdy block to the ief_struct
244
- ied_struct.append(bdy_block)
245
- bdy_block = {} # reset bdy block
246
- in_block = True
247
- bdy_block["Type"] = line.split(" ")[0] # start new bdy block
248
- bdy_block["start"] = idx # add starting index
249
-
250
- elif " ".join(line.split(" ")[:2]) in units.ALL_UNIT_TYPES:
251
- if in_block is True:
252
- bdy_block["end"] = idx - 1 # add ending index
253
- # append existing bdy block to the ief_struct
254
- ied_struct.append(bdy_block)
255
- bdy_block = {} # reset bdy block
256
- in_block = True
257
- bdy_block["Type"] = " ".join(line.split(" ")[:2]) # start new bdy block
258
- bdy_block["start"] = idx # add starting index
259
- else:
260
- continue
261
- elif line in units.ALL_UNIT_TYPES:
262
- if in_block is True:
263
- bdy_block["end"] = idx - 1 # add ending index
264
- # append existing bdy block to the ief_struct
265
- ied_struct.append(bdy_block)
266
- bdy_block = {} # reset bdy block
267
- in_block = True
268
- bdy_block["Type"] = line # start new bdy block
269
- bdy_block["start"] = idx # add starting index
270
- else:
271
- continue
272
-
273
- if len(bdy_block) != 0:
274
- # Only adds end block if there is a bdy block present (i.e. an empty IED stays empty)
275
- # add ending index for final block
276
- bdy_block["end"] = len(self._raw_data) - 1
277
- ied_struct.append(bdy_block) # add final block
278
-
279
- self._ied_struct = ied_struct
280
- pass
281
-
282
- def diff(self, other: "IED", force_print: bool = False) -> None:
283
- """Compares the IED class against another IED class to check whether they are
284
- equivalent, or if not, what the differences are. Two instances of an IED class are
285
- deemed equivalent if all of their attributes are equal except for the filepath and
286
- raw data. For example, two IED files from different filepaths that had the same
287
- data except maybe some differences in decimal places and some default parameters
288
- ommitted, would be classed as equaivalent as they would produce the same IED instance
289
- and write the exact same data.
290
-
291
- The result is printed to the console. If you need to access the returned data, use
292
- the method ``IED._get_diff()``
293
-
294
- Args:
295
- other (floodmodeller_api.IED): Other instance of an IED class
296
- force_print (bool): Forces the API to print every difference found, rather than
297
- just the first 25 differences. Defaults to False.
298
- """
299
- self._diff(other, force_print=force_print)
300
-
301
- def update(self) -> None:
302
- """Updates the existing IED based on any altered attributes"""
303
-
304
- self._update()
305
-
306
- def save(self, filepath: Union[str, Path]) -> None:
307
- """Saves the IED to the given location, if pointing to an existing file it will be overwritten.
308
- Once saved, the IED() class will continue working from the saved location, therefore any further calls to IED.update() will update in the latest saved location
309
- rather than the original source IED used to construct the class"""
310
-
311
- self._save(filepath)
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, Any
20
+
21
+ from . import units
22
+ from ._base import FMFile
23
+
24
+ if TYPE_CHECKING:
25
+ from pathlib import Path
26
+
27
+
28
+ class IED(FMFile):
29
+ """Reads and write Flood Modeller event data format '.ied'
30
+ Args:
31
+ ied_filepath (str, optional): Full filepath to ied file. If not specified, a new IED class will be created.
32
+
33
+ Output:
34
+ Initiates 'IED' class object
35
+
36
+ Raises:
37
+ TypeError: Raised if ied_filepath does not point to a .ied file
38
+ FileNotFoundError: Raised if ied_filepath points to a file which does not exist
39
+ """
40
+
41
+ _filetype: str = "IED"
42
+ _suffix: str = ".ied"
43
+
44
+ def __init__(self, ied_filepath: str | Path | None = None, from_json: bool = False):
45
+ try:
46
+ if from_json:
47
+ return
48
+ if ied_filepath is not None:
49
+ FMFile.__init__(self, ied_filepath)
50
+
51
+ self._read()
52
+
53
+ else:
54
+ # No filepath specified, create new 'blank' IED in memory
55
+ self._ied_struct: list[dict[str, Any]] = []
56
+ self._raw_data: list[str] = []
57
+
58
+ self._get_unit_definitions()
59
+
60
+ except Exception as e:
61
+ self._handle_exception(e, when="read")
62
+
63
+ def _read(self):
64
+ # Read IED data
65
+ with open(self._filepath) as ied_file:
66
+ self._raw_data = [line.rstrip("\n") for line in ied_file.readlines()]
67
+
68
+ # Generate IED structure
69
+ self._update_ied_struct()
70
+
71
+ def _write(self) -> str: # noqa: C901, PLR0912
72
+ """Returns string representation of the current IED data"""
73
+ try:
74
+ block_shift = 0
75
+ existing_units: dict[str, list[str]] = {
76
+ "boundaries": [],
77
+ "structures": [],
78
+ "sections": [],
79
+ "conduits": [],
80
+ "losses": [],
81
+ }
82
+
83
+ for block in self._ied_struct:
84
+ # Check for all supported boundary types
85
+ if block["Type"] in units.SUPPORTED_UNIT_TYPES:
86
+ unit_data = self._raw_data[
87
+ block["start"] + block_shift : block["end"] + 1 + block_shift
88
+ ]
89
+ prev_block_len = len(unit_data)
90
+ if units.SUPPORTED_UNIT_TYPES[block["Type"]]["has_subtype"]:
91
+ unit_name = unit_data[2][:12].strip()
92
+ else:
93
+ unit_name = unit_data[1][:12].strip()
94
+
95
+ # Get unit object
96
+ unit_group = getattr(self, units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"])
97
+ if unit_name in unit_group:
98
+ # block still exists
99
+ new_unit_data = unit_group[unit_name]._write()
100
+ existing_units[units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"]].append(
101
+ unit_name,
102
+ )
103
+ else:
104
+ # Bdy block has been deleted
105
+ new_unit_data = []
106
+
107
+ new_block_len = len(new_unit_data)
108
+ self._raw_data[
109
+ block["start"] + block_shift : block["end"] + 1 + block_shift
110
+ ] = new_unit_data
111
+ # adjust block shift for change in number of lines in bdy block
112
+ block_shift += new_block_len - prev_block_len
113
+
114
+ # Add any new units
115
+ for group_name, _units in existing_units.items():
116
+ for name, unit in getattr(self, group_name).items():
117
+ if name not in _units:
118
+ # Newly added unit
119
+ # Ensure that the 'name' attribute matches name key in boundaries
120
+ self._raw_data.extend(unit._write())
121
+
122
+ # Update ied_struct
123
+ self._update_ied_struct()
124
+
125
+ # Update unit names
126
+ for unit_group, unit_group_name in [
127
+ (self.boundaries, "boundaries"),
128
+ (self.sections, "sections"),
129
+ (self.structures, "structures"),
130
+ (self.conduits, "conduits"),
131
+ (self.losses, "losses"),
132
+ ]:
133
+ for name, unit in unit_group.copy().items():
134
+ if name != unit.name:
135
+ # Check if new name already exists as a label
136
+ if unit.name in unit_group:
137
+ raise Exception(
138
+ f'Error: Cannot update label "{name}" to "{unit.name}" because "{unit.name}" already exists in the Network {unit_group_name} group',
139
+ )
140
+ unit_group[unit.name] = unit
141
+ del unit_group[name]
142
+
143
+ return "\n".join(self._raw_data) + "\n"
144
+
145
+ except Exception as e:
146
+ self._handle_exception(e, when="write")
147
+
148
+ def _get_unit_definitions(self):
149
+ # Get unit definitions
150
+ self.sections = {}
151
+ self.boundaries = {}
152
+ self.structures = {}
153
+ self.conduits = {}
154
+ self.losses = {}
155
+ self._unsupported = {}
156
+ self._all_units = []
157
+ for block in self._ied_struct:
158
+ unit_data = self._raw_data[block["start"] : block["end"] + 1]
159
+ # Check for all supported boundary types, starting just with QTBDY type
160
+ if block["Type"] in units.SUPPORTED_UNIT_TYPES:
161
+ # Check to see whether unit type has associated subtypes so that unit name can be correctly assigned
162
+ if units.SUPPORTED_UNIT_TYPES[block["Type"]]["has_subtype"]:
163
+ # Takes first 12 characters as name
164
+ unit_name = unit_data[2][:12].strip()
165
+ else:
166
+ unit_name = unit_data[1][:12].strip()
167
+
168
+ # Create instance of unit and add to relevant group
169
+ unit_group = getattr(self, units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"])
170
+ if unit_name in unit_group:
171
+ raise Exception(
172
+ f'Duplicate label ({unit_name}) encountered within category: {units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"]}',
173
+ )
174
+ unit_group[unit_name] = eval(f'units.{block["Type"]}({unit_data})')
175
+
176
+ self._all_units.append(unit_group[unit_name])
177
+
178
+ elif block["Type"] in units.UNSUPPORTED_UNIT_TYPES:
179
+ # Check to see whether unit type has associated subtypes so that unit name can be correctly assigned
180
+ if units.UNSUPPORTED_UNIT_TYPES[block["Type"]]["has_subtype"]:
181
+ # Takes first 12 characters as name
182
+ unit_name = unit_data[2][:12].strip()
183
+ subtype = True
184
+ else:
185
+ unit_name = unit_data[1][:12].strip()
186
+ subtype = False
187
+
188
+ self._unsupported[f"{unit_name} ({block['Type']})"] = units.UNSUPPORTED(
189
+ unit_data,
190
+ 12,
191
+ unit_name=unit_name,
192
+ unit_type=block["Type"],
193
+ subtype=subtype,
194
+ )
195
+ self._all_units.append(self._unsupported[f"{unit_name} ({block['Type']})"])
196
+
197
+ print()
198
+
199
+ def _update_ied_struct(self): # noqa: C901, PLR0912, PLR0915
200
+ # Generate IED structure
201
+ ied_struct = []
202
+ in_block = False
203
+ bdy_block = {}
204
+ comment_n = None
205
+ in_comment = False
206
+
207
+ for idx, line in enumerate(self._raw_data):
208
+ # Deal with comment blocks explicitly as they could contain unit keywords
209
+ if in_comment and comment_n is None:
210
+ comment_n = int(line.strip())
211
+ continue
212
+ if in_comment:
213
+ comment_n -= 1
214
+ if comment_n == 0:
215
+ bdy_block["end"] = idx # add ending index
216
+ # append existing bdy block to the ied_struct
217
+ ied_struct.append(bdy_block)
218
+ bdy_block = {} # reset bdy block
219
+ in_comment = False
220
+ in_block = False
221
+ comment_n = None
222
+ continue # move onto next line as still in comment block
223
+
224
+ if line == "COMMENT":
225
+ in_comment = True
226
+ if in_block is True:
227
+ bdy_block["end"] = idx - 1 # add ending index
228
+ # append existing bdy block to the ied_struct
229
+ ied_struct.append(bdy_block)
230
+ bdy_block = {} # reset bdy block
231
+ # start new block for COMMENT
232
+ bdy_block["Type"] = line.split(" ")[0]
233
+ bdy_block["start"] = idx # add starting index
234
+ continue
235
+
236
+ if len(line.split(" ")[0]) > 1:
237
+ if line.split(" ")[0] in units.ALL_UNIT_TYPES:
238
+ if in_block is True:
239
+ bdy_block["end"] = idx - 1 # add ending index
240
+ # append existing bdy block to the ief_struct
241
+ ied_struct.append(bdy_block)
242
+ bdy_block = {} # reset bdy block
243
+ in_block = True
244
+ bdy_block["Type"] = line.split(" ")[0] # start new bdy block
245
+ bdy_block["start"] = idx # add starting index
246
+
247
+ elif " ".join(line.split(" ")[:2]) in units.ALL_UNIT_TYPES:
248
+ if in_block is True:
249
+ bdy_block["end"] = idx - 1 # add ending index
250
+ # append existing bdy block to the ief_struct
251
+ ied_struct.append(bdy_block)
252
+ bdy_block = {} # reset bdy block
253
+ in_block = True
254
+ bdy_block["Type"] = " ".join(line.split(" ")[:2]) # start new bdy block
255
+ bdy_block["start"] = idx # add starting index
256
+ else:
257
+ continue
258
+ elif line in units.ALL_UNIT_TYPES:
259
+ if in_block is True:
260
+ bdy_block["end"] = idx - 1 # add ending index
261
+ # append existing bdy block to the ief_struct
262
+ ied_struct.append(bdy_block)
263
+ bdy_block = {} # reset bdy block
264
+ in_block = True
265
+ bdy_block["Type"] = line # start new bdy block
266
+ bdy_block["start"] = idx # add starting index
267
+ else:
268
+ continue
269
+
270
+ if len(bdy_block) != 0:
271
+ # Only adds end block if there is a bdy block present (i.e. an empty IED stays empty)
272
+ # add ending index for final block
273
+ bdy_block["end"] = len(self._raw_data) - 1
274
+ ied_struct.append(bdy_block) # add final block
275
+
276
+ self._ied_struct = ied_struct
277
+
278
+ def diff(self, other: IED, force_print: bool = False) -> None:
279
+ """Compares the IED class against another IED class to check whether they are
280
+ equivalent, or if not, what the differences are. Two instances of an IED class are
281
+ deemed equivalent if all of their attributes are equal except for the filepath and
282
+ raw data. For example, two IED files from different filepaths that had the same
283
+ data except maybe some differences in decimal places and some default parameters
284
+ ommitted, would be classed as equaivalent as they would produce the same IED instance
285
+ and write the exact same data.
286
+
287
+ The result is printed to the console. If you need to access the returned data, use
288
+ the method ``IED._get_diff()``
289
+
290
+ Args:
291
+ other (floodmodeller_api.IED): Other instance of an IED class
292
+ force_print (bool): Forces the API to print every difference found, rather than
293
+ just the first 25 differences. Defaults to False.
294
+ """
295
+ self._diff(other, force_print=force_print)
296
+
297
+ def update(self) -> None:
298
+ """Updates the existing IED based on any altered attributes"""
299
+
300
+ self._update()
301
+
302
+ def save(self, filepath: str | Path) -> None:
303
+ """Saves the IED to the given location, if pointing to an existing file it will be overwritten.
304
+ Once saved, the IED() class will continue working from the saved location, therefore any further calls to IED.update() will update in the latest saved location
305
+ rather than the original source IED used to construct the class"""
306
+
307
+ self._save(filepath)