Coverage for src/beamme/four_c/model_importer.py: 93%

178 statements  

« prev     ^ index     » next       coverage.py v7.14.1, created at 2026-06-08 11:03 +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 contains functions to load and parse existing 4C input files.""" 

23 

24from collections import defaultdict as _defaultdict 

25from pathlib import Path as _Path 

26from typing import Tuple as _Tuple 

27 

28import numpy as _np 

29 

30from beamme.core.boundary_condition import BoundaryCondition as _BoundaryCondition 

31from beamme.core.boundary_condition import ( 

32 BoundaryConditionBase as _BoundaryConditionBase, 

33) 

34from beamme.core.conf import Geometry as _Geometry 

35from beamme.core.conf import bme as _bme 

36from beamme.core.coupling import Coupling as _Coupling 

37from beamme.core.geometry_set import GeometrySetNodes as _GeometrySetNodes 

38from beamme.core.mesh import Mesh as _Mesh 

39from beamme.core.mesh_representation import ( 

40 MESH_REPRESENTATION_MAPPINGS as _MESH_REPRESENTATION_MAPPINGS, 

41) 

42from beamme.core.mesh_representation import GeometrySetInfo as _GeometrySetInfo 

43from beamme.core.mesh_representation import MeshRepresentation as _MeshRepresentation 

44from beamme.core.mesh_representation import ( 

45 string_to_geometry_set_info as _string_to_geometry_set_info, 

46) 

47from beamme.core.node import Node as _Node 

48from beamme.four_c.element_data import FourCElementData as _FourCElementData 

49from beamme.four_c.element_data import ( 

50 four_c_element_data_from_legacy_dict as _four_c_element_data_from_legacy_dict, 

51) 

52from beamme.four_c.element_solid import get_four_c_solid as _get_four_c_solid 

53from beamme.four_c.input_file import InputFile as _InputFile 

54from beamme.four_c.input_file_mappings import ( 

55 INPUT_FILE_MAPPINGS as _INPUT_FILE_MAPPINGS, 

56) 

57from beamme.four_c.material import MaterialSolid as _MaterialSolid 

58from beamme.utils.environment import cubitpy_is_available as _cubitpy_is_available 

59 

60if _cubitpy_is_available(): 

61 from cubitpy.cubit_to_fourc_input import ( 

62 get_input_file_with_mesh as _get_input_file_with_mesh, 

63 ) 

64 

65 

66class UniqueDataTracker: 

67 """Helper class to track unique data dictionaries and assign IDs to them. 

68 

69 When importing input files, we need to identify elements of the same 

70 type. The type information is given in dictionaries. This class 

71 provides a tracker that can be queried with a given element data and 

72 return an already matching element type ID or create a new one. 

73 """ 

74 

75 def __init__(self) -> None: 

76 self.unique_id_to_data: dict[int, _FourCElementData] = {} 

77 

78 def get_unique_id(self, data: _FourCElementData) -> int: 

79 """Get the unique ID for the given data. If the data has not been seen 

80 before, a new ID will be assigned to it. 

81 

82 Args: 

83 data: The data dictionary to get the ID for. 

84 

85 Returns: 

86 The unique ID for the given data. 

87 """ 

88 for unique_id, seen_data in self.unique_id_to_data.items(): 

89 if data == seen_data: 

90 return unique_id 

91 

92 # If we reach this point, the data has not been seen before. We assign a new ID to it. 

93 new_unique_id = len(self.unique_id_to_data) 

94 self.unique_id_to_data[new_unique_id] = data 

95 return new_unique_id 

96 

97 

98def import_cubitpy_model( 

99 cubit, convert_input_to_mesh: bool = False 

100) -> _Tuple[_InputFile, _Mesh]: 

101 """Convert a CubitPy instance to a BeamMe InputFile. 

102 

103 Args: 

104 cubit (CubitPy): An instance of a cubit model. 

105 convert_input_to_mesh: If this is false, the cubit model will be 

106 converted to plain FourCIPP input data. If this is true, an input 

107 file with all the parameters will be returned and a mesh which 

108 contains the mesh information from cubit converted to BeamMe 

109 objects. 

110 

111 Returns: 

112 A tuple with the input file and the mesh. If convert_input_to_mesh is 

113 False, the mesh will be empty. Note that the input sections which are 

114 converted to a BeamMe mesh are removed from the input file object. 

115 """ 

116 

117 input_file = _InputFile() 

118 input_file.add(_get_input_file_with_mesh(cubit).sections) 

119 

120 if convert_input_to_mesh: 

121 return _extract_mesh_from_input_file(input_file) 

122 else: 

123 return input_file, _Mesh() 

124 

125 

126def import_four_c_model( 

127 input_file_path: _Path, convert_input_to_mesh: bool = False 

128) -> _Tuple[_InputFile, _Mesh]: 

129 """Import an existing 4C input file and optionally convert it into a BeamMe 

130 mesh. 

131 

132 Args: 

133 input_file_path: A file path to an existing 4C input file that will be 

134 imported. 

135 convert_input_to_mesh: If True, the input file will be converted to a 

136 BeamMe mesh. 

137 

138 Returns: 

139 A tuple with the input file and the mesh. If convert_input_to_mesh is 

140 False, the mesh will be empty. Note that the input sections which are 

141 converted to a BeamMe mesh are removed from the input file object. 

142 """ 

143 

144 input_file = _InputFile().from_4C_yaml(input_file_path=input_file_path) 

145 

146 if convert_input_to_mesh: 

147 return _extract_mesh_from_input_file(input_file) 

148 else: 

149 return input_file, _Mesh() 

150 

151 

152def _extract_mesh_from_input_file(input_file: _InputFile) -> tuple[_InputFile, _Mesh]: 

153 """Convert an InputFile into a native mesh by translating sections like 

154 materials, nodes, elements, geometry sets, and boundary conditions. 

155 

156 Args: 

157 input_file: The input file containing 4C sections. 

158 Returns: 

159 A tuple (input_file, mesh). The input_file is modified in place to remove 

160 sections converted into BeamMe objects. 

161 """ 

162 if input_file.contains_external_mesh_based_geometry(): 

163 raise NotImplementedError( 

164 "Importing external mesh-based geometry from 4C input files " 

165 "is not yet implemented." 

166 ) 

167 else: 

168 ( 

169 mesh_representation, 

170 element_type_id_to_data, 

171 node_set_id_mesh_representation_to_input_file, 

172 ) = _extract_mesh_representation(input_file) 

173 return _create_mesh_from_mesh_representation( 

174 input_file, 

175 mesh_representation, 

176 element_type_id_to_data, 

177 node_set_id_mesh_representation_to_input_file, 

178 ) 

179 

180 

181def _extract_mesh_representation( 

182 input_file: _InputFile, 

183) -> tuple[_MeshRepresentation, dict[int, _FourCElementData], dict[int, int]]: 

184 """Extract the mesh representation from mesh data directly contained in the 

185 input file. 

186 

187 This will do an inplace removal of the mesh data from the provided input file. 

188 

189 Args: 

190 input_file: The input file containing the mesh data, will be modified in place. 

191 

192 Returns: 

193 A tuple containing: 

194 - `mesh_representation`: Contains the mesh data extracted from the input file. 

195 - `element_type_id_to_data`: A mapping between the element type ID and the 

196 element data. 

197 - `node_set_id_mesh_representation_to_input_file`: A mapping that can be used 

198 to map the IDs in the mesh representation to the IDs in the input file. 

199 """ 

200 

201 # extract nodes 

202 nodes = input_file.pop("NODE COORDS", []) 

203 n_nodes = len(nodes) 

204 points = _np.zeros((n_nodes, 3)) 

205 point_types = _np.full(n_nodes, -1) 

206 control_point_weights = _np.full(n_nodes, -1.0) 

207 for i, node in enumerate(nodes): 

208 four_c_node_type = node["data"]["type"] 

209 node_id = node["id"] 

210 try: 

211 node_type = _INPUT_FILE_MAPPINGS["four_c_node_type_to_beamme_node_type"][ 

212 four_c_node_type 

213 ] 

214 except KeyError: 

215 raise ValueError( 

216 f"Unknown node type `{four_c_node_type}` for node {node_id}." 

217 ) 

218 points[i] = node["COORD"] 

219 point_types[i] = node_type.value 

220 if node_type == _bme.node_type.control_point: 

221 control_point_weights[i] = node["data"]["weight"] 

222 

223 # extract elements 

224 element_type_tracker = UniqueDataTracker() 

225 elements = input_file.pop("STRUCTURE ELEMENTS", []) 

226 n_elements = len(elements) 

227 cell_connectivity = [] 

228 cell_types = _np.full(n_elements, -1) 

229 cell_element_type_ids = _np.full(n_elements, -1) 

230 cell_material_ids = _np.full(n_elements, -1) 

231 for i_element, input_element in enumerate(elements): 

232 four_c_element_data, element_id, connectivity, material_id = ( 

233 _four_c_element_data_from_legacy_dict(input_element) 

234 ) 

235 element_type_id = element_type_tracker.get_unique_id(four_c_element_data) 

236 

237 # Check if connectivity has to be reordered 

238 reorder_indices = _INPUT_FILE_MAPPINGS[ 

239 "four_c_cell_to_vtk_connectivity_mapping" 

240 ].get(four_c_element_data.four_c_cell, None) 

241 if reorder_indices is not None: 

242 connectivity = connectivity[reorder_indices] 

243 

244 cell_connectivity.extend([len(connectivity), *connectivity.tolist()]) 

245 

246 try: 

247 vtk_cell_type = _INPUT_FILE_MAPPINGS["four_c_cell_to_vtk_cell_type"][ 

248 four_c_element_data.four_c_cell 

249 ] 

250 except KeyError: 

251 raise ValueError( 

252 f"Unknown cell type `{four_c_element_data.four_c_cell}` for element {element_id}." 

253 ) 

254 

255 cell_types[i_element] = vtk_cell_type 

256 cell_element_type_ids[i_element] = element_type_id 

257 cell_material_ids[i_element] = material_id 

258 

259 # extract geometry sets 

260 node_sets: list[_GeometrySetInfo] = [] 

261 node_set_id_mesh_representation_to_input_file: dict[int, int] = {} 

262 for section_name in input_file.sections: 

263 if not section_name.endswith("TOPOLOGY"): 

264 continue 

265 

266 items = input_file.pop(section_name, []) 

267 if not items: 

268 continue 

269 

270 # Find geometry type for this section 

271 try: 

272 geometry_type = _INPUT_FILE_MAPPINGS[ 

273 "geometry_sets_condition_to_geometry_name" 

274 ][section_name] 

275 except KeyError as e: 

276 raise ValueError(f"Unknown geometry section: {section_name}") from e 

277 

278 # Extract geometry set indices 

279 geom_dict: dict[int, set[int]] = _defaultdict(set) 

280 for entry in items: 

281 geom_dict[entry["d_id"]].add(entry["node_id"] - 1) 

282 

283 for input_file_node_set_id, node_ids in geom_dict.items(): 

284 node_set_id = len(node_sets) 

285 

286 node_set_flag = _np.zeros(n_nodes, dtype=int) 

287 node_set_flag[list(node_ids)] = 1 

288 

289 node_sets.append( 

290 _GeometrySetInfo( 

291 geometry_type=geometry_type, 

292 i_global=node_set_id, 

293 point_flag_vector=node_set_flag, 

294 ) 

295 ) 

296 

297 node_set_id_mesh_representation_to_input_file[node_set_id] = ( 

298 input_file_node_set_id 

299 ) 

300 

301 # Create the mesh representation and add the extracted data to it. 

302 mesh_representation = _MeshRepresentation( 

303 cell_connectivity=cell_connectivity, 

304 cell_types=cell_types, 

305 points=points, 

306 geometry_sets=node_sets, 

307 point_data={ 

308 "point_type": point_types, 

309 "control_point_weight": control_point_weights, 

310 }, 

311 cell_data={ 

312 "element_type_id": cell_element_type_ids, 

313 "material_id": cell_material_ids, 

314 }, 

315 ) 

316 

317 return ( 

318 mesh_representation, 

319 element_type_tracker.unique_id_to_data, 

320 node_set_id_mesh_representation_to_input_file, 

321 ) 

322 

323 

324def _create_mesh_from_mesh_representation( 

325 input_file, 

326 mesh_representation, 

327 element_type_id_to_data, 

328 node_set_id_mesh_representation_to_input_file, 

329) -> tuple[_InputFile, _Mesh]: 

330 """Extract a BeamMe mesh from a mesh representation. 

331 

332 Args: 

333 input_file: The input file containing general data. 

334 mesh_representation: The mesh representation to convert. 

335 node_set_id_mesh_representation_to_input_file: A mapping of the mesh 

336 representation node set IDs to input file IDs, which can be used to link 

337 the geometry sets in the input file to the node sets in the mesh 

338 representation. 

339 

340 Returns: 

341 A tuple (input_file, mesh). The input_file is modified in place to remove 

342 sections converted into the BeamMe mesh. 

343 """ 

344 

345 # convert all sections to native objects and add to a new mesh 

346 mesh = _Mesh() 

347 

348 # extract materials 

349 material_id_map = _extract_materials_from_input_file(input_file) 

350 mesh.materials.extend(material_id_map.values()) 

351 

352 # extract nodes 

353 for node_coordinates, node_type in zip( 

354 mesh_representation.points, mesh_representation.point_data["point_type"] 

355 ): 

356 if node_type == _bme.node_type.node.value: 

357 mesh.nodes.append(_Node(node_coordinates)) 

358 else: 

359 raise ValueError( 

360 f"Mesh conversion for node type {_bme.node_type(node_type).name} is not implemented!" 

361 ) 

362 

363 # extract element types 

364 element_type_id_to_element_type: dict[int, type] = {} 

365 for ( 

366 element_type_id, 

367 element_data, 

368 ) in element_type_id_to_data.items(): 

369 element_type, n_nodes = _INPUT_FILE_MAPPINGS[ 

370 "four_c_cell_to_element_type_and_n_nodes" 

371 ][element_data.four_c_cell] 

372 if not element_type == _bme.element_type.solid: 

373 raise ValueError( 

374 f"Mesh conversion for element type {element_type} is not implemented!" 

375 ) 

376 element_type_id_to_element_type[element_type_id] = _get_four_c_solid( 

377 element_type, 

378 element_data.four_c_type, 

379 n_nodes=n_nodes, 

380 element_technology=element_data.element_technology, 

381 ) 

382 

383 # loop over the elements and create the mesh elements with the correct type, connectivity and material. 

384 for connectivity, cell_element_type_id_, material_id in zip( 

385 mesh_representation.connectivity_iterator(), 

386 mesh_representation.cell_data["element_type_id"], 

387 mesh_representation.cell_data["material_id"], 

388 ): 

389 element_type = element_type_id_to_element_type[cell_element_type_id_] 

390 

391 reorder_indices = _MESH_REPRESENTATION_MAPPINGS[ 

392 "element_type_and_n_nodes_to_connectivity_mapping_vtk_to_beamme" 

393 ].get((element_type.element_type, len(connectivity)), None) 

394 if reorder_indices is not None: 

395 nodes = [mesh.nodes[connectivity[i]] for i in reorder_indices] 

396 else: 

397 nodes = [mesh.nodes[i] for i in connectivity] 

398 

399 element = element_type(nodes=nodes) 

400 

401 if not material_id == -1: 

402 element.material = material_id_map[material_id] 

403 mesh.elements.append(element) 

404 

405 # extract geometry sets 

406 geometry_sets_in_sections: dict[_Geometry, dict[int, _GeometrySetNodes]] = ( 

407 _defaultdict(dict) 

408 ) 

409 for name in mesh_representation.point_data.keys(): 

410 info = _string_to_geometry_set_info(name) 

411 if info is not None: 

412 node_indices = _np.nonzero(mesh_representation.point_data[name])[0] 

413 geometry_type = info.geometry_type 

414 geometry_set = _GeometrySetNodes( 

415 geometry_type, nodes=[mesh.nodes[i] for i in node_indices] 

416 ) 

417 input_file_id = node_set_id_mesh_representation_to_input_file[info.i_global] 

418 geometry_sets_in_sections[geometry_type][input_file_id] = geometry_set 

419 mesh.add(geometry_set) 

420 

421 # extract boundary conditions 

422 _standard_bc_types = ( 

423 _bme.bc.dirichlet, 

424 _bme.bc.neumann, 

425 _bme.bc.locsys, 

426 _bme.bc.beam_to_solid_surface_meshtying, 

427 _bme.bc.beam_to_solid_surface_contact, 

428 _bme.bc.beam_to_solid_volume_meshtying, 

429 ) 

430 

431 for (bc_key, geometry_type), section_name in _INPUT_FILE_MAPPINGS[ 

432 "boundary_conditions" 

433 ].items(): 

434 for bc_data in input_file.pop(section_name, []): 

435 geometry_set = geometry_sets_in_sections[geometry_type][bc_data.pop("E")] 

436 

437 bc_obj: _BoundaryConditionBase 

438 

439 if bc_key in _standard_bc_types or isinstance(bc_key, str): 

440 bc_obj = _BoundaryCondition(geometry_set, bc_data, bc_type=bc_key) 

441 elif bc_key is _bme.bc.point_coupling: 

442 bc_obj = _Coupling( 

443 geometry_set, bc_key, bc_data, check_overlapping_nodes=False 

444 ) 

445 else: 

446 raise ValueError(f"Unexpected boundary condition: {bc_key}") 

447 

448 mesh.boundary_conditions.append((bc_key, geometry_type), bc_obj) 

449 

450 return input_file, mesh 

451 

452 

453def _extract_materials_from_input_file( 

454 input_file: _InputFile, 

455) -> dict[int, _MaterialSolid]: 

456 """Extract all materials from the input file and convert them to BeamMe 

457 materials. 

458 

459 Args: 

460 input_file: The input file containing the material sections. 

461 

462 Returns: 

463 A mapping of material IDs to BeamMe material objects. 

464 """ 

465 

466 material_id_map_all = {} 

467 

468 for mat in input_file.pop("MATERIALS", []): 

469 mat_id = mat.pop("MAT") - 1 

470 if len(mat) != 1: 

471 raise ValueError( 

472 f"Could not convert the material data `{mat}` to a BeamMe material!" 

473 ) 

474 mat_name, mat_data = list(mat.items())[0] 

475 material = _MaterialSolid(material_string=mat_name, data=mat_data) 

476 material_id_map_all[mat_id] = material 

477 

478 nested_materials = set() 

479 for material in material_id_map_all.values(): 

480 # Replace the integer IDs in the "MATIDS" list of the material with the actual 

481 # material objects. 

482 sub_materials = material.data.get("MATIDS", []) 

483 sub_material_ids = _np.array(sub_materials) - 1 

484 for i_sub_material, sub_material_id in enumerate(sub_material_ids): 

485 try: 

486 sub_materials[i_sub_material] = material_id_map_all[sub_material_id] 

487 except KeyError as key_exception: 

488 raise KeyError( 

489 f"Material ID {sub_material_id} not in material_id_map_all (available " 

490 f"IDs: {list(material_id_map_all.keys())})." 

491 ) from key_exception 

492 nested_materials.add(sub_material_id) 

493 

494 # Get a map of all non-nested materials. We assume that only those are used as 

495 # materials for elements. Also, add the non-nested materials to the mesh. 

496 material_id_map = { 

497 key: val 

498 for key, val in material_id_map_all.items() 

499 if key not in nested_materials 

500 } 

501 

502 return material_id_map