Coverage for src/meshpy/four_c/input_file.py: 93%

298 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-28 04:21 +0000

1# The MIT License (MIT) 

2# 

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

25import copy as _copy 

26import os as _os 

27import shutil as _shutil 

28import subprocess as _subprocess # nosec B404 

29import sys as _sys 

30from datetime import datetime as _datetime 

31from pathlib import Path as _Path 

32from typing import Any as _Any 

33from typing import Dict as _Dict 

34from typing import List as _List 

35from typing import Optional as _Optional 

36from typing import Tuple as _Tuple 

37from typing import Union as _Union 

38 

39import yaml as _yaml 

40 

41import meshpy.core.conf as _conf 

42from meshpy.core.boundary_condition import BoundaryCondition as _BoundaryCondition 

43from meshpy.core.boundary_condition import ( 

44 BoundaryConditionBase as _BoundaryConditionBase, 

45) 

46from meshpy.core.conf import mpy as _mpy 

47from meshpy.core.container import ContainerBase as _ContainerBase 

48from meshpy.core.coupling import Coupling as _Coupling 

49from meshpy.core.element import Element as _Element 

50from meshpy.core.geometry_set import GeometryName as _GeometryName 

51from meshpy.core.geometry_set import GeometrySetNodes as _GeometrySetNodes 

52from meshpy.core.mesh import Mesh as _Mesh 

53from meshpy.core.node import Node as _Node 

54from meshpy.core.nurbs_patch import NURBSPatch as _NURBSPatch 

55from meshpy.four_c.yaml_dumper import MeshPyDumper as _MeshPyDumper 

56from meshpy.utils.environment import cubitpy_is_available as _cubitpy_is_available 

57from meshpy.utils.environment import fourcipp_is_available as _fourcipp_is_available 

58 

59if _cubitpy_is_available(): 

60 import cubitpy as _cubitpy 

61 

62 

63def _boundary_condition_from_dict( 

64 geometry_set: _GeometrySetNodes, 

65 bc_key: _Union[_conf.BoundaryCondition, str], 

66 data: _Dict, 

67) -> _BoundaryConditionBase: 

68 """This function acts as a factory and creates the correct boundary 

69 condition object from a dictionary parsed from an input file.""" 

70 

71 del data["E"] 

72 

73 if bc_key in ( 

74 _mpy.bc.dirichlet, 

75 _mpy.bc.neumann, 

76 _mpy.bc.locsys, 

77 _mpy.bc.beam_to_solid_surface_meshtying, 

78 _mpy.bc.beam_to_solid_surface_contact, 

79 _mpy.bc.beam_to_solid_volume_meshtying, 

80 ) or isinstance(bc_key, str): 

81 return _BoundaryCondition(geometry_set, data, bc_type=bc_key) 

82 elif bc_key is _mpy.bc.point_coupling: 

83 return _Coupling(geometry_set, bc_key, data, check_overlapping_nodes=False) 

84 else: 

85 raise ValueError("Got unexpected boundary condition!") 

86 

87 

88def _get_geometry_set_indices_from_section( 

89 section_list: _List, *, append_node_ids: bool = True 

90) -> _Dict: 

91 """Return a dictionary with the geometry set ID as keys and the node IDs as 

92 values. 

93 

94 Args: 

95 section_list: A list with the legacy strings for the geometry pair 

96 append_node_ids: If the node IDs shall be appended, or only the 

97 dict with the keys should be returned. 

98 """ 

99 

100 if _fourcipp_is_available(): 

101 raise ValueError("Port this functionality to not use the legacy string.") 

102 

103 geometry_set_dict: _Dict[int, _List[int]] = {} 

104 for line in section_list: 

105 id_geometry_set = int(line.split()[-1]) 

106 index_node = int(line.split()[1]) - 1 

107 if id_geometry_set not in geometry_set_dict: 

108 geometry_set_dict[id_geometry_set] = [] 

109 if append_node_ids: 

110 geometry_set_dict[id_geometry_set].append(index_node) 

111 

112 return geometry_set_dict 

113 

114 

115def _get_yaml_geometry_sets( 

116 nodes: _List[_Node], geometry_key: _conf.Geometry, section_list: _List 

117) -> _Dict[int, _GeometrySetNodes]: 

118 """Add sets of points, lines, surfaces or volumes to the object.""" 

119 

120 # Create the individual geometry sets. The nodes are still integers at this 

121 # point. They have to be converted to links to the actual nodes later on. 

122 geometry_set_dict = _get_geometry_set_indices_from_section(section_list) 

123 geometry_sets_in_this_section = {} 

124 for geometry_set_id, node_ids in geometry_set_dict.items(): 

125 geometry_sets_in_this_section[geometry_set_id] = _GeometrySetNodes( 

126 geometry_key, nodes=[nodes[node_id] for node_id in node_ids] 

127 ) 

128 return geometry_sets_in_this_section 

129 

130 

131class InputFile(_Mesh): 

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

133 

134 # Define the names of sections and boundary conditions in the input file. 

135 geometry_set_names = { 

136 _mpy.geo.point: "DNODE-NODE TOPOLOGY", 

137 _mpy.geo.line: "DLINE-NODE TOPOLOGY", 

138 _mpy.geo.surface: "DSURF-NODE TOPOLOGY", 

139 _mpy.geo.volume: "DVOL-NODE TOPOLOGY", 

140 } 

141 boundary_condition_names = { 

142 (_mpy.bc.dirichlet, _mpy.geo.point): "DESIGN POINT DIRICH CONDITIONS", 

143 (_mpy.bc.dirichlet, _mpy.geo.line): "DESIGN LINE DIRICH CONDITIONS", 

144 (_mpy.bc.dirichlet, _mpy.geo.surface): "DESIGN SURF DIRICH CONDITIONS", 

145 (_mpy.bc.dirichlet, _mpy.geo.volume): "DESIGN VOL DIRICH CONDITIONS", 

146 (_mpy.bc.locsys, _mpy.geo.point): "DESIGN POINT LOCSYS CONDITIONS", 

147 (_mpy.bc.locsys, _mpy.geo.line): "DESIGN LINE LOCSYS CONDITIONS", 

148 (_mpy.bc.locsys, _mpy.geo.surface): "DESIGN SURF LOCSYS CONDITIONS", 

149 (_mpy.bc.locsys, _mpy.geo.volume): "DESIGN VOL LOCSYS CONDITIONS", 

150 (_mpy.bc.neumann, _mpy.geo.point): "DESIGN POINT NEUMANN CONDITIONS", 

151 (_mpy.bc.neumann, _mpy.geo.line): "DESIGN LINE NEUMANN CONDITIONS", 

152 (_mpy.bc.neumann, _mpy.geo.surface): "DESIGN SURF NEUMANN CONDITIONS", 

153 (_mpy.bc.neumann, _mpy.geo.volume): "DESIGN VOL NEUMANN CONDITIONS", 

154 ( 

155 _mpy.bc.moment_euler_bernoulli, 

156 _mpy.geo.point, 

157 ): "DESIGN POINT MOMENT EB CONDITIONS", 

158 ( 

159 _mpy.bc.beam_to_solid_volume_meshtying, 

160 _mpy.geo.line, 

161 ): "BEAM INTERACTION/BEAM TO SOLID VOLUME MESHTYING LINE", 

162 ( 

163 _mpy.bc.beam_to_solid_volume_meshtying, 

164 _mpy.geo.volume, 

165 ): "BEAM INTERACTION/BEAM TO SOLID VOLUME MESHTYING VOLUME", 

166 ( 

167 _mpy.bc.beam_to_solid_surface_meshtying, 

168 _mpy.geo.line, 

169 ): "BEAM INTERACTION/BEAM TO SOLID SURFACE MESHTYING LINE", 

170 ( 

171 _mpy.bc.beam_to_solid_surface_meshtying, 

172 _mpy.geo.surface, 

173 ): "BEAM INTERACTION/BEAM TO SOLID SURFACE MESHTYING SURFACE", 

174 ( 

175 _mpy.bc.beam_to_solid_surface_contact, 

176 _mpy.geo.line, 

177 ): "BEAM INTERACTION/BEAM TO SOLID SURFACE CONTACT LINE", 

178 ( 

179 _mpy.bc.beam_to_solid_surface_contact, 

180 _mpy.geo.surface, 

181 ): "BEAM INTERACTION/BEAM TO SOLID SURFACE CONTACT SURFACE", 

182 (_mpy.bc.point_coupling, _mpy.geo.point): "DESIGN POINT COUPLING CONDITIONS", 

183 ( 

184 _mpy.bc.beam_to_beam_contact, 

185 _mpy.geo.line, 

186 ): "BEAM INTERACTION/BEAM TO BEAM CONTACT CONDITIONS", 

187 ( 

188 _mpy.bc.point_coupling_penalty, 

189 _mpy.geo.point, 

190 ): "DESIGN POINT PENALTY COUPLING CONDITIONS", 

191 ( 

192 "DESIGN SURF MORTAR CONTACT CONDITIONS 3D", 

193 _mpy.geo.surface, 

194 ): "DESIGN SURF MORTAR CONTACT CONDITIONS 3D", 

195 } 

196 

197 def __init__(self, *, yaml_file: _Optional[_Path] = None, cubit=None): 

198 """Initialize the input file. 

199 

200 Args: 

201 yaml_file: 

202 A file path to an existing input file that will be read into this 

203 object. 

204 cubit: 

205 A cubit object, that contains an input file which will be loaded 

206 into this input file. 

207 """ 

208 

209 super().__init__() 

210 

211 # Everything that is not a full MeshPy object is stored here, e.g., parameters 

212 # and imported nodes/elements/materials/... 

213 self.sections: _Dict[str, _Any] = dict() 

214 

215 # Contents of NOX xml file. 

216 self.nox_xml = None 

217 self._nox_xml_file = None 

218 

219 if yaml_file is not None: 

220 self.read_yaml(yaml_file) 

221 

222 # TODO fix once cubit is converted to YAML 

223 # if cubit is not None: 

224 # self._read_dat_lines(cubit.get_dat_lines()) 

225 

226 def read_yaml(self, file_path: _Path): 

227 """Read an existing input file into this object. 

228 

229 Args: 

230 file_path: 

231 A file path to an existing input file that will be read into this 

232 object. 

233 """ 

234 

235 if _fourcipp_is_available(): 

236 raise ValueError("Use fourcipp to parse the yaml file.") 

237 

238 with open(file_path) as stream: 

239 self.sections = _yaml.safe_load(stream) 

240 

241 if _mpy.import_mesh_full: 

242 self.sections_to_mesh() 

243 

244 def sections_to_mesh(self): 

245 """Convert mesh items, e.g., nodes, elements, element sets, node sets, 

246 boundary conditions, materials, ... 

247 

248 Note: In the current implementation we cannibalize the mesh sections in 

249 the self.sections dictionary. This should be reconsidered and be done 

250 in a better way when this function is generalized. 

251 

252 to "true" MeshPy objects. 

253 """ 

254 

255 def _get_section_items(section_name): 

256 """Return the items in a given section. 

257 

258 Since we will add the created MeshPy objects to the mesh, we 

259 delete them from the plain data storage to avoid having 

260 duplicate entries. 

261 """ 

262 if section_name in self.sections: 

263 return_list = self.sections[section_name] 

264 self.sections[section_name] = [] 

265 else: 

266 return_list = [] 

267 return return_list 

268 

269 # Go through all sections that have to be converted to full MeshPy objects 

270 mesh = _Mesh() 

271 

272 # Add nodes 

273 for item in _get_section_items("NODE COORDS"): 

274 mesh.nodes.append(_Node.from_legacy_string(item)) 

275 

276 # Add elements 

277 for item in _get_section_items("STRUCTURE ELEMENTS"): 

278 if _fourcipp_is_available(): 

279 raise ValueError( 

280 "Port this functionality to not use the legacy string." 

281 ) 

282 

283 # Get a list containing the element nodes. 

284 element_nodes = [] 

285 for split_item in item.split()[3:]: 

286 if split_item.isdigit(): 

287 node_id = int(split_item) - 1 

288 element_nodes.append(mesh.nodes[node_id]) 

289 else: 

290 break 

291 else: 

292 raise ValueError( 

293 f'The input line:\n"{item}"\ncould not be converted to a element!' 

294 ) 

295 

296 mesh.elements.append(_Element.from_legacy_string(element_nodes, item)) 

297 

298 # Add geometry sets 

299 geometry_sets_in_sections = {key: None for key in _mpy.geo} 

300 for section_name in self.sections.keys(): 

301 if section_name.endswith("TOPOLOGY"): 

302 section_items = _get_section_items(section_name) 

303 if len(section_items) > 0: 

304 # Get the geometry key for this set 

305 for key, value in self.geometry_set_names.items(): 

306 if value == section_name: 

307 geometry_key = key 

308 break 

309 else: 

310 raise ValueError(f"Could not find the set {section_name}") 

311 geometry_sets_in_section = _get_yaml_geometry_sets( 

312 mesh.nodes, geometry_key, section_items 

313 ) 

314 geometry_sets_in_sections[geometry_key] = geometry_sets_in_section 

315 mesh.geometry_sets[geometry_key] = list( 

316 geometry_sets_in_section.values() 

317 ) 

318 

319 # Add boundary conditions 

320 for ( 

321 bc_key, 

322 geometry_key, 

323 ), section_name in self.boundary_condition_names.items(): 

324 for item in _get_section_items(section_name): 

325 geometry_set_id = item["E"] 

326 geometry_set = geometry_sets_in_sections[geometry_key][geometry_set_id] 

327 mesh.boundary_conditions.append( 

328 (bc_key, geometry_key), 

329 _boundary_condition_from_dict(geometry_set, bc_key, item), 

330 ) 

331 

332 self.add(mesh) 

333 

334 def add(self, *args, **kwargs): 

335 """Add to this object. 

336 

337 If the type is not recognized, the child add method is called. 

338 """ 

339 if len(args) == 1 and isinstance(args[0], dict): 

340 # TODO: We have to check here if the item is not of any of the types we 

341 # use that derive from dict, as they should be added in super().add 

342 if not isinstance(args[0], _ContainerBase) and not isinstance( 

343 args[0], _GeometryName 

344 ): 

345 self.add_section(args[0], **kwargs) 

346 return 

347 

348 super().add(*args, **kwargs) 

349 

350 def add_section(self, section, *, option_overwrite=False): 

351 """Add a section to the object. 

352 

353 If the section name already exists, it is added to that section. 

354 """ 

355 

356 for section_name, section_value in section.items(): 

357 if section_name in self.sections: 

358 section_data = self.sections[section_name] 

359 if isinstance(section_data, list): 

360 section_data.extend(section_value) 

361 else: 

362 for option_key, option_value in section_value.items(): 

363 if option_key in self.sections[section_name]: 

364 if ( 

365 not self.sections[section_name][option_key] 

366 == option_value 

367 ): 

368 if not option_overwrite: 

369 raise KeyError( 

370 f"Key {option_key} with the value {self.sections[section_name][option_key]} already set. You tried to set it to {option_value}" 

371 ) 

372 self.sections[section_name][option_key] = option_value 

373 else: 

374 self.sections[section_name] = section_value 

375 

376 def delete_section(self, section_name): 

377 """Delete a section from the dictionary self.sections.""" 

378 if section_name in self.sections.keys(): 

379 del self.sections[section_name] 

380 else: 

381 raise Warning(f"Section {section_name} does not exist!") 

382 

383 def write_input_file( 

384 self, 

385 file_path: _Path, 

386 *, 

387 nox_xml_file: _Optional[str] = None, 

388 add_header_default: bool = True, 

389 add_footer_application_script: bool = True, 

390 **kwargs, 

391 ): 

392 """Write the input file to disk. 

393 

394 Args: 

395 file_path: 

396 Path to the input file that should be created. 

397 nox_xml_file: 

398 (optional) If this argument is given, the xml file will be created 

399 with this name, in the same directory as the input file. 

400 add_header_default: 

401 Prepend the default MeshPy header comment to the input file. 

402 add_footer_application_script: 

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

404 comment at the end of the input file. 

405 """ 

406 

407 # Check if a xml file needs to be written. 

408 if self.nox_xml is not None: 

409 if nox_xml_file is None: 

410 # Get the name of the xml file. 

411 self._nox_xml_file = ( 

412 _os.path.splitext(_os.path.basename(file_path))[0] + ".xml" 

413 ) 

414 else: 

415 self._nox_xml_file = nox_xml_file 

416 

417 # Write the xml file to the disc. 

418 with open( 

419 _os.path.join(_os.path.dirname(file_path), self._nox_xml_file), "w" 

420 ) as xml_file: 

421 xml_file.write(self.nox_xml) 

422 

423 with open(file_path, "w") as input_file: 

424 # write MeshPy header 

425 if add_header_default: 

426 input_file.writelines( 

427 "# " + line + "\n" for line in _mpy.input_file_meshpy_header 

428 ) 

429 

430 _yaml.dump( 

431 self.get_dict_to_dump(**kwargs), 

432 input_file, 

433 Dumper=_MeshPyDumper, 

434 width=float("inf"), 

435 ) 

436 

437 # Add the application script to the input file. 

438 if add_footer_application_script: 

439 application_path = _Path(_sys.argv[0]).resolve() 

440 application_script_lines = self._get_application_script( 

441 application_path 

442 ) 

443 input_file.writelines(application_script_lines) 

444 

445 def get_dict_to_dump( 

446 self, 

447 *, 

448 add_header_information: bool = True, 

449 check_nox: bool = True, 

450 ): 

451 """Return the dictionary representation of this input file for dumping 

452 to a yaml file. 

453 

454 Args: 

455 add_header_information: 

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

457 Contains creation date, git details of MeshPy, CubitPy and 

458 original application which created the input file if available. 

459 check_nox: 

460 If this is true, an error will be thrown if no nox file is set. 

461 """ 

462 

463 # Perform some checks on the mesh. 

464 if _mpy.check_overlapping_elements: 

465 self.check_overlapping_elements() 

466 

467 # The base dictionary we use here is the one that already exists. 

468 # This one might already contain mesh sections - stored in pure 

469 # data format. 

470 # TODO: Check if the deepcopy makes sense to be optional 

471 yaml_dict = _copy.deepcopy(self.sections) 

472 

473 # Add information header to the input file 

474 if add_header_information: 

475 yaml_dict["TITLE"] = self._get_header() 

476 

477 # Check if a file has to be created for the NOX xml information. 

478 if self.nox_xml is not None: 

479 if self._nox_xml_file is None: 

480 if check_nox: 

481 raise ValueError("NOX xml content is given, but no file defined!") 

482 nox_xml_name = "NOT_DEFINED" 

483 else: 

484 nox_xml_name = self._nox_xml_file 

485 # TODO: Use something like the add_section here 

486 yaml_dict["STRUCT NOX/Status Test"] = {"XML File": nox_xml_name} 

487 

488 def _get_global_start_geometry_set(yaml_dict): 

489 """Get the indices for the first "real" MeshPy geometry sets.""" 

490 

491 start_indices_geometry_set = {} 

492 for geometry_type, section_name in self.geometry_set_names.items(): 

493 max_geometry_set_id = 0 

494 if section_name in yaml_dict: 

495 section_list = yaml_dict[section_name] 

496 if len(section_list) > 0: 

497 geometry_set_dict = _get_geometry_set_indices_from_section( 

498 section_list, append_node_ids=False 

499 ) 

500 max_geometry_set_id = max(geometry_set_dict.keys()) 

501 start_indices_geometry_set[geometry_type] = max_geometry_set_id 

502 return start_indices_geometry_set 

503 

504 def _get_global_start_node(yaml_dict): 

505 """Get the index for the first "real" MeshPy node.""" 

506 

507 if _fourcipp_is_available(): 

508 raise ValueError( 

509 "Port this functionality to not use the legacy format any more" 

510 "TODO: Check if we really want this - should we just assume that the" 

511 "imported nodes are in order and without any 'missing' nodes?" 

512 ) 

513 

514 section_name = "NODE COORDS" 

515 if section_name in yaml_dict: 

516 return len(yaml_dict[section_name]) 

517 else: 

518 return 0 

519 

520 def _get_global_start_element(yaml_dict): 

521 """Get the index for the first "real" MeshPy element.""" 

522 

523 if _fourcipp_is_available(): 

524 raise ValueError( 

525 "Port this functionality to not use the legacy format any more" 

526 "TODO: Check if we really want this - should we just assume that the" 

527 "imported elements are in order and without any 'missing' elements?" 

528 ) 

529 

530 start_index = 0 

531 section_names = ["FLUID ELEMENTS", "STRUCTURE ELEMENTS"] 

532 for section_name in section_names: 

533 if section_name in yaml_dict: 

534 start_index += len(yaml_dict[section_name]) 

535 return start_index 

536 

537 def _get_global_start_material(yaml_dict): 

538 """Get the index for the first "real" MeshPy material. 

539 

540 We have to account for materials imported from yaml files 

541 that have arbitrary numbering. 

542 """ 

543 

544 # Get the maximum material index in materials imported from a yaml file 

545 max_material_id = 0 

546 section_name = "MATERIALS" 

547 if section_name in yaml_dict: 

548 for material in yaml_dict[section_name]: 

549 max_material_id = max(max_material_id, material["MAT"]) 

550 return max_material_id 

551 

552 def _get_global_start_function(yaml_dict): 

553 """Get the index for the first "real" MeshPy function.""" 

554 

555 max_function_id = 0 

556 for section_name in yaml_dict.keys(): 

557 if section_name.startswith("FUNCT"): 

558 max_function_id = max( 

559 max_function_id, int(section_name.split("FUNCT")[-1]) 

560 ) 

561 return max_function_id 

562 

563 def _set_i_global(data_list, *, start_index=0): 

564 """Set i_global in every item of data_list.""" 

565 

566 # A check is performed that every entry in data_list is unique. 

567 if len(data_list) != len(set(data_list)): 

568 raise ValueError("Elements in data_list are not unique!") 

569 

570 # Set the values for i_global. 

571 for i, item in enumerate(data_list): 

572 # TODO make i_global index-0 based 

573 item.i_global = i + 1 + start_index 

574 

575 def _set_i_global_elements(element_list, *, start_index=0): 

576 """Set i_global in every item of element_list.""" 

577 

578 # A check is performed that every entry in element_list is unique. 

579 if len(element_list) != len(set(element_list)): 

580 raise ValueError("Elements in element_list are not unique!") 

581 

582 # Set the values for i_global. 

583 i = start_index 

584 i_nurbs_patch = 0 

585 for item in element_list: 

586 # As a NURBS patch can be defined with more elements, an offset is applied to the 

587 # rest of the items 

588 # TODO make i_global index-0 based 

589 item.i_global = i + 1 

590 if isinstance(item, _NURBSPatch): 

591 item.n_nurbs_patch = i_nurbs_patch + 1 

592 offset = item.get_number_elements() 

593 i += offset 

594 i_nurbs_patch += 1 

595 else: 

596 i += 1 

597 

598 def _dump_mesh_items(yaml_dict, section_name, data_list): 

599 """Output a section name and apply either the default dump or the 

600 specialized the dump_to_list for each list item.""" 

601 

602 # Do not write section if no content is available 

603 if len(data_list) == 0: 

604 return 

605 

606 # Check if section already exists 

607 if section_name not in yaml_dict.keys(): 

608 yaml_dict[section_name] = [] 

609 

610 item_dict_list = yaml_dict[section_name] 

611 for item in data_list: 

612 if hasattr(item, "dump_to_list"): 

613 item_dict_list.extend(item.dump_to_list()) 

614 elif isinstance(item, _BoundaryCondition): 

615 item_dict_list.append( 

616 {"E": item.geometry_set.i_global, **item.data} 

617 ) 

618 else: 

619 raise TypeError(f"Could not dump {item}") 

620 

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

622 self.unlink_nodes() 

623 start_indices_geometry_set = _get_global_start_geometry_set(yaml_dict) 

624 mesh_sets = self.get_unique_geometry_sets( 

625 geometry_set_start_indices=start_indices_geometry_set 

626 ) 

627 

628 # Assign global indices to all entries. 

629 start_index_nodes = _get_global_start_node(yaml_dict) 

630 _set_i_global(self.nodes, start_index=start_index_nodes) 

631 start_index_elements = _get_global_start_element(yaml_dict) 

632 _set_i_global_elements(self.elements, start_index=start_index_elements) 

633 start_index_materials = _get_global_start_material(yaml_dict) 

634 _set_i_global(self.materials, start_index=start_index_materials) 

635 start_index_functions = _get_global_start_function(yaml_dict) 

636 _set_i_global(self.functions, start_index=start_index_functions) 

637 

638 # Add material data to the input file. 

639 _dump_mesh_items(yaml_dict, "MATERIALS", self.materials) 

640 

641 # Add the functions. 

642 for function in self.functions: 

643 yaml_dict[f"FUNCT{function.i_global}"] = function.dump_to_list() 

644 

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

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

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

648 def get_number_of_coupling_conditions(key): 

649 """Return the number of coupling conditions in the mesh.""" 

650 if (key, _mpy.geo.point) in self.boundary_conditions.keys(): 

651 return len(self.boundary_conditions[key, _mpy.geo.point]) 

652 else: 

653 return 0 

654 

655 if ( 

656 get_number_of_coupling_conditions(_mpy.bc.point_coupling) 

657 + get_number_of_coupling_conditions(_mpy.bc.point_coupling_penalty) 

658 > 0 

659 ): 

660 self.set_node_links() 

661 

662 # Add the boundary conditions. 

663 for (bc_key, geom_key), bc_list in self.boundary_conditions.items(): 

664 if len(bc_list) > 0: 

665 section_name = ( 

666 bc_key 

667 if isinstance(bc_key, str) 

668 else self.boundary_condition_names[bc_key, geom_key] 

669 ) 

670 _dump_mesh_items(yaml_dict, section_name, bc_list) 

671 

672 # Add additional element sections, e.g., for NURBS knot vectors. 

673 for element in self.elements: 

674 element.dump_element_specific_section(yaml_dict) 

675 

676 # Add the geometry sets. 

677 for geom_key, item in mesh_sets.items(): 

678 if len(item) > 0: 

679 _dump_mesh_items(yaml_dict, self.geometry_set_names[geom_key], item) 

680 

681 # Add the nodes and elements. 

682 _dump_mesh_items(yaml_dict, "NODE COORDS", self.nodes) 

683 _dump_mesh_items(yaml_dict, "STRUCTURE ELEMENTS", self.elements) 

684 

685 return yaml_dict 

686 

687 def _get_header(self) -> dict: 

688 """Return the information header for the current MeshPy run. 

689 

690 Returns: 

691 A dictionary with the header information. 

692 """ 

693 

694 def _get_git_data(repo_path: _Path) -> _Tuple[_Optional[str], _Optional[str]]: 

695 """Return the hash and date of the current git commit. 

696 

697 Args: 

698 repo_path: Path to the git repository. 

699 Returns: 

700 A tuple with the hash and date of the current git commit 

701 if available, otherwise None. 

702 """ 

703 git = _shutil.which("git") 

704 if git is None: 

705 raise RuntimeError("Git executable not found") 

706 out_sha = _subprocess.run( # nosec B603 

707 [git, "rev-parse", "HEAD"], 

708 cwd=repo_path, 

709 stdout=_subprocess.PIPE, 

710 stderr=_subprocess.DEVNULL, 

711 ) 

712 out_date = _subprocess.run( # nosec B603 

713 [git, "show", "-s", "--format=%ci"], 

714 cwd=repo_path, 

715 stdout=_subprocess.PIPE, 

716 stderr=_subprocess.DEVNULL, 

717 ) 

718 

719 if not out_sha.returncode + out_date.returncode == 0: 

720 return None, None 

721 

722 git_sha = out_sha.stdout.decode("ascii").strip() 

723 git_date = out_date.stdout.decode("ascii").strip() 

724 return git_sha, git_date 

725 

726 header: dict = {"MeshPy": {}} 

727 

728 header["MeshPy"]["creation_date"] = _datetime.now().isoformat( 

729 sep=" ", timespec="seconds" 

730 ) 

731 

732 # application which created the input file 

733 application_path = _Path(_sys.argv[0]).resolve() 

734 header["MeshPy"]["Application"] = {"path": str(application_path)} 

735 

736 application_git_sha, application_git_date = _get_git_data( 

737 application_path.parent 

738 ) 

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

740 header["MeshPy"]["Application"].update( 

741 { 

742 "git_sha": application_git_sha, 

743 "git_date": application_git_date, 

744 } 

745 ) 

746 

747 # MeshPy information 

748 meshpy_git_sha, meshpy_git_date = _get_git_data( 

749 _Path(__file__).resolve().parent 

750 ) 

751 if meshpy_git_sha is not None and meshpy_git_date is not None: 

752 header["MeshPy"]["MeshPy"] = { 

753 "git_SHA": meshpy_git_sha, 

754 "git_date": meshpy_git_date, 

755 } 

756 

757 # CubitPy information 

758 if _cubitpy_is_available(): 

759 cubitpy_git_sha, cubitpy_git_date = _get_git_data( 

760 _os.path.dirname(_cubitpy.__file__) 

761 ) 

762 

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

764 header["MeshPy"]["CubitPy"] = { 

765 "git_SHA": cubitpy_git_sha, 

766 "git_date": cubitpy_git_date, 

767 } 

768 

769 return header 

770 

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

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

773 

774 Args: 

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

776 Returns: 

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

778 """ 

779 

780 application_script_lines = [ 

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

782 ] 

783 

784 with open(application_path) as script_file: 

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

786 

787 return application_script_lines