floodmodeller-api 0.4.2.post1__py3-none-any.whl → 0.4.4__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 +169 -176
  3. floodmodeller_api/backup.py +273 -273
  4. floodmodeller_api/dat.py +889 -831
  5. floodmodeller_api/diff.py +136 -119
  6. floodmodeller_api/ied.py +302 -306
  7. floodmodeller_api/ief.py +553 -637
  8. floodmodeller_api/ief_flags.py +253 -253
  9. floodmodeller_api/inp.py +260 -266
  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 +364 -312
  21. floodmodeller_api/logs/lf_helpers.py +354 -352
  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 +16 -8
  26. floodmodeller_api/test/test_backup.py +117 -117
  27. floodmodeller_api/test/test_conveyance.py +107 -0
  28. floodmodeller_api/test/test_dat.py +222 -92
  29. floodmodeller_api/test/test_data/All Units 4_6.DAT +1081 -1081
  30. floodmodeller_api/test/test_data/All Units 4_6.feb +1081 -1081
  31. floodmodeller_api/test/test_data/BRIDGE.DAT +926 -926
  32. floodmodeller_api/test/test_data/Culvert_Inlet_Outlet.dat +36 -36
  33. floodmodeller_api/test/test_data/Culvert_Inlet_Outlet.feb +36 -36
  34. floodmodeller_api/test/test_data/DamBreakADI.xml +52 -52
  35. floodmodeller_api/test/test_data/DamBreakFAST.xml +58 -58
  36. floodmodeller_api/test/test_data/DamBreakFAST_dy.xml +53 -53
  37. floodmodeller_api/test/test_data/DamBreakTVD.xml +55 -55
  38. floodmodeller_api/test/test_data/DefenceBreach.xml +53 -53
  39. floodmodeller_api/test/test_data/DefenceBreachFAST.xml +60 -60
  40. floodmodeller_api/test/test_data/DefenceBreachFAST_dy.xml +55 -55
  41. floodmodeller_api/test/test_data/Domain1+2_QH.xml +76 -76
  42. floodmodeller_api/test/test_data/Domain1_H.xml +41 -41
  43. floodmodeller_api/test/test_data/Domain1_Q.xml +41 -41
  44. floodmodeller_api/test/test_data/Domain1_Q_FAST.xml +48 -48
  45. floodmodeller_api/test/test_data/Domain1_Q_FAST_dy.xml +48 -48
  46. floodmodeller_api/test/test_data/Domain1_Q_xml_expected.json +263 -0
  47. floodmodeller_api/test/test_data/Domain1_W.xml +41 -41
  48. floodmodeller_api/test/test_data/EX1.DAT +321 -321
  49. floodmodeller_api/test/test_data/EX1.ext +107 -107
  50. floodmodeller_api/test/test_data/EX1.feb +320 -320
  51. floodmodeller_api/test/test_data/EX1.gxy +107 -107
  52. floodmodeller_api/test/test_data/EX17.DAT +421 -422
  53. floodmodeller_api/test/test_data/EX17.ext +213 -213
  54. floodmodeller_api/test/test_data/EX17.feb +422 -422
  55. floodmodeller_api/test/test_data/EX18.DAT +375 -375
  56. floodmodeller_api/test/test_data/EX18_DAT_expected.json +3876 -0
  57. floodmodeller_api/test/test_data/EX2.DAT +302 -302
  58. floodmodeller_api/test/test_data/EX3.DAT +926 -926
  59. floodmodeller_api/test/test_data/EX3_DAT_expected.json +16235 -0
  60. floodmodeller_api/test/test_data/EX3_IEF_expected.json +61 -0
  61. floodmodeller_api/test/test_data/EX6.DAT +2084 -2084
  62. floodmodeller_api/test/test_data/EX6.ext +532 -532
  63. floodmodeller_api/test/test_data/EX6.feb +2084 -2084
  64. floodmodeller_api/test/test_data/EX6_DAT_expected.json +31647 -0
  65. floodmodeller_api/test/test_data/Event Data Example.DAT +336 -336
  66. floodmodeller_api/test/test_data/Event Data Example.ext +107 -107
  67. floodmodeller_api/test/test_data/Event Data Example.feb +336 -336
  68. floodmodeller_api/test/test_data/Linked1D2D.xml +52 -52
  69. floodmodeller_api/test/test_data/Linked1D2DFAST.xml +53 -53
  70. floodmodeller_api/test/test_data/Linked1D2DFAST_dy.xml +48 -48
  71. floodmodeller_api/test/test_data/Linked1D2D_xml_expected.json +313 -0
  72. floodmodeller_api/test/test_data/blockage.dat +50 -50
  73. floodmodeller_api/test/test_data/blockage.ext +45 -45
  74. floodmodeller_api/test/test_data/blockage.feb +9 -9
  75. floodmodeller_api/test/test_data/blockage.gxy +71 -71
  76. floodmodeller_api/test/test_data/conveyance_test.dat +165 -0
  77. floodmodeller_api/test/test_data/conveyance_test.feb +116 -0
  78. floodmodeller_api/test/test_data/conveyance_test.gxy +85 -0
  79. floodmodeller_api/test/test_data/defaultUnits.dat +127 -127
  80. floodmodeller_api/test/test_data/defaultUnits.ext +45 -45
  81. floodmodeller_api/test/test_data/defaultUnits.feb +9 -9
  82. floodmodeller_api/test/test_data/defaultUnits.fmpx +58 -58
  83. floodmodeller_api/test/test_data/defaultUnits.gxy +85 -85
  84. floodmodeller_api/test/test_data/ex3.ief +20 -20
  85. floodmodeller_api/test/test_data/ex3.lf1 +2800 -2800
  86. floodmodeller_api/test/test_data/ex4.DAT +1374 -1374
  87. floodmodeller_api/test/test_data/ex4_changed.DAT +1374 -1374
  88. floodmodeller_api/test/test_data/example1.inp +329 -329
  89. floodmodeller_api/test/test_data/example2.inp +158 -158
  90. floodmodeller_api/test/test_data/example3.inp +297 -297
  91. floodmodeller_api/test/test_data/example4.inp +388 -388
  92. floodmodeller_api/test/test_data/example5.inp +147 -147
  93. floodmodeller_api/test/test_data/example6.inp +154 -154
  94. floodmodeller_api/test/test_data/expected_conveyance.csv +60 -0
  95. floodmodeller_api/test/test_data/jump.dat +176 -176
  96. floodmodeller_api/test/test_data/network.dat +1374 -1374
  97. floodmodeller_api/test/test_data/network.ext +45 -45
  98. floodmodeller_api/test/test_data/network.exy +1 -1
  99. floodmodeller_api/test/test_data/network.feb +45 -45
  100. floodmodeller_api/test/test_data/network.ied +45 -45
  101. floodmodeller_api/test/test_data/network.ief +20 -20
  102. floodmodeller_api/test/test_data/network.inp +147 -147
  103. floodmodeller_api/test/test_data/network.pxy +57 -57
  104. floodmodeller_api/test/test_data/network.zzd +122 -122
  105. floodmodeller_api/test/test_data/network_dat_expected.json +21837 -0
  106. floodmodeller_api/test/test_data/network_from_tabularCSV.csv +87 -87
  107. floodmodeller_api/test/test_data/network_ied_expected.json +287 -0
  108. floodmodeller_api/test/test_data/rnweir.dat +9 -9
  109. floodmodeller_api/test/test_data/rnweir.ext +45 -45
  110. floodmodeller_api/test/test_data/rnweir.feb +9 -9
  111. floodmodeller_api/test/test_data/rnweir.gxy +45 -45
  112. floodmodeller_api/test/test_data/rnweir_default.dat +74 -74
  113. floodmodeller_api/test/test_data/rnweir_default.ext +45 -45
  114. floodmodeller_api/test/test_data/rnweir_default.feb +9 -9
  115. floodmodeller_api/test/test_data/rnweir_default.fmpx +58 -58
  116. floodmodeller_api/test/test_data/rnweir_default.gxy +53 -53
  117. floodmodeller_api/test/test_data/unit checks.dat +16 -16
  118. floodmodeller_api/test/test_ied.py +29 -29
  119. floodmodeller_api/test/test_ief.py +136 -24
  120. floodmodeller_api/test/test_inp.py +47 -48
  121. floodmodeller_api/test/test_json.py +114 -0
  122. floodmodeller_api/test/test_logs_lf.py +102 -51
  123. floodmodeller_api/test/test_tool.py +165 -152
  124. floodmodeller_api/test/test_toolbox_structure_log.py +234 -239
  125. floodmodeller_api/test/test_xml2d.py +151 -156
  126. floodmodeller_api/test/test_zzn.py +36 -34
  127. floodmodeller_api/to_from_json.py +230 -0
  128. floodmodeller_api/tool.py +332 -329
  129. floodmodeller_api/toolbox/__init__.py +5 -5
  130. floodmodeller_api/toolbox/example_tool.py +45 -45
  131. floodmodeller_api/toolbox/model_build/__init__.py +2 -2
  132. floodmodeller_api/toolbox/model_build/add_siltation_definition.py +100 -98
  133. floodmodeller_api/toolbox/model_build/structure_log/__init__.py +1 -1
  134. floodmodeller_api/toolbox/model_build/structure_log/structure_log.py +287 -289
  135. floodmodeller_api/toolbox/model_build/structure_log_definition.py +76 -76
  136. floodmodeller_api/units/__init__.py +10 -10
  137. floodmodeller_api/units/_base.py +214 -212
  138. floodmodeller_api/units/boundaries.py +467 -467
  139. floodmodeller_api/units/comment.py +52 -55
  140. floodmodeller_api/units/conduits.py +382 -402
  141. floodmodeller_api/units/conveyance.py +301 -0
  142. floodmodeller_api/units/helpers.py +123 -131
  143. floodmodeller_api/units/iic.py +107 -101
  144. floodmodeller_api/units/losses.py +305 -306
  145. floodmodeller_api/units/sections.py +465 -446
  146. floodmodeller_api/units/structures.py +1690 -1683
  147. floodmodeller_api/units/units.py +93 -104
  148. floodmodeller_api/units/unsupported.py +44 -44
  149. floodmodeller_api/units/variables.py +87 -89
  150. floodmodeller_api/urban1d/__init__.py +11 -11
  151. floodmodeller_api/urban1d/_base.py +188 -179
  152. floodmodeller_api/urban1d/conduits.py +93 -85
  153. floodmodeller_api/urban1d/general_parameters.py +58 -58
  154. floodmodeller_api/urban1d/junctions.py +81 -79
  155. floodmodeller_api/urban1d/losses.py +81 -74
  156. floodmodeller_api/urban1d/outfalls.py +114 -110
  157. floodmodeller_api/urban1d/raingauges.py +111 -111
  158. floodmodeller_api/urban1d/subsections.py +92 -98
  159. floodmodeller_api/urban1d/xsections.py +147 -144
  160. floodmodeller_api/util.py +119 -21
  161. floodmodeller_api/validation/parameters.py +660 -660
  162. floodmodeller_api/validation/urban_parameters.py +388 -404
  163. floodmodeller_api/validation/validation.py +110 -108
  164. floodmodeller_api/version.py +1 -1
  165. floodmodeller_api/xml2d.py +632 -673
  166. floodmodeller_api/xml2d_template.py +37 -37
  167. floodmodeller_api/zzn.py +414 -363
  168. {floodmodeller_api-0.4.2.post1.dist-info → floodmodeller_api-0.4.4.dist-info}/LICENSE.txt +13 -13
  169. {floodmodeller_api-0.4.2.post1.dist-info → floodmodeller_api-0.4.4.dist-info}/METADATA +85 -82
  170. floodmodeller_api-0.4.4.dist-info/RECORD +185 -0
  171. {floodmodeller_api-0.4.2.post1.dist-info → floodmodeller_api-0.4.4.dist-info}/WHEEL +1 -1
  172. floodmodeller_api/libifcoremd.dll +0 -0
  173. floodmodeller_api/test/test_data/EX3.bmp +0 -0
  174. floodmodeller_api/test/test_data/test_output.csv +0 -87
  175. floodmodeller_api/zzn_read.dll +0 -0
  176. floodmodeller_api-0.4.2.post1.dist-info/RECORD +0 -164
  177. {floodmodeller_api-0.4.2.post1.dist-info → floodmodeller_api-0.4.4.dist-info}/entry_points.txt +0 -0
  178. {floodmodeller_api-0.4.2.post1.dist-info → floodmodeller_api-0.4.4.dist-info}/top_level.txt +0 -0
floodmodeller_api/ied.py CHANGED
@@ -1,306 +1,302 @@
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 Any, Dict, List, 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
- if ied_filepath is not None:
43
- FMFile.__init__(self, ied_filepath)
44
-
45
- self._read()
46
-
47
- else:
48
- # No filepath specified, create new 'blank' IED in memory
49
- self._ied_struct: List[Dict[str, Any]] = []
50
- self._raw_data: List[str] = []
51
-
52
- self._get_unit_definitions()
53
-
54
- except Exception as e:
55
- self._handle_exception(e, when="read")
56
-
57
- def _read(self):
58
- # Read IED data
59
- with open(self._filepath, "r") as ied_file:
60
- self._raw_data = [line.rstrip("\n") for line in ied_file.readlines()]
61
-
62
- # Generate IED structure
63
- self._update_ied_struct()
64
-
65
- def _write(self) -> str: # noqa: C901
66
- """Returns string representation of the current IED data"""
67
- try:
68
- block_shift = 0
69
- existing_units: Dict[str, List[str]] = {
70
- "boundaries": [],
71
- "structures": [],
72
- "sections": [],
73
- "conduits": [],
74
- "losses": [],
75
- }
76
-
77
- for block in self._ied_struct:
78
- # Check for all supported boundary types
79
- if block["Type"] in units.SUPPORTED_UNIT_TYPES:
80
- unit_data = self._raw_data[
81
- block["start"] + block_shift : block["end"] + 1 + block_shift
82
- ]
83
- prev_block_len = len(unit_data)
84
- if units.SUPPORTED_UNIT_TYPES[block["Type"]]["has_subtype"]:
85
- unit_name = unit_data[2][:12].strip()
86
- else:
87
- unit_name = unit_data[1][:12].strip()
88
-
89
- # Get unit object
90
- unit_group = getattr(self, units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"])
91
- if unit_name in unit_group:
92
- # block still exists
93
- new_unit_data = unit_group[unit_name]._write()
94
- existing_units[units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"]].append(
95
- unit_name
96
- )
97
- else:
98
- # Bdy block has been deleted
99
- new_unit_data = []
100
-
101
- new_block_len = len(new_unit_data)
102
- self._raw_data[
103
- block["start"] + block_shift : block["end"] + 1 + block_shift
104
- ] = new_unit_data
105
- # adjust block shift for change in number of lines in bdy block
106
- block_shift += new_block_len - prev_block_len
107
-
108
- # Add any new units
109
- for group_name, _units in existing_units.items():
110
- for name, unit in getattr(self, group_name).items():
111
- if name not in _units:
112
- # Newly added unit
113
- # Ensure that the 'name' attribute matches name key in boundaries
114
- self._raw_data.extend(unit._write())
115
-
116
- # Update ied_struct
117
- self._update_ied_struct()
118
-
119
- # Update unit names
120
- for unit_group, unit_group_name in [
121
- (self.boundaries, "boundaries"),
122
- (self.sections, "sections"),
123
- (self.structures, "structures"),
124
- (self.conduits, "conduits"),
125
- (self.losses, "losses"),
126
- ]:
127
- for name, unit in unit_group.copy().items():
128
- if name != unit.name:
129
- # Check if new name already exists as a label
130
- if unit.name in unit_group:
131
- raise Exception(
132
- f'Error: Cannot update label "{name}" to "{unit.name}" because "{unit.name}" already exists in the Network {unit_group_name} group'
133
- )
134
- unit_group[unit.name] = unit
135
- del unit_group[name]
136
-
137
- ied_string = ""
138
- for line in self._raw_data:
139
- ied_string += line + "\n"
140
-
141
- return ied_string
142
-
143
- except Exception as e:
144
- self._handle_exception(e, when="write")
145
-
146
- def _get_unit_definitions(self):
147
- # Get unit definitions
148
- self.sections = {}
149
- self.boundaries = {}
150
- self.structures = {}
151
- self.conduits = {}
152
- self.losses = {}
153
- self._unsupported = {}
154
- self._all_units = []
155
- for block in self._ied_struct:
156
- unit_data = self._raw_data[block["start"] : block["end"] + 1]
157
- # Check for all supported boundary types, starting just with QTBDY type
158
- if block["Type"] in units.SUPPORTED_UNIT_TYPES:
159
- # Check to see whether unit type has associated subtypes so that unit name can be correctly assigned
160
- if units.SUPPORTED_UNIT_TYPES[block["Type"]]["has_subtype"]:
161
- # Takes first 12 characters as name
162
- unit_name = unit_data[2][:12].strip()
163
- else:
164
- unit_name = unit_data[1][:12].strip()
165
-
166
- # Create instance of unit and add to relevant group
167
- unit_group = getattr(self, units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"])
168
- if unit_name in unit_group:
169
- raise Exception(
170
- f'Duplicate label ({unit_name}) encountered within category: {units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"]}'
171
- )
172
- unit_group[unit_name] = eval(f'units.{block["Type"]}({unit_data})')
173
-
174
- self._all_units.append(unit_group[unit_name])
175
-
176
- elif block["Type"] in units.UNSUPPORTED_UNIT_TYPES:
177
- # Check to see whether unit type has associated subtypes so that unit name can be correctly assigned
178
- if units.UNSUPPORTED_UNIT_TYPES[block["Type"]]["has_subtype"]:
179
- # Takes first 12 characters as name
180
- unit_name = unit_data[2][:12].strip()
181
- subtype = True
182
- else:
183
- unit_name = unit_data[1][:12].strip()
184
- subtype = False
185
-
186
- # _label_len = _to_int(params[5], 12) # label length
187
- self._unsupported[f"{unit_name} ({block['Type']})"] = units.UNSUPPORTED(
188
- unit_data,
189
- 12,
190
- unit_name=unit_name,
191
- unit_type=block["Type"],
192
- subtype=subtype,
193
- )
194
- self._all_units.append(self._unsupported[f"{unit_name} ({block['Type']})"])
195
-
196
- print()
197
-
198
- def _update_ied_struct(self): # noqa: C901
199
- # Generate IED structure
200
- ied_struct = []
201
- in_block = False
202
- bdy_block = {}
203
- comment_n = None
204
- in_comment = False
205
-
206
- for idx, line in enumerate(self._raw_data):
207
- # Deal with comment blocks explicitly as they could contain unit keywords
208
- if in_comment and comment_n is None:
209
- comment_n = int(line.strip())
210
- continue
211
- if in_comment:
212
- comment_n -= 1
213
- if comment_n == 0:
214
- bdy_block["end"] = idx # add ending index
215
- # append existing bdy block to the ied_struct
216
- ied_struct.append(bdy_block)
217
- bdy_block = {} # reset bdy block
218
- in_comment = False
219
- in_block = False
220
- comment_n = None
221
- continue # move onto next line as still in comment block
222
-
223
- if line == "COMMENT":
224
- in_comment = True
225
- if in_block is True:
226
- bdy_block["end"] = idx - 1 # add ending index
227
- # append existing bdy block to the ied_struct
228
- ied_struct.append(bdy_block)
229
- bdy_block = {} # reset bdy block
230
- # start new block for COMMENT
231
- bdy_block["Type"] = line.split(" ")[0]
232
- bdy_block["start"] = idx # add starting index
233
- continue
234
-
235
- if len(line.split(" ")[0]) > 1:
236
- if line.split(" ")[0] in units.ALL_UNIT_TYPES:
237
- if in_block is True:
238
- bdy_block["end"] = idx - 1 # add ending index
239
- # append existing bdy block to the ief_struct
240
- ied_struct.append(bdy_block)
241
- bdy_block = {} # reset bdy block
242
- in_block = True
243
- bdy_block["Type"] = line.split(" ")[0] # start new bdy block
244
- bdy_block["start"] = idx # add starting index
245
-
246
- elif " ".join(line.split(" ")[:2]) in units.ALL_UNIT_TYPES:
247
- if in_block is True:
248
- bdy_block["end"] = idx - 1 # add ending index
249
- # append existing bdy block to the ief_struct
250
- ied_struct.append(bdy_block)
251
- bdy_block = {} # reset bdy block
252
- in_block = True
253
- bdy_block["Type"] = " ".join(line.split(" ")[:2]) # start new bdy block
254
- bdy_block["start"] = idx # add starting index
255
- else:
256
- continue
257
- elif line in units.ALL_UNIT_TYPES:
258
- if in_block is True:
259
- bdy_block["end"] = idx - 1 # add ending index
260
- # append existing bdy block to the ief_struct
261
- ied_struct.append(bdy_block)
262
- bdy_block = {} # reset bdy block
263
- in_block = True
264
- bdy_block["Type"] = line # start new bdy block
265
- bdy_block["start"] = idx # add starting index
266
- else:
267
- continue
268
-
269
- if len(bdy_block) != 0:
270
- # Only adds end block if there is a bdy block present (i.e. an empty IED stays empty)
271
- # add ending index for final block
272
- bdy_block["end"] = len(self._raw_data) - 1
273
- ied_struct.append(bdy_block) # add final block
274
-
275
- self._ied_struct = ied_struct
276
-
277
- def diff(self, other: "IED", force_print: bool = False) -> None:
278
- """Compares the IED class against another IED class to check whether they are
279
- equivalent, or if not, what the differences are. Two instances of an IED class are
280
- deemed equivalent if all of their attributes are equal except for the filepath and
281
- raw data. For example, two IED files from different filepaths that had the same
282
- data except maybe some differences in decimal places and some default parameters
283
- ommitted, would be classed as equaivalent as they would produce the same IED instance
284
- and write the exact same data.
285
-
286
- The result is printed to the console. If you need to access the returned data, use
287
- the method ``IED._get_diff()``
288
-
289
- Args:
290
- other (floodmodeller_api.IED): Other instance of an IED class
291
- force_print (bool): Forces the API to print every difference found, rather than
292
- just the first 25 differences. Defaults to False.
293
- """
294
- self._diff(other, force_print=force_print)
295
-
296
- def update(self) -> None:
297
- """Updates the existing IED based on any altered attributes"""
298
-
299
- self._update()
300
-
301
- def save(self, filepath: Union[str, Path]) -> None:
302
- """Saves the IED to the given location, if pointing to an existing file it will be overwritten.
303
- 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
304
- rather than the original source IED used to construct the class"""
305
-
306
- 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
+ from .util import handle_exception
24
+
25
+ if TYPE_CHECKING:
26
+ from pathlib import Path
27
+
28
+
29
+ class IED(FMFile):
30
+ """Reads and write Flood Modeller event data format '.ied'
31
+ Args:
32
+ ied_filepath (str, optional): Full filepath to ied file. If not specified, a new IED class will be created.
33
+
34
+ Output:
35
+ Initiates 'IED' class object
36
+
37
+ Raises:
38
+ TypeError: Raised if ied_filepath does not point to a .ied file
39
+ FileNotFoundError: Raised if ied_filepath points to a file which does not exist
40
+ """
41
+
42
+ _filetype: str = "IED"
43
+ _suffix: str = ".ied"
44
+
45
+ @handle_exception(when="read")
46
+ def __init__(self, ied_filepath: str | Path | None = None, from_json: bool = False):
47
+ if from_json:
48
+ return
49
+ if ied_filepath is not None:
50
+ FMFile.__init__(self, ied_filepath)
51
+
52
+ self._read()
53
+
54
+ else:
55
+ # No filepath specified, create new 'blank' IED in memory
56
+ self._ied_struct: list[dict[str, Any]] = []
57
+ self._raw_data: list[str] = []
58
+
59
+ self._get_unit_definitions()
60
+
61
+ def _read(self):
62
+ # Read IED data
63
+ with open(self._filepath) as ied_file:
64
+ self._raw_data = [line.rstrip("\n") for line in ied_file.readlines()]
65
+
66
+ # Generate IED structure
67
+ self._update_ied_struct()
68
+
69
+ @handle_exception(when="write")
70
+ def _write(self) -> str: # noqa: C901, PLR0912
71
+ """Returns string representation of the current IED data"""
72
+ block_shift = 0
73
+ existing_units: dict[str, list[str]] = {
74
+ "boundaries": [],
75
+ "structures": [],
76
+ "sections": [],
77
+ "conduits": [],
78
+ "losses": [],
79
+ }
80
+
81
+ for block in self._ied_struct:
82
+ # Check for all supported boundary types
83
+ if block["Type"] in units.SUPPORTED_UNIT_TYPES:
84
+ unit_data = self._raw_data[
85
+ block["start"] + block_shift : block["end"] + 1 + block_shift
86
+ ]
87
+ prev_block_len = len(unit_data)
88
+ if units.SUPPORTED_UNIT_TYPES[block["Type"]]["has_subtype"]:
89
+ unit_name = unit_data[2][:12].strip()
90
+ else:
91
+ unit_name = unit_data[1][:12].strip()
92
+
93
+ # Get unit object
94
+ unit_group = getattr(self, units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"])
95
+ if unit_name in unit_group:
96
+ # block still exists
97
+ new_unit_data = unit_group[unit_name]._write()
98
+ existing_units[units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"]].append(
99
+ unit_name,
100
+ )
101
+ else:
102
+ # Bdy block has been deleted
103
+ new_unit_data = []
104
+
105
+ new_block_len = len(new_unit_data)
106
+ self._raw_data[block["start"] + block_shift : block["end"] + 1 + block_shift] = (
107
+ new_unit_data
108
+ )
109
+ # adjust block shift for change in number of lines in bdy block
110
+ block_shift += new_block_len - prev_block_len
111
+
112
+ # Add any new units
113
+ for group_name, _units in existing_units.items():
114
+ for name, unit in getattr(self, group_name).items():
115
+ if name not in _units:
116
+ # Newly added unit
117
+ # Ensure that the 'name' attribute matches name key in boundaries
118
+ self._raw_data.extend(unit._write())
119
+
120
+ # Update ied_struct
121
+ self._update_ied_struct()
122
+
123
+ # Update unit names
124
+ for unit_group, unit_group_name in [
125
+ (self.boundaries, "boundaries"),
126
+ (self.sections, "sections"),
127
+ (self.structures, "structures"),
128
+ (self.conduits, "conduits"),
129
+ (self.losses, "losses"),
130
+ ]:
131
+ for name, unit in unit_group.copy().items():
132
+ if name != unit.name:
133
+ # Check if new name already exists as a label
134
+ if unit.name in unit_group:
135
+ raise Exception(
136
+ f'Error: Cannot update label "{name}" to "{unit.name}" because "{unit.name}" already exists in the Network {unit_group_name} group',
137
+ )
138
+ unit_group[unit.name] = unit
139
+ del unit_group[name]
140
+
141
+ return "\n".join(self._raw_data) + "\n"
142
+
143
+ def _get_unit_definitions(self):
144
+ # Get unit definitions
145
+ self.sections = {}
146
+ self.boundaries = {}
147
+ self.structures = {}
148
+ self.conduits = {}
149
+ self.losses = {}
150
+ self._unsupported = {}
151
+ self._all_units = []
152
+ for block in self._ied_struct:
153
+ unit_data = self._raw_data[block["start"] : block["end"] + 1]
154
+ # Check for all supported boundary types, starting just with QTBDY type
155
+ if block["Type"] in units.SUPPORTED_UNIT_TYPES:
156
+ # Check to see whether unit type has associated subtypes so that unit name can be correctly assigned
157
+ if units.SUPPORTED_UNIT_TYPES[block["Type"]]["has_subtype"]:
158
+ # Takes first 12 characters as name
159
+ unit_name = unit_data[2][:12].strip()
160
+ else:
161
+ unit_name = unit_data[1][:12].strip()
162
+
163
+ # Create instance of unit and add to relevant group
164
+ unit_group = getattr(self, units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"])
165
+ if unit_name in unit_group:
166
+ raise Exception(
167
+ f'Duplicate label ({unit_name}) encountered within category: {units.SUPPORTED_UNIT_TYPES[block["Type"]]["group"]}',
168
+ )
169
+ unit_group[unit_name] = eval(f'units.{block["Type"]}({unit_data})')
170
+
171
+ self._all_units.append(unit_group[unit_name])
172
+
173
+ elif block["Type"] in units.UNSUPPORTED_UNIT_TYPES:
174
+ # Check to see whether unit type has associated subtypes so that unit name can be correctly assigned
175
+ if units.UNSUPPORTED_UNIT_TYPES[block["Type"]]["has_subtype"]:
176
+ # Takes first 12 characters as name
177
+ unit_name = unit_data[2][:12].strip()
178
+ subtype = True
179
+ else:
180
+ unit_name = unit_data[1][:12].strip()
181
+ subtype = False
182
+
183
+ self._unsupported[f"{unit_name} ({block['Type']})"] = units.UNSUPPORTED(
184
+ unit_data,
185
+ 12,
186
+ unit_name=unit_name,
187
+ unit_type=block["Type"],
188
+ subtype=subtype,
189
+ )
190
+ self._all_units.append(self._unsupported[f"{unit_name} ({block['Type']})"])
191
+
192
+ print()
193
+
194
+ def _update_ied_struct(self): # noqa: C901, PLR0912, PLR0915
195
+ # Generate IED structure
196
+ ied_struct = []
197
+ in_block = False
198
+ bdy_block = {}
199
+ comment_n = None
200
+ in_comment = False
201
+
202
+ for idx, line in enumerate(self._raw_data):
203
+ # Deal with comment blocks explicitly as they could contain unit keywords
204
+ if in_comment and comment_n is None:
205
+ comment_n = int(line.strip())
206
+ continue
207
+ if in_comment:
208
+ comment_n -= 1
209
+ if comment_n == 0:
210
+ bdy_block["end"] = idx # add ending index
211
+ # append existing bdy block to the ied_struct
212
+ ied_struct.append(bdy_block)
213
+ bdy_block = {} # reset bdy block
214
+ in_comment = False
215
+ in_block = False
216
+ comment_n = None
217
+ continue # move onto next line as still in comment block
218
+
219
+ if line == "COMMENT":
220
+ in_comment = True
221
+ if in_block is True:
222
+ bdy_block["end"] = idx - 1 # add ending index
223
+ # append existing bdy block to the ied_struct
224
+ ied_struct.append(bdy_block)
225
+ bdy_block = {} # reset bdy block
226
+ # start new block for COMMENT
227
+ bdy_block["Type"] = line.split(" ")[0]
228
+ bdy_block["start"] = idx # add starting index
229
+ continue
230
+
231
+ if len(line.split(" ")[0]) > 1:
232
+ if line.split(" ")[0] in units.ALL_UNIT_TYPES:
233
+ if in_block is True:
234
+ bdy_block["end"] = idx - 1 # add ending index
235
+ # append existing bdy block to the ief_struct
236
+ ied_struct.append(bdy_block)
237
+ bdy_block = {} # reset bdy block
238
+ in_block = True
239
+ bdy_block["Type"] = line.split(" ")[0] # start new bdy block
240
+ bdy_block["start"] = idx # add starting index
241
+
242
+ elif " ".join(line.split(" ")[:2]) in units.ALL_UNIT_TYPES:
243
+ if in_block is True:
244
+ bdy_block["end"] = idx - 1 # add ending index
245
+ # append existing bdy block to the ief_struct
246
+ ied_struct.append(bdy_block)
247
+ bdy_block = {} # reset bdy block
248
+ in_block = True
249
+ bdy_block["Type"] = " ".join(line.split(" ")[:2]) # start new bdy block
250
+ bdy_block["start"] = idx # add starting index
251
+ else:
252
+ continue
253
+ elif line in units.ALL_UNIT_TYPES:
254
+ if in_block is True:
255
+ bdy_block["end"] = idx - 1 # add ending index
256
+ # append existing bdy block to the ief_struct
257
+ ied_struct.append(bdy_block)
258
+ bdy_block = {} # reset bdy block
259
+ in_block = True
260
+ bdy_block["Type"] = line # start new bdy block
261
+ bdy_block["start"] = idx # add starting index
262
+ else:
263
+ continue
264
+
265
+ if len(bdy_block) != 0:
266
+ # Only adds end block if there is a bdy block present (i.e. an empty IED stays empty)
267
+ # add ending index for final block
268
+ bdy_block["end"] = len(self._raw_data) - 1
269
+ ied_struct.append(bdy_block) # add final block
270
+
271
+ self._ied_struct = ied_struct
272
+
273
+ def diff(self, other: IED, force_print: bool = False) -> None:
274
+ """Compares the IED class against another IED class to check whether they are
275
+ equivalent, or if not, what the differences are. Two instances of an IED class are
276
+ deemed equivalent if all of their attributes are equal except for the filepath and
277
+ raw data. For example, two IED files from different filepaths that had the same
278
+ data except maybe some differences in decimal places and some default parameters
279
+ ommitted, would be classed as equaivalent as they would produce the same IED instance
280
+ and write the exact same data.
281
+
282
+ The result is printed to the console. If you need to access the returned data, use
283
+ the method ``IED._get_diff()``
284
+
285
+ Args:
286
+ other (floodmodeller_api.IED): Other instance of an IED class
287
+ force_print (bool): Forces the API to print every difference found, rather than
288
+ just the first 25 differences. Defaults to False.
289
+ """
290
+ self._diff(other, force_print=force_print)
291
+
292
+ def update(self) -> None:
293
+ """Updates the existing IED based on any altered attributes"""
294
+
295
+ self._update()
296
+
297
+ def save(self, filepath: str | Path) -> None:
298
+ """Saves the IED to the given location, if pointing to an existing file it will be overwritten.
299
+ 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
300
+ rather than the original source IED used to construct the class"""
301
+
302
+ self._save(filepath)