Coverage for src/beamme/four_c/input_file.py: 85%

124 statements  

« prev     ^ index     » next       coverage.py v7.15.2, created at 2026-07-28 15:20 +0000

1# The MIT License (MIT) 

2# 

3# Copyright (c) 2018-2026 BeamMe Authors 

4# 

5# Permission is hereby granted, free of charge, to any person obtaining a copy 

6# of this software and associated documentation files (the "Software"), to deal 

7# in the Software without restriction, including without limitation the rights 

8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 

9# copies of the Software, and to permit persons to whom the Software is 

10# furnished to do so, subject to the following conditions: 

11# 

12# The above copyright notice and this permission notice shall be included in 

13# all copies or substantial portions of the Software. 

14# 

15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 

16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 

17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 

18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 

19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 

20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 

21# THE SOFTWARE. 

22"""This module defines the classes that are used to create an input file for 4C.""" 

23 

24import os as _os 

25from collections import defaultdict as _defaultdict 

26from collections.abc import Callable as _Callable 

27from datetime import datetime as _datetime 

28from pathlib import Path as _Path 

29from typing import Any as _Any 

30from typing import Self as _Self 

31 

32from fourcipp.fourc_input import FourCInput as _FourCInput 

33from fourcipp.fourc_input import sort_by_section_names as _sort_by_section_names 

34from fourcipp.utils.not_set import NOT_SET as _NOT_SET 

35 

36from beamme.core.conf import INPUT_FILE_HEADER as _INPUT_FILE_HEADER 

37from beamme.core.mesh import Mesh as _Mesh 

38from beamme.core.mesh_representation import MeshRepresentation as _MeshRepresentation 

39from beamme.four_c.boundary_condition_data import ( 

40 FourCBoundaryConditionData as _FourCBoundaryConditionData, 

41) 

42from beamme.four_c.element_data import FourCElementData as _FourCElementData 

43from beamme.four_c.input_file_dump_functions import ( 

44 dump_mesh_representation_to_input_file_vtu as _dump_mesh_representation_to_input_file_vtu, 

45) 

46from beamme.four_c.input_file_dump_functions import ( 

47 dump_mesh_representation_to_input_file_yaml as _dump_mesh_representation_to_input_file_yaml, 

48) 

49from beamme.four_c.input_file_dump_functions import ( 

50 dump_mesh_to_input_file as _dump_mesh_to_input_file, 

51) 

52from beamme.utils.environment import cubitpy_is_available as _cubitpy_is_available 

53from beamme.utils.environment import get_application_path as _get_application_path 

54from beamme.utils.environment import get_git_data as _get_git_data 

55 

56if _cubitpy_is_available(): 

57 import cubitpy as _cubitpy 

58 

59 

60class InputFile: 

61 """An item that represents a complete 4C input file.""" 

62 

63 def __init__(self) -> None: 

64 """Initialize the input file.""" 

65 self.fourc_input = _FourCInput() 

66 

67 # Register converters to directly convert non-primitive types 

68 # to native Python types via the FourCIPP type converter. 

69 self.fourc_input.type_converter.register_numpy_types() 

70 

71 # Contents of NOX xml file. 

72 self.nox_xml_contents = "" 

73 

74 # Mesh representation for this input file. 

75 self.mesh_representation = _MeshRepresentation() 

76 self.element_type_id_to_data: dict[_Any, _FourCElementData] = {} 

77 

78 # Boundary conditions 

79 self.boundary_conditions: dict[str, list[_FourCBoundaryConditionData]] = ( 

80 _defaultdict(list) 

81 ) 

82 

83 def __contains__(self, key: str) -> bool: 

84 """Contains function. 

85 

86 Allows to use the `in` operator. 

87 

88 Args: 

89 key: Section name to check if it is set 

90 

91 Returns: 

92 True if section is set 

93 """ 

94 return key in self.fourc_input 

95 

96 def __setitem__(self, key: str, value: _Any) -> None: 

97 """Set section. 

98 

99 Args: 

100 key: Section name 

101 value: Section entry 

102 """ 

103 self.fourc_input[key] = value 

104 

105 def __getitem__(self, key: str) -> _Any: 

106 """Get section of input file. 

107 

108 Allows to use the indexing operator. 

109 

110 Args: 

111 key: Section name to get 

112 

113 Returns: 

114 The section content 

115 """ 

116 return self.fourc_input[key] 

117 

118 @classmethod 

119 def from_4C_yaml( 

120 cls, input_file_path: str | _Path, header_only: bool = False 

121 ) -> _Self: 

122 """Load 4C yaml file. 

123 

124 Args: 

125 input_file_path: Path to yaml file 

126 header_only: Only extract header, i.e., all sections except the legacy ones 

127 

128 Returns: 

129 Initialised object 

130 """ 

131 obj = cls() 

132 obj.fourc_input = _FourCInput.from_4C_yaml(input_file_path, header_only) 

133 return obj 

134 

135 @property 

136 def sections(self) -> dict: 

137 """All the set sections. 

138 

139 Returns: 

140 dict: Set sections 

141 """ 

142 return self.fourc_input.sections 

143 

144 def pop(self, key: str, default_value: _Any = _NOT_SET) -> _Any: 

145 """Pop section of input file. 

146 

147 Args: 

148 key: Section name to pop 

149 

150 Returns: 

151 The section content 

152 """ 

153 return self.fourc_input.pop(key, default_value) 

154 

155 def add(self, object_to_add, **kwargs): 

156 """Add a mesh or a dictionary to the input file. 

157 

158 Args: 

159 object: The object to be added. This can be a mesh or a dictionary. 

160 **kwargs: Additional arguments to be passed to the add method. 

161 """ 

162 if isinstance(object_to_add, _Mesh): 

163 _dump_mesh_to_input_file(self, mesh=object_to_add, **kwargs) 

164 

165 else: 

166 self.fourc_input.combine_sections(object_to_add) 

167 

168 def get_fourcipp_input_with_mesh(self) -> _FourCInput: 

169 """Return a copy of the FourCIPP input file with the contents of the mesh 

170 representation dumped to the yaml sections.""" 

171 fourc_input = self.fourc_input.copy() 

172 _dump_mesh_representation_to_input_file_yaml( 

173 fourc_input, 

174 self.mesh_representation, 

175 self.element_type_id_to_data, 

176 self.boundary_conditions, 

177 ) 

178 return fourc_input 

179 

180 def dump( 

181 self, 

182 input_file_path: str | _Path, 

183 *, 

184 mesh_format: str = "vtu", 

185 vtu_binary: bool = True, 

186 nox_xml_file: str | None = None, 

187 add_header_default: bool = True, 

188 add_header_information: bool = True, 

189 add_footer_application_script: bool = True, 

190 validate=True, 

191 validate_sections_only: bool = False, 

192 sort_function: _Callable[[dict], dict] | None = _sort_by_section_names, 

193 fourcipp_yaml_style: bool = True, 

194 ): 

195 """Write the input file to disk. 

196 

197 Args: 

198 input_file_path: 

199 Path to the input file that should be created. 

200 mesh_format: 

201 The format in which the mesh information should be written. 

202 Currently, "vtu" and "yaml" are supported. 

203 vtu_binary: 

204 Only relevant if mesh_format is "vtu". If True, the vtu file will 

205 be written in binary format. Otherwise, it will be written in ascii format. 

206 nox_xml_file: 

207 If this is a string, the NOX xml file will be created with this 

208 name. If this is None, the NOX xml file will be created with the 

209 name of the input file with the extension ".nox.xml". 

210 add_header_default: 

211 Prepend the default header comment to the input file. 

212 add_header_information: 

213 If the information header should be exported to the input file 

214 Contains creation date, git details of BeamMe, CubitPy and 

215 original application which created the input file if available. 

216 add_footer_application_script: 

217 Append the application script which creates the input files as a 

218 comment at the end of the input file. 

219 validate: 

220 Validate if the created input file is compatible with 4C with FourCIPP. 

221 validate_sections_only: 

222 Validate each section independently. Required sections are no longer 

223 required, but the sections must be valid. 

224 sort_function: 

225 A function which sorts the sections of the input file. 

226 fourcipp_yaml_style: 

227 If True, the input file is written in the fourcipp yaml style. 

228 """ 

229 # Make sure the given input file is a Path instance. 

230 input_file_path = _Path(input_file_path) 

231 

232 # Base name of the input file without extension. 

233 if not input_file_path.name.endswith(".4C.yaml"): 

234 raise ValueError( 

235 "Input file must have a .4C.yaml extension, but got the " 

236 f"path {input_file_path}" 

237 ) 

238 input_file_base_name = input_file_path.name.removesuffix(".4C.yaml") 

239 

240 # Create a deep copy of the existing input sections - this function should not alter 

241 # the present instance of InputFile 

242 fourc_input = self.fourc_input.copy() 

243 

244 # Add the mesh representation, either directly to the yaml input file or via an 

245 # external mesh format. 

246 if mesh_format == "vtu": 

247 vtu_file_path = input_file_path.parent / ( 

248 input_file_base_name + ".mesh.vtu" 

249 ) 

250 vtu_grid = _dump_mesh_representation_to_input_file_vtu( 

251 fourc_input, 

252 self.mesh_representation, 

253 self.element_type_id_to_data, 

254 self.boundary_conditions, 

255 ) 

256 # Save the grid and add the file name to the input file 

257 vtu_grid.save(vtu_file_path, binary=vtu_binary) 

258 fourc_input["STRUCTURE GEOMETRY"]["FILE"] = vtu_file_path.name 

259 elif mesh_format == "yaml": 

260 _dump_mesh_representation_to_input_file_yaml( 

261 fourc_input, 

262 self.mesh_representation, 

263 self.element_type_id_to_data, 

264 self.boundary_conditions, 

265 ) 

266 else: 

267 raise ValueError(f"Unsupported mesh format: {mesh_format}.") 

268 

269 if self.nox_xml_contents: 

270 if nox_xml_file is None: 

271 nox_xml_file = input_file_base_name + ".nox.xml" 

272 

273 fourc_input["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file} 

274 

275 # Write the xml file to the disc. 

276 with open(input_file_path.parent / nox_xml_file, "w") as xml_file: 

277 xml_file.write(self.nox_xml_contents) 

278 

279 # Add information header to the input file 

280 if add_header_information: 

281 fourc_input.combine_sections({"TITLE": self._get_header()}) 

282 

283 fourc_input.dump( 

284 input_file_path=input_file_path, 

285 validate=validate, 

286 validate_sections_only=validate_sections_only, 

287 convert_to_native_types=False, # conversion already happens during add() 

288 sort_function=sort_function, 

289 use_fourcipp_yaml_style=fourcipp_yaml_style, 

290 ) 

291 

292 if add_header_default or add_footer_application_script: 

293 with open(input_file_path, "r") as input_file: 

294 lines = input_file.readlines() 

295 

296 if add_header_default: 

297 lines = ["# " + line + "\n" for line in _INPUT_FILE_HEADER] + lines 

298 

299 if add_footer_application_script: 

300 application_path = _get_application_path() 

301 if application_path is not None: 

302 lines += self._get_application_script(application_path) 

303 

304 with open(input_file_path, "w") as input_file: 

305 input_file.writelines(lines) 

306 

307 def _get_header(self) -> dict: 

308 """Return the information header for the current BeamMe run. 

309 

310 Returns: 

311 A dictionary with the header information. 

312 """ 

313 header: dict = {"BeamMe": {}} 

314 

315 header["BeamMe"]["creation_date"] = _datetime.now().isoformat( 

316 sep=" ", timespec="seconds" 

317 ) 

318 

319 # application which created the input file 

320 application_path = _get_application_path() 

321 if application_path is not None: 

322 header["BeamMe"]["Application"] = {"path": str(application_path)} 

323 

324 application_git_sha, application_git_date = _get_git_data( 

325 application_path.parent 

326 ) 

327 if application_git_sha is not None and application_git_date is not None: 

328 header["BeamMe"]["Application"].update( 

329 { 

330 "git_sha": application_git_sha, 

331 "git_date": application_git_date, 

332 } 

333 ) 

334 

335 # BeamMe information 

336 beamme_git_sha, beamme_git_date = _get_git_data( 

337 _Path(__file__).resolve().parent 

338 ) 

339 if beamme_git_sha is not None and beamme_git_date is not None: 

340 header["BeamMe"]["BeamMe"] = { 

341 "git_SHA": beamme_git_sha, 

342 "git_date": beamme_git_date, 

343 } 

344 

345 # CubitPy information 

346 if _cubitpy_is_available(): 

347 cubitpy_git_sha, cubitpy_git_date = _get_git_data( 

348 _os.path.dirname(_cubitpy.__file__) 

349 ) 

350 

351 if cubitpy_git_sha is not None and cubitpy_git_date is not None: 

352 header["BeamMe"]["CubitPy"] = { 

353 "git_SHA": cubitpy_git_sha, 

354 "git_date": cubitpy_git_date, 

355 } 

356 

357 return header 

358 

359 def _get_application_script(self, application_path: _Path) -> list[str]: 

360 """Get the script that created this input file. 

361 

362 Args: 

363 application_path: Path to the script that created this input file. 

364 Returns: 

365 A list of strings with the script that created this input file. 

366 """ 

367 application_script_lines = [ 

368 "# Application script which created this input file:\n" 

369 ] 

370 

371 with open(application_path) as script_file: 

372 application_script_lines.extend("# " + line for line in script_file) 

373 

374 return application_script_lines 

375 

376 def _contains_mesh_based_geometry(self, allowed_extensions: list[str]) -> bool: 

377 """Check if the input file contains mesh-based geometry. 

378 

379 Args: 

380 allowed_extensions: List of allowed file extensions for mesh files. 

381 

382 Returns: 

383 True if the input file contains mesh-based geometry of given type, 

384 False otherwise. 

385 """ 

386 structure_geometry_section = self.fourc_input.sections.get( 

387 "STRUCTURE GEOMETRY", None 

388 ) 

389 if structure_geometry_section is not None: 

390 file_name = structure_geometry_section.get("FILE", None) 

391 if file_name is not None: 

392 mesh_file_name = _Path(file_name) 

393 if mesh_file_name.suffix.lower() in allowed_extensions: 

394 return True 

395 return False 

396 

397 def contains_mesh_based_geometry_exodus(self) -> bool: 

398 """Check if the input file contains exodus mesh-based geometry. 

399 

400 Returns: 

401 True if the input file contains exodus mesh-based geometry, False otherwise. 

402 """ 

403 return self._contains_mesh_based_geometry([".exo", ".e"]) 

404 

405 def contains_mesh_based_geometry_vtu(self) -> bool: 

406 """Check if the input file contains VTU mesh-based geometry. 

407 

408 Returns: 

409 True if the input file contains VTU mesh-based geometry, False otherwise. 

410 """ 

411 return self._contains_mesh_based_geometry([".vtu"])