beamme.four_c.input_file
This module defines the classes that are used to create an input file for 4C.
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 25import os as _os 26import sys as _sys 27from datetime import datetime as _datetime 28from pathlib import Path as _Path 29from typing import Dict as _Dict 30from typing import List as _List 31 32from fourcipp.fourc_input import FourCInput as _FourCInput 33 34from beamme.core.boundary_condition import BoundaryCondition as _BoundaryCondition 35from beamme.core.conf import mpy as _mpy 36from beamme.core.coupling import Coupling as _Coupling 37from beamme.core.function import Function as _Function 38from beamme.core.geometry_set import GeometrySet as _GeometrySet 39from beamme.core.geometry_set import GeometrySetNodes as _GeometrySetNodes 40from beamme.core.material import Material as _Material 41from beamme.core.mesh import Mesh as _Mesh 42from beamme.core.node import Node as _Node 43from beamme.core.nurbs_patch import NURBSPatch as _NURBSPatch 44from beamme.four_c.input_file_mappings import ( 45 INPUT_FILE_MAPPINGS as _INPUT_FILE_MAPPINGS, 46) 47from beamme.utils.environment import cubitpy_is_available as _cubitpy_is_available 48from beamme.utils.environment import get_git_data as _get_git_data 49 50if _cubitpy_is_available(): 51 import cubitpy as _cubitpy 52 53 54def get_geometry_set_indices_from_section( 55 section_list: _List, *, append_node_ids: bool = True 56) -> _Dict: 57 """Return a dictionary with the geometry set ID as keys and the node IDs as 58 values. 59 60 Args: 61 section_list: A list with the legacy strings for the geometry pair 62 append_node_ids: If the node IDs shall be appended, or only the 63 dict with the keys should be returned. 64 """ 65 66 geometry_set_dict: _Dict[int, _List[int]] = {} 67 for entry in section_list: 68 id_geometry_set = entry["d_id"] 69 index_node = entry["node_id"] - 1 70 if id_geometry_set not in geometry_set_dict: 71 geometry_set_dict[id_geometry_set] = [] 72 if append_node_ids: 73 geometry_set_dict[id_geometry_set].append(index_node) 74 75 return geometry_set_dict 76 77 78def _dump_coupling(coupling): 79 """Return the input file representation of the coupling condition.""" 80 81 # TODO: Move this to a better place / gather all dump functions for general 82 # BeamMe items in a file or so. 83 84 if isinstance(coupling.data, dict): 85 data = coupling.data 86 else: 87 # In this case we have to check which beams are connected to the node. 88 # TODO: Coupling also makes sense for different beam types, this can 89 # be implemented at some point. 90 nodes = coupling.geometry_set.get_points() 91 connected_elements = [ 92 element for node in nodes for element in node.element_link 93 ] 94 element_types = {type(element) for element in connected_elements} 95 if len(element_types) > 1: 96 raise TypeError( 97 f"Expected a single connected type of beam elements, got {element_types}" 98 ) 99 element_type = element_types.pop() 100 if element_type.beam_type is _mpy.beam.kirchhoff: 101 rotvec = {element.rotvec for element in connected_elements} 102 if len(rotvec) > 1 or not rotvec.pop(): 103 raise TypeError( 104 "Couplings for Kirchhoff beams and rotvec==False not yet implemented." 105 ) 106 107 data = element_type.get_coupling_dict(coupling.data) 108 109 return {"E": coupling.geometry_set.i_global, **data} 110 111 112class InputFile(_FourCInput): 113 """An item that represents a complete 4C input file.""" 114 115 def __init__(self, sections=None): 116 """Initialize the input file.""" 117 118 super().__init__(sections=sections) 119 120 # Contents of NOX xml file. 121 self.nox_xml_contents = "" 122 123 # Register converters to directly convert non-primitive types 124 # to native Python types via the FourCIPP type converter. 125 self.type_converter.register_numpy_types() 126 self.type_converter.register_type( 127 (_Function, _Material, _Node), lambda converter, obj: obj.i_global 128 ) 129 130 def add(self, object_to_add, **kwargs): 131 """Add a mesh or a dictionary to the input file. 132 133 Args: 134 object: The object to be added. This can be a mesh or a dictionary. 135 **kwargs: Additional arguments to be passed to the add method. 136 """ 137 138 if isinstance(object_to_add, _Mesh): 139 self.add_mesh_to_input_file(mesh=object_to_add, **kwargs) 140 141 else: 142 super().combine_sections(object_to_add) 143 144 def dump( 145 self, 146 input_file_path: str | _Path, 147 *, 148 nox_xml_file: str | None = None, 149 add_header_default: bool = True, 150 add_header_information: bool = True, 151 add_footer_application_script: bool = True, 152 sort_sections=False, 153 validate=True, 154 validate_sections_only: bool = False, 155 ): 156 """Write the input file to disk. 157 158 Args: 159 input_file_path: 160 Path to the input file that should be created. 161 nox_xml_file: 162 If this is a string, the NOX xml file will be created with this 163 name. If this is None, the NOX xml file will be created with the 164 name of the input file with the extension ".nox.xml". 165 add_header_default: 166 Prepend the default header comment to the input file. 167 add_header_information: 168 If the information header should be exported to the input file 169 Contains creation date, git details of BeamMe, CubitPy and 170 original application which created the input file if available. 171 add_footer_application_script: 172 Append the application script which creates the input files as a 173 comment at the end of the input file. 174 sort_sections: 175 Sort sections alphabetically with FourCIPP. 176 validate: 177 Validate if the created input file is compatible with 4C with FourCIPP. 178 validate_sections_only: 179 Validate each section independently. Required sections are no longer 180 required, but the sections must be valid. 181 """ 182 183 # Make sure the given input file is a Path instance. 184 input_file_path = _Path(input_file_path) 185 186 if self.nox_xml_contents: 187 if nox_xml_file is None: 188 nox_xml_file = input_file_path.name.split(".")[0] + ".nox.xml" 189 190 self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file} 191 192 # Write the xml file to the disc. 193 with open(input_file_path.parent / nox_xml_file, "w") as xml_file: 194 xml_file.write(self.nox_xml_contents) 195 196 # Add information header to the input file 197 if add_header_information: 198 self.add({"TITLE": self._get_header()}) 199 200 super().dump( 201 input_file_path=input_file_path, 202 sort_sections=sort_sections, 203 validate=validate, 204 validate_sections_only=validate_sections_only, 205 convert_to_native_types=False, # conversion already happens during add() 206 ) 207 208 if add_header_default or add_footer_application_script: 209 with open(input_file_path, "r") as input_file: 210 lines = input_file.readlines() 211 212 if add_header_default: 213 lines = [ 214 "# " + line + "\n" for line in _mpy.input_file_header 215 ] + lines 216 217 if add_footer_application_script: 218 application_path = _Path(_sys.argv[0]).resolve() 219 lines += self._get_application_script(application_path) 220 221 with open(input_file_path, "w") as input_file: 222 input_file.writelines(lines) 223 224 def add_mesh_to_input_file(self, mesh: _Mesh): 225 """Add a mesh to the input file. 226 227 Args: 228 mesh: The mesh to be added to the input file. 229 """ 230 231 # Perform some checks on the mesh. 232 if _mpy.check_overlapping_elements: 233 mesh.check_overlapping_elements() 234 235 def _get_global_start_geometry_set(dictionary): 236 """Get the indices for the first "real" BeamMe geometry sets.""" 237 238 start_indices_geometry_set = {} 239 for geometry_type, section_name in _INPUT_FILE_MAPPINGS[ 240 "geometry_sets" 241 ].items(): 242 max_geometry_set_id = 0 243 if section_name in dictionary: 244 section_list = dictionary[section_name] 245 if len(section_list) > 0: 246 geometry_set_dict = get_geometry_set_indices_from_section( 247 section_list, append_node_ids=False 248 ) 249 max_geometry_set_id = max(geometry_set_dict.keys()) 250 start_indices_geometry_set[geometry_type] = max_geometry_set_id 251 return start_indices_geometry_set 252 253 def _get_global_start_node(): 254 """Get the index for the first "real" BeamMe node.""" 255 256 return len(self.sections.get("NODE COORDS", [])) 257 258 def _get_global_start_element(): 259 """Get the index for the first "real" BeamMe element.""" 260 261 return sum( 262 len(self.sections.get(section, [])) 263 for section in ["FLUID ELEMENTS", "STRUCTURE ELEMENTS"] 264 ) 265 266 def _get_global_start_material(): 267 """Get the index for the first "real" BeamMe material. 268 269 We have to account for materials imported from yaml files 270 that have arbitrary numbering. 271 """ 272 273 # Get the maximum material index in materials imported from a yaml file 274 max_material_id = 0 275 section_name = "MATERIALS" 276 if section_name in self.sections: 277 for material in self.sections[section_name]: 278 max_material_id = max(max_material_id, material["MAT"]) 279 return max_material_id 280 281 def _get_global_start_function(): 282 """Get the index for the first "real" BeamMe function.""" 283 284 max_function_id = 0 285 for section_name in self.sections.keys(): 286 if section_name.startswith("FUNCT"): 287 max_function_id = max( 288 max_function_id, int(section_name.split("FUNCT")[-1]) 289 ) 290 return max_function_id 291 292 def _set_i_global(data_list, *, start_index=0): 293 """Set i_global in every item of data_list.""" 294 295 # A check is performed that every entry in data_list is unique. 296 if len(data_list) != len(set(data_list)): 297 raise ValueError("Elements in data_list are not unique!") 298 299 # Set the values for i_global. 300 for i, item in enumerate(data_list): 301 # TODO make i_global index-0 based 302 item.i_global = i + 1 + start_index 303 304 def _set_i_global_elements(element_list, *, start_index=0): 305 """Set i_global in every item of element_list.""" 306 307 # A check is performed that every entry in element_list is unique. 308 if len(element_list) != len(set(element_list)): 309 raise ValueError("Elements in element_list are not unique!") 310 311 # Set the values for i_global. 312 i = start_index 313 i_nurbs_patch = 0 314 for item in element_list: 315 # As a NURBS patch can be defined with more elements, an offset is applied to the 316 # rest of the items 317 # TODO make i_global index-0 based 318 item.i_global = i + 1 319 if isinstance(item, _NURBSPatch): 320 item.n_nurbs_patch = i_nurbs_patch + 1 321 offset = item.get_number_elements() 322 i += offset 323 i_nurbs_patch += 1 324 else: 325 i += 1 326 327 def _dump_mesh_items(section_name, data_list): 328 """Output a section name and apply either the default dump or the 329 specialized the dump_to_list for each list item.""" 330 331 # Do not write section if no content is available 332 if len(data_list) == 0: 333 return 334 335 list = [] 336 337 for item in data_list: 338 if ( 339 isinstance(item, _GeometrySet) 340 or isinstance(item, _GeometrySetNodes) 341 or isinstance(item, _NURBSPatch) 342 ): 343 list.extend(item.dump_to_list()) 344 elif hasattr(item, "dump_to_list"): 345 list.append(item.dump_to_list()) 346 elif isinstance(item, _BoundaryCondition): 347 list.append( 348 { 349 "E": item.geometry_set.i_global, 350 **item.data, 351 } 352 ) 353 354 elif isinstance(item, _Coupling): 355 list.append(_dump_coupling(item)) 356 else: 357 raise TypeError(f"Could not dump {item}") 358 359 # If section already exists, retrieve from input file and 360 # add newly. We always need to go through fourcipp to convert 361 # the data types correctly. 362 if section_name in self.sections: 363 existing_entries = self.pop(section_name) 364 existing_entries.extend(list) 365 list = existing_entries 366 367 self.add({section_name: list}) 368 369 # Add sets from couplings and boundary conditions to a temp container. 370 mesh.unlink_nodes() 371 start_indices_geometry_set = _get_global_start_geometry_set(self.sections) 372 mesh_sets = mesh.get_unique_geometry_sets( 373 geometry_set_start_indices=start_indices_geometry_set 374 ) 375 376 # Assign global indices to all entries. 377 start_index_nodes = _get_global_start_node() 378 _set_i_global(mesh.nodes, start_index=start_index_nodes) 379 380 start_index_elements = _get_global_start_element() 381 _set_i_global_elements(mesh.elements, start_index=start_index_elements) 382 383 start_index_materials = _get_global_start_material() 384 _set_i_global(mesh.materials, start_index=start_index_materials) 385 386 start_index_functions = _get_global_start_function() 387 _set_i_global(mesh.functions, start_index=start_index_functions) 388 389 # Add material data to the input file. 390 _dump_mesh_items("MATERIALS", mesh.materials) 391 392 # Add the functions. 393 for function in mesh.functions: 394 self.add({f"FUNCT{function.i_global}": function.data}) 395 396 # If there are couplings in the mesh, set the link between the nodes 397 # and elements, so the couplings can decide which DOFs they couple, 398 # depending on the type of the connected beam element. 399 def get_number_of_coupling_conditions(key): 400 """Return the number of coupling conditions in the mesh.""" 401 if (key, _mpy.geo.point) in mesh.boundary_conditions.keys(): 402 return len(mesh.boundary_conditions[key, _mpy.geo.point]) 403 else: 404 return 0 405 406 if ( 407 get_number_of_coupling_conditions(_mpy.bc.point_coupling) 408 + get_number_of_coupling_conditions(_mpy.bc.point_coupling_penalty) 409 > 0 410 ): 411 mesh.set_node_links() 412 413 # Add the boundary conditions. 414 for (bc_key, geom_key), bc_list in mesh.boundary_conditions.items(): 415 if len(bc_list) > 0: 416 section_name = ( 417 bc_key 418 if isinstance(bc_key, str) 419 else _INPUT_FILE_MAPPINGS["boundary_conditions"][(bc_key, geom_key)] 420 ) 421 _dump_mesh_items(section_name, bc_list) 422 423 # Add additional element sections, e.g., for NURBS knot vectors. 424 for element in mesh.elements: 425 element.dump_element_specific_section(self) 426 427 # Add the geometry sets. 428 for geom_key, item in mesh_sets.items(): 429 if len(item) > 0: 430 _dump_mesh_items(_INPUT_FILE_MAPPINGS["geometry_sets"][geom_key], item) 431 432 # Add the nodes and elements. 433 _dump_mesh_items("NODE COORDS", mesh.nodes) 434 _dump_mesh_items("STRUCTURE ELEMENTS", mesh.elements) 435 # TODO: reset all links and counters set in this method. 436 437 def _get_header(self) -> dict: 438 """Return the information header for the current BeamMe run. 439 440 Returns: 441 A dictionary with the header information. 442 """ 443 444 header: dict = {"BeamMe": {}} 445 446 header["BeamMe"]["creation_date"] = _datetime.now().isoformat( 447 sep=" ", timespec="seconds" 448 ) 449 450 # application which created the input file 451 application_path = _Path(_sys.argv[0]).resolve() 452 header["BeamMe"]["Application"] = {"path": str(application_path)} 453 454 application_git_sha, application_git_date = _get_git_data( 455 application_path.parent 456 ) 457 if application_git_sha is not None and application_git_date is not None: 458 header["BeamMe"]["Application"].update( 459 { 460 "git_sha": application_git_sha, 461 "git_date": application_git_date, 462 } 463 ) 464 465 # BeamMe information 466 beamme_git_sha, beamme_git_date = _get_git_data( 467 _Path(__file__).resolve().parent 468 ) 469 if beamme_git_sha is not None and beamme_git_date is not None: 470 header["BeamMe"]["BeamMe"] = { 471 "git_SHA": beamme_git_sha, 472 "git_date": beamme_git_date, 473 } 474 475 # CubitPy information 476 if _cubitpy_is_available(): 477 cubitpy_git_sha, cubitpy_git_date = _get_git_data( 478 _os.path.dirname(_cubitpy.__file__) 479 ) 480 481 if cubitpy_git_sha is not None and cubitpy_git_date is not None: 482 header["BeamMe"]["CubitPy"] = { 483 "git_SHA": cubitpy_git_sha, 484 "git_date": cubitpy_git_date, 485 } 486 487 return header 488 489 def _get_application_script(self, application_path: _Path) -> list[str]: 490 """Get the script that created this input file. 491 492 Args: 493 application_path: Path to the script that created this input file. 494 Returns: 495 A list of strings with the script that created this input file. 496 """ 497 498 application_script_lines = [ 499 "# Application script which created this input file:\n" 500 ] 501 502 with open(application_path) as script_file: 503 application_script_lines.extend("# " + line for line in script_file) 504 505 return application_script_lines
def
get_geometry_set_indices_from_section(section_list: List, *, append_node_ids: bool = True) -> Dict:
55def get_geometry_set_indices_from_section( 56 section_list: _List, *, append_node_ids: bool = True 57) -> _Dict: 58 """Return a dictionary with the geometry set ID as keys and the node IDs as 59 values. 60 61 Args: 62 section_list: A list with the legacy strings for the geometry pair 63 append_node_ids: If the node IDs shall be appended, or only the 64 dict with the keys should be returned. 65 """ 66 67 geometry_set_dict: _Dict[int, _List[int]] = {} 68 for entry in section_list: 69 id_geometry_set = entry["d_id"] 70 index_node = entry["node_id"] - 1 71 if id_geometry_set not in geometry_set_dict: 72 geometry_set_dict[id_geometry_set] = [] 73 if append_node_ids: 74 geometry_set_dict[id_geometry_set].append(index_node) 75 76 return geometry_set_dict
Return a dictionary with the geometry set ID as keys and the node IDs as values.
Arguments:
- section_list: A list with the legacy strings for the geometry pair
- append_node_ids: If the node IDs shall be appended, or only the dict with the keys should be returned.
class
InputFile(fourcipp.fourc_input.FourCInput):
113class InputFile(_FourCInput): 114 """An item that represents a complete 4C input file.""" 115 116 def __init__(self, sections=None): 117 """Initialize the input file.""" 118 119 super().__init__(sections=sections) 120 121 # Contents of NOX xml file. 122 self.nox_xml_contents = "" 123 124 # Register converters to directly convert non-primitive types 125 # to native Python types via the FourCIPP type converter. 126 self.type_converter.register_numpy_types() 127 self.type_converter.register_type( 128 (_Function, _Material, _Node), lambda converter, obj: obj.i_global 129 ) 130 131 def add(self, object_to_add, **kwargs): 132 """Add a mesh or a dictionary to the input file. 133 134 Args: 135 object: The object to be added. This can be a mesh or a dictionary. 136 **kwargs: Additional arguments to be passed to the add method. 137 """ 138 139 if isinstance(object_to_add, _Mesh): 140 self.add_mesh_to_input_file(mesh=object_to_add, **kwargs) 141 142 else: 143 super().combine_sections(object_to_add) 144 145 def dump( 146 self, 147 input_file_path: str | _Path, 148 *, 149 nox_xml_file: str | None = None, 150 add_header_default: bool = True, 151 add_header_information: bool = True, 152 add_footer_application_script: bool = True, 153 sort_sections=False, 154 validate=True, 155 validate_sections_only: bool = False, 156 ): 157 """Write the input file to disk. 158 159 Args: 160 input_file_path: 161 Path to the input file that should be created. 162 nox_xml_file: 163 If this is a string, the NOX xml file will be created with this 164 name. If this is None, the NOX xml file will be created with the 165 name of the input file with the extension ".nox.xml". 166 add_header_default: 167 Prepend the default header comment to the input file. 168 add_header_information: 169 If the information header should be exported to the input file 170 Contains creation date, git details of BeamMe, CubitPy and 171 original application which created the input file if available. 172 add_footer_application_script: 173 Append the application script which creates the input files as a 174 comment at the end of the input file. 175 sort_sections: 176 Sort sections alphabetically with FourCIPP. 177 validate: 178 Validate if the created input file is compatible with 4C with FourCIPP. 179 validate_sections_only: 180 Validate each section independently. Required sections are no longer 181 required, but the sections must be valid. 182 """ 183 184 # Make sure the given input file is a Path instance. 185 input_file_path = _Path(input_file_path) 186 187 if self.nox_xml_contents: 188 if nox_xml_file is None: 189 nox_xml_file = input_file_path.name.split(".")[0] + ".nox.xml" 190 191 self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file} 192 193 # Write the xml file to the disc. 194 with open(input_file_path.parent / nox_xml_file, "w") as xml_file: 195 xml_file.write(self.nox_xml_contents) 196 197 # Add information header to the input file 198 if add_header_information: 199 self.add({"TITLE": self._get_header()}) 200 201 super().dump( 202 input_file_path=input_file_path, 203 sort_sections=sort_sections, 204 validate=validate, 205 validate_sections_only=validate_sections_only, 206 convert_to_native_types=False, # conversion already happens during add() 207 ) 208 209 if add_header_default or add_footer_application_script: 210 with open(input_file_path, "r") as input_file: 211 lines = input_file.readlines() 212 213 if add_header_default: 214 lines = [ 215 "# " + line + "\n" for line in _mpy.input_file_header 216 ] + lines 217 218 if add_footer_application_script: 219 application_path = _Path(_sys.argv[0]).resolve() 220 lines += self._get_application_script(application_path) 221 222 with open(input_file_path, "w") as input_file: 223 input_file.writelines(lines) 224 225 def add_mesh_to_input_file(self, mesh: _Mesh): 226 """Add a mesh to the input file. 227 228 Args: 229 mesh: The mesh to be added to the input file. 230 """ 231 232 # Perform some checks on the mesh. 233 if _mpy.check_overlapping_elements: 234 mesh.check_overlapping_elements() 235 236 def _get_global_start_geometry_set(dictionary): 237 """Get the indices for the first "real" BeamMe geometry sets.""" 238 239 start_indices_geometry_set = {} 240 for geometry_type, section_name in _INPUT_FILE_MAPPINGS[ 241 "geometry_sets" 242 ].items(): 243 max_geometry_set_id = 0 244 if section_name in dictionary: 245 section_list = dictionary[section_name] 246 if len(section_list) > 0: 247 geometry_set_dict = get_geometry_set_indices_from_section( 248 section_list, append_node_ids=False 249 ) 250 max_geometry_set_id = max(geometry_set_dict.keys()) 251 start_indices_geometry_set[geometry_type] = max_geometry_set_id 252 return start_indices_geometry_set 253 254 def _get_global_start_node(): 255 """Get the index for the first "real" BeamMe node.""" 256 257 return len(self.sections.get("NODE COORDS", [])) 258 259 def _get_global_start_element(): 260 """Get the index for the first "real" BeamMe element.""" 261 262 return sum( 263 len(self.sections.get(section, [])) 264 for section in ["FLUID ELEMENTS", "STRUCTURE ELEMENTS"] 265 ) 266 267 def _get_global_start_material(): 268 """Get the index for the first "real" BeamMe material. 269 270 We have to account for materials imported from yaml files 271 that have arbitrary numbering. 272 """ 273 274 # Get the maximum material index in materials imported from a yaml file 275 max_material_id = 0 276 section_name = "MATERIALS" 277 if section_name in self.sections: 278 for material in self.sections[section_name]: 279 max_material_id = max(max_material_id, material["MAT"]) 280 return max_material_id 281 282 def _get_global_start_function(): 283 """Get the index for the first "real" BeamMe function.""" 284 285 max_function_id = 0 286 for section_name in self.sections.keys(): 287 if section_name.startswith("FUNCT"): 288 max_function_id = max( 289 max_function_id, int(section_name.split("FUNCT")[-1]) 290 ) 291 return max_function_id 292 293 def _set_i_global(data_list, *, start_index=0): 294 """Set i_global in every item of data_list.""" 295 296 # A check is performed that every entry in data_list is unique. 297 if len(data_list) != len(set(data_list)): 298 raise ValueError("Elements in data_list are not unique!") 299 300 # Set the values for i_global. 301 for i, item in enumerate(data_list): 302 # TODO make i_global index-0 based 303 item.i_global = i + 1 + start_index 304 305 def _set_i_global_elements(element_list, *, start_index=0): 306 """Set i_global in every item of element_list.""" 307 308 # A check is performed that every entry in element_list is unique. 309 if len(element_list) != len(set(element_list)): 310 raise ValueError("Elements in element_list are not unique!") 311 312 # Set the values for i_global. 313 i = start_index 314 i_nurbs_patch = 0 315 for item in element_list: 316 # As a NURBS patch can be defined with more elements, an offset is applied to the 317 # rest of the items 318 # TODO make i_global index-0 based 319 item.i_global = i + 1 320 if isinstance(item, _NURBSPatch): 321 item.n_nurbs_patch = i_nurbs_patch + 1 322 offset = item.get_number_elements() 323 i += offset 324 i_nurbs_patch += 1 325 else: 326 i += 1 327 328 def _dump_mesh_items(section_name, data_list): 329 """Output a section name and apply either the default dump or the 330 specialized the dump_to_list for each list item.""" 331 332 # Do not write section if no content is available 333 if len(data_list) == 0: 334 return 335 336 list = [] 337 338 for item in data_list: 339 if ( 340 isinstance(item, _GeometrySet) 341 or isinstance(item, _GeometrySetNodes) 342 or isinstance(item, _NURBSPatch) 343 ): 344 list.extend(item.dump_to_list()) 345 elif hasattr(item, "dump_to_list"): 346 list.append(item.dump_to_list()) 347 elif isinstance(item, _BoundaryCondition): 348 list.append( 349 { 350 "E": item.geometry_set.i_global, 351 **item.data, 352 } 353 ) 354 355 elif isinstance(item, _Coupling): 356 list.append(_dump_coupling(item)) 357 else: 358 raise TypeError(f"Could not dump {item}") 359 360 # If section already exists, retrieve from input file and 361 # add newly. We always need to go through fourcipp to convert 362 # the data types correctly. 363 if section_name in self.sections: 364 existing_entries = self.pop(section_name) 365 existing_entries.extend(list) 366 list = existing_entries 367 368 self.add({section_name: list}) 369 370 # Add sets from couplings and boundary conditions to a temp container. 371 mesh.unlink_nodes() 372 start_indices_geometry_set = _get_global_start_geometry_set(self.sections) 373 mesh_sets = mesh.get_unique_geometry_sets( 374 geometry_set_start_indices=start_indices_geometry_set 375 ) 376 377 # Assign global indices to all entries. 378 start_index_nodes = _get_global_start_node() 379 _set_i_global(mesh.nodes, start_index=start_index_nodes) 380 381 start_index_elements = _get_global_start_element() 382 _set_i_global_elements(mesh.elements, start_index=start_index_elements) 383 384 start_index_materials = _get_global_start_material() 385 _set_i_global(mesh.materials, start_index=start_index_materials) 386 387 start_index_functions = _get_global_start_function() 388 _set_i_global(mesh.functions, start_index=start_index_functions) 389 390 # Add material data to the input file. 391 _dump_mesh_items("MATERIALS", mesh.materials) 392 393 # Add the functions. 394 for function in mesh.functions: 395 self.add({f"FUNCT{function.i_global}": function.data}) 396 397 # If there are couplings in the mesh, set the link between the nodes 398 # and elements, so the couplings can decide which DOFs they couple, 399 # depending on the type of the connected beam element. 400 def get_number_of_coupling_conditions(key): 401 """Return the number of coupling conditions in the mesh.""" 402 if (key, _mpy.geo.point) in mesh.boundary_conditions.keys(): 403 return len(mesh.boundary_conditions[key, _mpy.geo.point]) 404 else: 405 return 0 406 407 if ( 408 get_number_of_coupling_conditions(_mpy.bc.point_coupling) 409 + get_number_of_coupling_conditions(_mpy.bc.point_coupling_penalty) 410 > 0 411 ): 412 mesh.set_node_links() 413 414 # Add the boundary conditions. 415 for (bc_key, geom_key), bc_list in mesh.boundary_conditions.items(): 416 if len(bc_list) > 0: 417 section_name = ( 418 bc_key 419 if isinstance(bc_key, str) 420 else _INPUT_FILE_MAPPINGS["boundary_conditions"][(bc_key, geom_key)] 421 ) 422 _dump_mesh_items(section_name, bc_list) 423 424 # Add additional element sections, e.g., for NURBS knot vectors. 425 for element in mesh.elements: 426 element.dump_element_specific_section(self) 427 428 # Add the geometry sets. 429 for geom_key, item in mesh_sets.items(): 430 if len(item) > 0: 431 _dump_mesh_items(_INPUT_FILE_MAPPINGS["geometry_sets"][geom_key], item) 432 433 # Add the nodes and elements. 434 _dump_mesh_items("NODE COORDS", mesh.nodes) 435 _dump_mesh_items("STRUCTURE ELEMENTS", mesh.elements) 436 # TODO: reset all links and counters set in this method. 437 438 def _get_header(self) -> dict: 439 """Return the information header for the current BeamMe run. 440 441 Returns: 442 A dictionary with the header information. 443 """ 444 445 header: dict = {"BeamMe": {}} 446 447 header["BeamMe"]["creation_date"] = _datetime.now().isoformat( 448 sep=" ", timespec="seconds" 449 ) 450 451 # application which created the input file 452 application_path = _Path(_sys.argv[0]).resolve() 453 header["BeamMe"]["Application"] = {"path": str(application_path)} 454 455 application_git_sha, application_git_date = _get_git_data( 456 application_path.parent 457 ) 458 if application_git_sha is not None and application_git_date is not None: 459 header["BeamMe"]["Application"].update( 460 { 461 "git_sha": application_git_sha, 462 "git_date": application_git_date, 463 } 464 ) 465 466 # BeamMe information 467 beamme_git_sha, beamme_git_date = _get_git_data( 468 _Path(__file__).resolve().parent 469 ) 470 if beamme_git_sha is not None and beamme_git_date is not None: 471 header["BeamMe"]["BeamMe"] = { 472 "git_SHA": beamme_git_sha, 473 "git_date": beamme_git_date, 474 } 475 476 # CubitPy information 477 if _cubitpy_is_available(): 478 cubitpy_git_sha, cubitpy_git_date = _get_git_data( 479 _os.path.dirname(_cubitpy.__file__) 480 ) 481 482 if cubitpy_git_sha is not None and cubitpy_git_date is not None: 483 header["BeamMe"]["CubitPy"] = { 484 "git_SHA": cubitpy_git_sha, 485 "git_date": cubitpy_git_date, 486 } 487 488 return header 489 490 def _get_application_script(self, application_path: _Path) -> list[str]: 491 """Get the script that created this input file. 492 493 Args: 494 application_path: Path to the script that created this input file. 495 Returns: 496 A list of strings with the script that created this input file. 497 """ 498 499 application_script_lines = [ 500 "# Application script which created this input file:\n" 501 ] 502 503 with open(application_path) as script_file: 504 application_script_lines.extend("# " + line for line in script_file) 505 506 return application_script_lines
An item that represents a complete 4C input file.
InputFile(sections=None)
116 def __init__(self, sections=None): 117 """Initialize the input file.""" 118 119 super().__init__(sections=sections) 120 121 # Contents of NOX xml file. 122 self.nox_xml_contents = "" 123 124 # Register converters to directly convert non-primitive types 125 # to native Python types via the FourCIPP type converter. 126 self.type_converter.register_numpy_types() 127 self.type_converter.register_type( 128 (_Function, _Material, _Node), lambda converter, obj: obj.i_global 129 )
Initialize the input file.
def
add(self, object_to_add, **kwargs):
131 def add(self, object_to_add, **kwargs): 132 """Add a mesh or a dictionary to the input file. 133 134 Args: 135 object: The object to be added. This can be a mesh or a dictionary. 136 **kwargs: Additional arguments to be passed to the add method. 137 """ 138 139 if isinstance(object_to_add, _Mesh): 140 self.add_mesh_to_input_file(mesh=object_to_add, **kwargs) 141 142 else: 143 super().combine_sections(object_to_add)
Add a mesh or a dictionary to the input file.
Arguments:
- object: The object to be added. This can be a mesh or a dictionary.
- **kwargs: Additional arguments to be passed to the add method.
def
dump( self, input_file_path: str | pathlib.Path, *, nox_xml_file: str | None = None, add_header_default: bool = True, add_header_information: bool = True, add_footer_application_script: bool = True, sort_sections=False, validate=True, validate_sections_only: bool = False):
145 def dump( 146 self, 147 input_file_path: str | _Path, 148 *, 149 nox_xml_file: str | None = None, 150 add_header_default: bool = True, 151 add_header_information: bool = True, 152 add_footer_application_script: bool = True, 153 sort_sections=False, 154 validate=True, 155 validate_sections_only: bool = False, 156 ): 157 """Write the input file to disk. 158 159 Args: 160 input_file_path: 161 Path to the input file that should be created. 162 nox_xml_file: 163 If this is a string, the NOX xml file will be created with this 164 name. If this is None, the NOX xml file will be created with the 165 name of the input file with the extension ".nox.xml". 166 add_header_default: 167 Prepend the default header comment to the input file. 168 add_header_information: 169 If the information header should be exported to the input file 170 Contains creation date, git details of BeamMe, CubitPy and 171 original application which created the input file if available. 172 add_footer_application_script: 173 Append the application script which creates the input files as a 174 comment at the end of the input file. 175 sort_sections: 176 Sort sections alphabetically with FourCIPP. 177 validate: 178 Validate if the created input file is compatible with 4C with FourCIPP. 179 validate_sections_only: 180 Validate each section independently. Required sections are no longer 181 required, but the sections must be valid. 182 """ 183 184 # Make sure the given input file is a Path instance. 185 input_file_path = _Path(input_file_path) 186 187 if self.nox_xml_contents: 188 if nox_xml_file is None: 189 nox_xml_file = input_file_path.name.split(".")[0] + ".nox.xml" 190 191 self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file} 192 193 # Write the xml file to the disc. 194 with open(input_file_path.parent / nox_xml_file, "w") as xml_file: 195 xml_file.write(self.nox_xml_contents) 196 197 # Add information header to the input file 198 if add_header_information: 199 self.add({"TITLE": self._get_header()}) 200 201 super().dump( 202 input_file_path=input_file_path, 203 sort_sections=sort_sections, 204 validate=validate, 205 validate_sections_only=validate_sections_only, 206 convert_to_native_types=False, # conversion already happens during add() 207 ) 208 209 if add_header_default or add_footer_application_script: 210 with open(input_file_path, "r") as input_file: 211 lines = input_file.readlines() 212 213 if add_header_default: 214 lines = [ 215 "# " + line + "\n" for line in _mpy.input_file_header 216 ] + lines 217 218 if add_footer_application_script: 219 application_path = _Path(_sys.argv[0]).resolve() 220 lines += self._get_application_script(application_path) 221 222 with open(input_file_path, "w") as input_file: 223 input_file.writelines(lines)
Write the input file to disk.
Arguments:
- input_file_path: Path to the input file that should be created.
- nox_xml_file: If this is a string, the NOX xml file will be created with this name. If this is None, the NOX xml file will be created with the name of the input file with the extension ".nox.xml".
- add_header_default: Prepend the default header comment to the input file.
- add_header_information: If the information header should be exported to the input file Contains creation date, git details of BeamMe, CubitPy and original application which created the input file if available.
- add_footer_application_script: Append the application script which creates the input files as a comment at the end of the input file.
- sort_sections: Sort sections alphabetically with FourCIPP.
- validate: Validate if the created input file is compatible with 4C with FourCIPP.
- validate_sections_only: Validate each section independently. Required sections are no longer required, but the sections must be valid.
225 def add_mesh_to_input_file(self, mesh: _Mesh): 226 """Add a mesh to the input file. 227 228 Args: 229 mesh: The mesh to be added to the input file. 230 """ 231 232 # Perform some checks on the mesh. 233 if _mpy.check_overlapping_elements: 234 mesh.check_overlapping_elements() 235 236 def _get_global_start_geometry_set(dictionary): 237 """Get the indices for the first "real" BeamMe geometry sets.""" 238 239 start_indices_geometry_set = {} 240 for geometry_type, section_name in _INPUT_FILE_MAPPINGS[ 241 "geometry_sets" 242 ].items(): 243 max_geometry_set_id = 0 244 if section_name in dictionary: 245 section_list = dictionary[section_name] 246 if len(section_list) > 0: 247 geometry_set_dict = get_geometry_set_indices_from_section( 248 section_list, append_node_ids=False 249 ) 250 max_geometry_set_id = max(geometry_set_dict.keys()) 251 start_indices_geometry_set[geometry_type] = max_geometry_set_id 252 return start_indices_geometry_set 253 254 def _get_global_start_node(): 255 """Get the index for the first "real" BeamMe node.""" 256 257 return len(self.sections.get("NODE COORDS", [])) 258 259 def _get_global_start_element(): 260 """Get the index for the first "real" BeamMe element.""" 261 262 return sum( 263 len(self.sections.get(section, [])) 264 for section in ["FLUID ELEMENTS", "STRUCTURE ELEMENTS"] 265 ) 266 267 def _get_global_start_material(): 268 """Get the index for the first "real" BeamMe material. 269 270 We have to account for materials imported from yaml files 271 that have arbitrary numbering. 272 """ 273 274 # Get the maximum material index in materials imported from a yaml file 275 max_material_id = 0 276 section_name = "MATERIALS" 277 if section_name in self.sections: 278 for material in self.sections[section_name]: 279 max_material_id = max(max_material_id, material["MAT"]) 280 return max_material_id 281 282 def _get_global_start_function(): 283 """Get the index for the first "real" BeamMe function.""" 284 285 max_function_id = 0 286 for section_name in self.sections.keys(): 287 if section_name.startswith("FUNCT"): 288 max_function_id = max( 289 max_function_id, int(section_name.split("FUNCT")[-1]) 290 ) 291 return max_function_id 292 293 def _set_i_global(data_list, *, start_index=0): 294 """Set i_global in every item of data_list.""" 295 296 # A check is performed that every entry in data_list is unique. 297 if len(data_list) != len(set(data_list)): 298 raise ValueError("Elements in data_list are not unique!") 299 300 # Set the values for i_global. 301 for i, item in enumerate(data_list): 302 # TODO make i_global index-0 based 303 item.i_global = i + 1 + start_index 304 305 def _set_i_global_elements(element_list, *, start_index=0): 306 """Set i_global in every item of element_list.""" 307 308 # A check is performed that every entry in element_list is unique. 309 if len(element_list) != len(set(element_list)): 310 raise ValueError("Elements in element_list are not unique!") 311 312 # Set the values for i_global. 313 i = start_index 314 i_nurbs_patch = 0 315 for item in element_list: 316 # As a NURBS patch can be defined with more elements, an offset is applied to the 317 # rest of the items 318 # TODO make i_global index-0 based 319 item.i_global = i + 1 320 if isinstance(item, _NURBSPatch): 321 item.n_nurbs_patch = i_nurbs_patch + 1 322 offset = item.get_number_elements() 323 i += offset 324 i_nurbs_patch += 1 325 else: 326 i += 1 327 328 def _dump_mesh_items(section_name, data_list): 329 """Output a section name and apply either the default dump or the 330 specialized the dump_to_list for each list item.""" 331 332 # Do not write section if no content is available 333 if len(data_list) == 0: 334 return 335 336 list = [] 337 338 for item in data_list: 339 if ( 340 isinstance(item, _GeometrySet) 341 or isinstance(item, _GeometrySetNodes) 342 or isinstance(item, _NURBSPatch) 343 ): 344 list.extend(item.dump_to_list()) 345 elif hasattr(item, "dump_to_list"): 346 list.append(item.dump_to_list()) 347 elif isinstance(item, _BoundaryCondition): 348 list.append( 349 { 350 "E": item.geometry_set.i_global, 351 **item.data, 352 } 353 ) 354 355 elif isinstance(item, _Coupling): 356 list.append(_dump_coupling(item)) 357 else: 358 raise TypeError(f"Could not dump {item}") 359 360 # If section already exists, retrieve from input file and 361 # add newly. We always need to go through fourcipp to convert 362 # the data types correctly. 363 if section_name in self.sections: 364 existing_entries = self.pop(section_name) 365 existing_entries.extend(list) 366 list = existing_entries 367 368 self.add({section_name: list}) 369 370 # Add sets from couplings and boundary conditions to a temp container. 371 mesh.unlink_nodes() 372 start_indices_geometry_set = _get_global_start_geometry_set(self.sections) 373 mesh_sets = mesh.get_unique_geometry_sets( 374 geometry_set_start_indices=start_indices_geometry_set 375 ) 376 377 # Assign global indices to all entries. 378 start_index_nodes = _get_global_start_node() 379 _set_i_global(mesh.nodes, start_index=start_index_nodes) 380 381 start_index_elements = _get_global_start_element() 382 _set_i_global_elements(mesh.elements, start_index=start_index_elements) 383 384 start_index_materials = _get_global_start_material() 385 _set_i_global(mesh.materials, start_index=start_index_materials) 386 387 start_index_functions = _get_global_start_function() 388 _set_i_global(mesh.functions, start_index=start_index_functions) 389 390 # Add material data to the input file. 391 _dump_mesh_items("MATERIALS", mesh.materials) 392 393 # Add the functions. 394 for function in mesh.functions: 395 self.add({f"FUNCT{function.i_global}": function.data}) 396 397 # If there are couplings in the mesh, set the link between the nodes 398 # and elements, so the couplings can decide which DOFs they couple, 399 # depending on the type of the connected beam element. 400 def get_number_of_coupling_conditions(key): 401 """Return the number of coupling conditions in the mesh.""" 402 if (key, _mpy.geo.point) in mesh.boundary_conditions.keys(): 403 return len(mesh.boundary_conditions[key, _mpy.geo.point]) 404 else: 405 return 0 406 407 if ( 408 get_number_of_coupling_conditions(_mpy.bc.point_coupling) 409 + get_number_of_coupling_conditions(_mpy.bc.point_coupling_penalty) 410 > 0 411 ): 412 mesh.set_node_links() 413 414 # Add the boundary conditions. 415 for (bc_key, geom_key), bc_list in mesh.boundary_conditions.items(): 416 if len(bc_list) > 0: 417 section_name = ( 418 bc_key 419 if isinstance(bc_key, str) 420 else _INPUT_FILE_MAPPINGS["boundary_conditions"][(bc_key, geom_key)] 421 ) 422 _dump_mesh_items(section_name, bc_list) 423 424 # Add additional element sections, e.g., for NURBS knot vectors. 425 for element in mesh.elements: 426 element.dump_element_specific_section(self) 427 428 # Add the geometry sets. 429 for geom_key, item in mesh_sets.items(): 430 if len(item) > 0: 431 _dump_mesh_items(_INPUT_FILE_MAPPINGS["geometry_sets"][geom_key], item) 432 433 # Add the nodes and elements. 434 _dump_mesh_items("NODE COORDS", mesh.nodes) 435 _dump_mesh_items("STRUCTURE ELEMENTS", mesh.elements) 436 # TODO: reset all links and counters set in this method.
Add a mesh to the input file.
Arguments:
- mesh: The mesh to be added to the input file.