floodmodeller-api 0.4.2.post1__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 (172) 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 -831
  5. floodmodeller_api/diff.py +136 -119
  6. floodmodeller_api/ied.py +307 -306
  7. floodmodeller_api/ief.py +647 -637
  8. floodmodeller_api/ief_flags.py +253 -253
  9. floodmodeller_api/inp.py +266 -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 +320 -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 +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 -152
  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 -329
  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 -98
  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 -76
  131. floodmodeller_api/units/__init__.py +10 -10
  132. floodmodeller_api/units/_base.py +214 -212
  133. floodmodeller_api/units/boundaries.py +467 -467
  134. floodmodeller_api/units/comment.py +52 -55
  135. floodmodeller_api/units/conduits.py +382 -402
  136. floodmodeller_api/units/helpers.py +123 -131
  137. floodmodeller_api/units/iic.py +107 -101
  138. floodmodeller_api/units/losses.py +305 -306
  139. floodmodeller_api/units/sections.py +444 -446
  140. floodmodeller_api/units/structures.py +1690 -1683
  141. floodmodeller_api/units/units.py +93 -104
  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 -179
  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 -110
  151. floodmodeller_api/urban1d/raingauges.py +111 -111
  152. floodmodeller_api/urban1d/subsections.py +92 -98
  153. floodmodeller_api/urban1d/xsections.py +147 -144
  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 -108
  158. floodmodeller_api/version.py +1 -1
  159. floodmodeller_api/xml2d.py +688 -673
  160. floodmodeller_api/xml2d_template.py +37 -37
  161. floodmodeller_api/zzn.py +387 -363
  162. {floodmodeller_api-0.4.2.post1.dist-info → floodmodeller_api-0.4.3.dist-info}/LICENSE.txt +13 -13
  163. {floodmodeller_api-0.4.2.post1.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.post1.dist-info → floodmodeller_api-0.4.3.dist-info}/WHEEL +1 -1
  166. floodmodeller_api/libifcoremd.dll +0 -0
  167. floodmodeller_api/test/test_data/EX3.bmp +0 -0
  168. floodmodeller_api/test/test_data/test_output.csv +0 -87
  169. floodmodeller_api/zzn_read.dll +0 -0
  170. floodmodeller_api-0.4.2.post1.dist-info/RECORD +0 -164
  171. {floodmodeller_api-0.4.2.post1.dist-info → floodmodeller_api-0.4.3.dist-info}/entry_points.txt +0 -0
  172. {floodmodeller_api-0.4.2.post1.dist-info → floodmodeller_api-0.4.3.dist-info}/top_level.txt +0 -0
floodmodeller_api/ied.py CHANGED
@@ -1,306 +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 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
+
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)