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
« 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."""
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
39import yaml as _yaml
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
59if _cubitpy_is_available():
60 import cubitpy as _cubitpy
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."""
71 del data["E"]
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!")
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.
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 """
100 if _fourcipp_is_available():
101 raise ValueError("Port this functionality to not use the legacy string.")
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)
112 return geometry_set_dict
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."""
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
131class InputFile(_Mesh):
132 """An item that represents a complete 4C input file."""
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 }
197 def __init__(self, *, yaml_file: _Optional[_Path] = None, cubit=None):
198 """Initialize the input file.
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 """
209 super().__init__()
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()
215 # Contents of NOX xml file.
216 self.nox_xml = None
217 self._nox_xml_file = None
219 if yaml_file is not None:
220 self.read_yaml(yaml_file)
222 # TODO fix once cubit is converted to YAML
223 # if cubit is not None:
224 # self._read_dat_lines(cubit.get_dat_lines())
226 def read_yaml(self, file_path: _Path):
227 """Read an existing input file into this object.
229 Args:
230 file_path:
231 A file path to an existing input file that will be read into this
232 object.
233 """
235 if _fourcipp_is_available():
236 raise ValueError("Use fourcipp to parse the yaml file.")
238 with open(file_path) as stream:
239 self.sections = _yaml.safe_load(stream)
241 if _mpy.import_mesh_full:
242 self.sections_to_mesh()
244 def sections_to_mesh(self):
245 """Convert mesh items, e.g., nodes, elements, element sets, node sets,
246 boundary conditions, materials, ...
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.
252 to "true" MeshPy objects.
253 """
255 def _get_section_items(section_name):
256 """Return the items in a given section.
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
269 # Go through all sections that have to be converted to full MeshPy objects
270 mesh = _Mesh()
272 # Add nodes
273 for item in _get_section_items("NODE COORDS"):
274 mesh.nodes.append(_Node.from_legacy_string(item))
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 )
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 )
296 mesh.elements.append(_Element.from_legacy_string(element_nodes, item))
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 )
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 )
332 self.add(mesh)
334 def add(self, *args, **kwargs):
335 """Add to this object.
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
348 super().add(*args, **kwargs)
350 def add_section(self, section, *, option_overwrite=False):
351 """Add a section to the object.
353 If the section name already exists, it is added to that section.
354 """
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
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!")
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.
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 """
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
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)
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 )
430 _yaml.dump(
431 self.get_dict_to_dump(**kwargs),
432 input_file,
433 Dumper=_MeshPyDumper,
434 width=float("inf"),
435 )
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)
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.
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 """
463 # Perform some checks on the mesh.
464 if _mpy.check_overlapping_elements:
465 self.check_overlapping_elements()
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)
473 # Add information header to the input file
474 if add_header_information:
475 yaml_dict["TITLE"] = self._get_header()
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}
488 def _get_global_start_geometry_set(yaml_dict):
489 """Get the indices for the first "real" MeshPy geometry sets."""
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
504 def _get_global_start_node(yaml_dict):
505 """Get the index for the first "real" MeshPy node."""
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 )
514 section_name = "NODE COORDS"
515 if section_name in yaml_dict:
516 return len(yaml_dict[section_name])
517 else:
518 return 0
520 def _get_global_start_element(yaml_dict):
521 """Get the index for the first "real" MeshPy element."""
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 )
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
537 def _get_global_start_material(yaml_dict):
538 """Get the index for the first "real" MeshPy material.
540 We have to account for materials imported from yaml files
541 that have arbitrary numbering.
542 """
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
552 def _get_global_start_function(yaml_dict):
553 """Get the index for the first "real" MeshPy function."""
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
563 def _set_i_global(data_list, *, start_index=0):
564 """Set i_global in every item of data_list."""
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!")
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
575 def _set_i_global_elements(element_list, *, start_index=0):
576 """Set i_global in every item of element_list."""
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!")
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
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."""
602 # Do not write section if no content is available
603 if len(data_list) == 0:
604 return
606 # Check if section already exists
607 if section_name not in yaml_dict.keys():
608 yaml_dict[section_name] = []
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}")
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 )
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)
638 # Add material data to the input file.
639 _dump_mesh_items(yaml_dict, "MATERIALS", self.materials)
641 # Add the functions.
642 for function in self.functions:
643 yaml_dict[f"FUNCT{function.i_global}"] = function.dump_to_list()
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
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()
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)
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)
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)
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)
685 return yaml_dict
687 def _get_header(self) -> dict:
688 """Return the information header for the current MeshPy run.
690 Returns:
691 A dictionary with the header information.
692 """
694 def _get_git_data(repo_path: _Path) -> _Tuple[_Optional[str], _Optional[str]]:
695 """Return the hash and date of the current git commit.
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 )
719 if not out_sha.returncode + out_date.returncode == 0:
720 return None, None
722 git_sha = out_sha.stdout.decode("ascii").strip()
723 git_date = out_date.stdout.decode("ascii").strip()
724 return git_sha, git_date
726 header: dict = {"MeshPy": {}}
728 header["MeshPy"]["creation_date"] = _datetime.now().isoformat(
729 sep=" ", timespec="seconds"
730 )
732 # application which created the input file
733 application_path = _Path(_sys.argv[0]).resolve()
734 header["MeshPy"]["Application"] = {"path": str(application_path)}
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 )
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 }
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 )
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 }
769 return header
771 def _get_application_script(self, application_path: _Path) -> list[str]:
772 """Get the script that created this input file.
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 """
780 application_script_lines = [
781 "# Application script which created this input file:\n"
782 ]
784 with open(application_path) as script_file:
785 application_script_lines.extend("# " + line for line in script_file)
787 return application_script_lines