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