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

157 statements  

« prev     ^ index     » next       coverage.py v7.13.1, created at 2026-01-06 06:24 +0000

1# The MIT License (MIT) 

2# 

3# Copyright (c) 2018-2025 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 

234C.""" 

24 

25from __future__ import annotations as _annotations 

26 

27import os as _os 

28from datetime import datetime as _datetime 

29from pathlib import Path as _Path 

30from typing import Any as _Any 

31from typing import Callable as _Callable 

32from typing import List as _List 

33 

34from fourcipp.fourc_input import FourCInput as _FourCInput 

35from fourcipp.fourc_input import sort_by_section_names as _sort_by_section_names 

36from fourcipp.utils.not_set import NOT_SET as _NOT_SET 

37 

38from beamme.core.conf import INPUT_FILE_HEADER as _INPUT_FILE_HEADER 

39from beamme.core.conf import bme as _bme 

40from beamme.core.function import Function as _Function 

41from beamme.core.material import Material as _Material 

42from beamme.core.mesh import Mesh as _Mesh 

43from beamme.core.node import Node as _Node 

44from beamme.core.nurbs_patch import NURBSPatch as _NURBSPatch 

45from beamme.four_c.input_file_dump_item import dump_item_to_list as _dump_item_to_list 

46from beamme.four_c.input_file_dump_item import ( 

47 dump_item_to_section as _dump_item_to_section, 

48) 

49from beamme.four_c.input_file_mappings import ( 

50 INPUT_FILE_MAPPINGS as _INPUT_FILE_MAPPINGS, 

51) 

52from beamme.four_c.material import ( 

53 get_all_contained_materials as _get_all_contained_materials, 

54) 

55from beamme.utils.environment import cubitpy_is_available as _cubitpy_is_available 

56from beamme.utils.environment import get_application_path as _get_application_path 

57from beamme.utils.environment import get_git_data as _get_git_data 

58 

59if _cubitpy_is_available(): 

60 import cubitpy as _cubitpy 

61 

62 

63class InputFile: 

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

65 

66 def __init__(self): 

67 """Initialize the input file.""" 

68 

69 self.fourc_input = _FourCInput() 

70 

71 # Contents of NOX xml file. 

72 self.nox_xml_contents = "" 

73 

74 # Register converters to directly convert non-primitive types 

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

76 self.fourc_input.type_converter.register_numpy_types() 

77 self.fourc_input.type_converter.register_type( 

78 (_Function, _Material, _Node), lambda converter, obj: obj.i_global + 1 

79 ) 

80 

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

82 """Contains function. 

83 

84 Allows to use the `in` operator. 

85 

86 Args: 

87 key: Section name to check if it is set 

88 

89 Returns: 

90 True if section is set 

91 """ 

92 

93 return key in self.fourc_input 

94 

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

96 """Set section. 

97 

98 Args: 

99 key: Section name 

100 value: Section entry 

101 """ 

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 

117 return self.fourc_input[key] 

118 

119 @classmethod 

120 def from_4C_yaml( 

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

122 ) -> InputFile: 

123 """Load 4C yaml file. 

124 

125 Args: 

126 input_file_path: Path to yaml file 

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

128 

129 Returns: 

130 Initialised object 

131 """ 

132 

133 obj = cls() 

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

135 return obj 

136 

137 @property 

138 def sections(self) -> dict: 

139 """All the set sections. 

140 

141 Returns: 

142 dict: Set sections 

143 """ 

144 

145 return self.fourc_input.sections 

146 

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

148 """Pop section of input file. 

149 

150 Args: 

151 key: Section name to pop 

152 

153 Returns: 

154 The section content 

155 """ 

156 

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

158 

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

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

161 

162 Args: 

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

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

165 """ 

166 

167 if isinstance(object_to_add, _Mesh): 

168 self.add_mesh_to_input_file(mesh=object_to_add, **kwargs) 

169 

170 else: 

171 self.fourc_input.combine_sections(object_to_add) 

172 

173 def dump( 

174 self, 

175 input_file_path: str | _Path, 

176 *, 

177 nox_xml_file: str | None = None, 

178 add_header_default: bool = True, 

179 add_header_information: bool = True, 

180 add_footer_application_script: bool = True, 

181 validate=True, 

182 validate_sections_only: bool = False, 

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

184 fourcipp_yaml_style: bool = True, 

185 ): 

186 """Write the input file to disk. 

187 

188 Args: 

189 input_file_path: 

190 Path to the input file that should be created. 

191 nox_xml_file: 

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

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

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

195 add_header_default: 

196 Prepend the default header comment to the input file. 

197 add_header_information: 

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

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

200 original application which created the input file if available. 

201 add_footer_application_script: 

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

203 comment at the end of the input file. 

204 validate: 

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

206 validate_sections_only: 

207 Validate each section independently. Required sections are no longer 

208 required, but the sections must be valid. 

209 sort_function: 

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

211 fourcipp_yaml_style: 

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

213 """ 

214 

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

216 input_file_path = _Path(input_file_path) 

217 

218 if self.nox_xml_contents: 

219 if nox_xml_file is None: 

220 nox_xml_file = input_file_path.name.split(".")[0] + ".nox.xml" 

221 

222 self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file} 

223 

224 # Write the xml file to the disc. 

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

226 xml_file.write(self.nox_xml_contents) 

227 

228 # Add information header to the input file 

229 if add_header_information: 

230 self.add({"TITLE": self._get_header()}) 

231 

232 self.fourc_input.dump( 

233 input_file_path=input_file_path, 

234 validate=validate, 

235 validate_sections_only=validate_sections_only, 

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

237 sort_function=sort_function, 

238 use_fourcipp_yaml_style=fourcipp_yaml_style, 

239 ) 

240 

241 if add_header_default or add_footer_application_script: 

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

243 lines = input_file.readlines() 

244 

245 if add_header_default: 

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

247 

248 if add_footer_application_script: 

249 application_path = _get_application_path() 

250 if application_path is not None: 

251 lines += self._get_application_script(application_path) 

252 

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

254 input_file.writelines(lines) 

255 

256 def add_mesh_to_input_file(self, mesh: _Mesh) -> None: 

257 """Add a mesh to the input file. 

258 

259 Args: 

260 mesh: The mesh to be added to the input file. 

261 """ 

262 

263 if _bme.check_overlapping_elements: 

264 mesh.check_overlapping_elements() 

265 

266 # Compute geometry-set starting indices 

267 start_indices_geometry_set = { 

268 geometry_type: max( 

269 (entry["d_id"] for entry in self.sections.get(section_name, [])), 

270 default=0, 

271 ) 

272 for geometry_type, section_name in _INPUT_FILE_MAPPINGS[ 

273 "geometry_sets_geometry_to_condition_name" 

274 ].items() 

275 } 

276 

277 # Determine global start indices 

278 start_index_nodes = len(self.sections.get("NODE COORDS", [])) 

279 

280 start_index_elements = sum( 

281 len(self.sections.get(section, [])) 

282 for section in ("FLUID ELEMENTS", "STRUCTURE ELEMENTS") 

283 ) 

284 

285 start_index_functions = max( 

286 ( 

287 int(section.split("FUNCT")[-1]) 

288 for section in self.sections 

289 if section.startswith("FUNCT") 

290 ), 

291 default=0, 

292 ) 

293 

294 start_index_materials = max( 

295 (material["MAT"] for material in self.sections.get("MATERIALS", [])), 

296 default=0, 

297 ) # materials imported from YAML may have arbitrary numbering 

298 

299 # Add sets from couplings and boundary conditions to a temp container 

300 mesh.unlink_nodes() 

301 mesh_sets = mesh.get_unique_geometry_sets( 

302 geometry_set_start_indices=start_indices_geometry_set 

303 ) 

304 

305 # Assign global indices 

306 # Nodes 

307 if len(mesh.nodes) != len(set(mesh.nodes)): 

308 raise ValueError("Nodes are not unique!") 

309 for i, node in enumerate(mesh.nodes, start=start_index_nodes): 

310 node.i_global = i 

311 

312 # Elements 

313 if len(mesh.elements) != len(set(mesh.elements)): 

314 raise ValueError("Elements are not unique!") 

315 i = start_index_elements 

316 nurbs_count = 0 

317 

318 for element in mesh.elements: 

319 element.i_global = i 

320 if isinstance(element, _NURBSPatch): 

321 element.i_nurbs_patch = nurbs_count 

322 i += element.get_number_of_elements() 

323 nurbs_count += 1 

324 continue 

325 i += 1 

326 

327 # Materials: Get a list of all materials in the mesh, 

328 # including nested sub-materials. 

329 all_materials = [ 

330 material 

331 for mesh_material in mesh.materials 

332 for material in _get_all_contained_materials(mesh_material) 

333 ] 

334 if len(all_materials) != len(set(all_materials)): 

335 raise ValueError("Materials are not unique!") 

336 for i, material in enumerate(all_materials, start=start_index_materials): 

337 material.i_global = i 

338 

339 # Functions 

340 if len(mesh.functions) != len(set(mesh.functions)): 

341 raise ValueError("Functions are not unique!") 

342 for i, function in enumerate(mesh.functions, start=start_index_functions): 

343 function.i_global = i 

344 

345 # Dump mesh to input file 

346 def _dump(section_name: str, items: _List) -> None: 

347 """Dump list of items to a section in the input file. 

348 

349 Args: 

350 section_name: Name of the section 

351 items: List of items to be dumped 

352 """ 

353 if not items: # do not write empty sections 

354 return 

355 dumped: list[_Any] = [] 

356 for item in items: 

357 _dump_item_to_list(dumped, item) 

358 

359 # Go through FourCIPP to convert to native types 

360 # TODO this can be simplified/removed by using an internal type converter 

361 if section_name in self.sections: 

362 existing = self.pop(section_name) 

363 existing.extend(dumped) 

364 dumped = existing 

365 

366 self.add({section_name: dumped}) 

367 

368 # Materials 

369 _dump("MATERIALS", all_materials) 

370 

371 # Functions 

372 for function in mesh.functions: 

373 self.add({f"FUNCT{function.i_global + 1}": function.data}) 

374 

375 # Couplings 

376 # If there are couplings in the mesh, set the link between the nodes 

377 # and elements, so the couplings can decide which DOFs they couple, 

378 # depending on the type of the connected beam element. 

379 if any( 

380 mesh.boundary_conditions.get((key, _bme.geo.point), []) 

381 for key in (_bme.bc.point_coupling, _bme.bc.point_coupling_penalty) 

382 ): 

383 mesh.set_node_links() 

384 

385 # Boundary conditions 

386 for (bc_key, geometry_key), bc_list in mesh.boundary_conditions.items(): 

387 if bc_list: 

388 section = ( 

389 bc_key 

390 if isinstance(bc_key, str) 

391 else _INPUT_FILE_MAPPINGS["boundary_conditions"][ 

392 (bc_key, geometry_key) 

393 ] 

394 ) 

395 _dump(section, bc_list) 

396 

397 # Additional element sections (NURBS etc.) 

398 for element in mesh.elements: 

399 _dump_item_to_section(self, element) 

400 

401 # Geometry sets 

402 for geometry_key, items in mesh_sets.items(): 

403 _dump( 

404 _INPUT_FILE_MAPPINGS["geometry_sets_geometry_to_condition_name"][ 

405 geometry_key 

406 ], 

407 items, 

408 ) 

409 

410 # Nodes 

411 _dump("NODE COORDS", mesh.nodes) 

412 # Elements 

413 _dump("STRUCTURE ELEMENTS", mesh.elements) 

414 

415 # TODO: reset all links and counters set in this method. 

416 

417 def _get_header(self) -> dict: 

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

419 

420 Returns: 

421 A dictionary with the header information. 

422 """ 

423 

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

425 

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

427 sep=" ", timespec="seconds" 

428 ) 

429 

430 # application which created the input file 

431 application_path = _get_application_path() 

432 if application_path is not None: 

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

434 

435 application_git_sha, application_git_date = _get_git_data( 

436 application_path.parent 

437 ) 

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

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

440 { 

441 "git_sha": application_git_sha, 

442 "git_date": application_git_date, 

443 } 

444 ) 

445 

446 # BeamMe information 

447 beamme_git_sha, beamme_git_date = _get_git_data( 

448 _Path(__file__).resolve().parent 

449 ) 

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

451 header["BeamMe"]["BeamMe"] = { 

452 "git_SHA": beamme_git_sha, 

453 "git_date": beamme_git_date, 

454 } 

455 

456 # CubitPy information 

457 if _cubitpy_is_available(): 

458 cubitpy_git_sha, cubitpy_git_date = _get_git_data( 

459 _os.path.dirname(_cubitpy.__file__) 

460 ) 

461 

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

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

464 "git_SHA": cubitpy_git_sha, 

465 "git_date": cubitpy_git_date, 

466 } 

467 

468 return header 

469 

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

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

472 

473 Args: 

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

475 Returns: 

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

477 """ 

478 

479 application_script_lines = [ 

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

481 ] 

482 

483 with open(application_path) as script_file: 

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

485 

486 return application_script_lines