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-2026 BeamMe Authors 4# 5# Permission is hereby granted, free of charge, to any person obtaining a copy 6# of this software and associated documentation files (the "Software"), to deal 7# in the Software without restriction, including without limitation the rights 8# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9# copies of the Software, and to permit persons to whom the Software is 10# furnished to do so, subject to the following conditions: 11# 12# The above copyright notice and this permission notice shall be included in 13# all copies or substantial portions of the Software. 14# 15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21# THE SOFTWARE. 22"""This module defines the classes that are used to create an input file for 234C.""" 24 25from __future__ import annotations as _annotations 26 27import os as _os 28from datetime import datetime as _datetime 29from pathlib import Path as _Path 30from typing import Any as _Any 31from typing import Callable as _Callable 32from typing import List as _List 33 34from fourcipp.fourc_input import FourCInput as _FourCInput 35from fourcipp.fourc_input import sort_by_section_names as _sort_by_section_names 36from fourcipp.utils.not_set import NOT_SET as _NOT_SET 37 38from beamme.core.conf import INPUT_FILE_HEADER as _INPUT_FILE_HEADER 39from beamme.core.conf import bme as _bme 40from beamme.core.function import Function as _Function 41from beamme.core.material import Material as _Material 42from beamme.core.mesh import Mesh as _Mesh 43from beamme.core.node import Node as _Node 44from beamme.core.nurbs_patch import NURBSPatch as _NURBSPatch 45from beamme.four_c.input_file_dump_item import dump_item_to_list as _dump_item_to_list 46from beamme.four_c.input_file_dump_item import ( 47 dump_item_to_section as _dump_item_to_section, 48) 49from beamme.four_c.input_file_mappings import ( 50 INPUT_FILE_MAPPINGS as _INPUT_FILE_MAPPINGS, 51) 52from beamme.four_c.material import ( 53 get_all_contained_materials as _get_all_contained_materials, 54) 55from beamme.utils.environment import cubitpy_is_available as _cubitpy_is_available 56from beamme.utils.environment import get_application_path as _get_application_path 57from beamme.utils.environment import get_git_data as _get_git_data 58 59if _cubitpy_is_available(): 60 import cubitpy as _cubitpy 61 62 63class InputFile: 64 """An item that represents a complete 4C input file.""" 65 66 def __init__(self): 67 """Initialize the input file.""" 68 69 self.fourc_input = _FourCInput() 70 71 # Contents of NOX xml file. 72 self.nox_xml_contents = "" 73 74 # Register converters to directly convert non-primitive types 75 # to native Python types via the FourCIPP type converter. 76 self.fourc_input.type_converter.register_numpy_types() 77 self.fourc_input.type_converter.register_type( 78 (_Function, _Material, _Node), lambda converter, obj: obj.i_global + 1 79 ) 80 81 def __contains__(self, key: str) -> bool: 82 """Contains function. 83 84 Allows to use the `in` operator. 85 86 Args: 87 key: Section name to check if it is set 88 89 Returns: 90 True if section is set 91 """ 92 93 return key in self.fourc_input 94 95 def __setitem__(self, key: str, value: _Any) -> None: 96 """Set section. 97 98 Args: 99 key: Section name 100 value: Section entry 101 """ 102 103 self.fourc_input[key] = value 104 105 def __getitem__(self, key: str) -> _Any: 106 """Get section of input file. 107 108 Allows to use the indexing operator. 109 110 Args: 111 key: Section name to get 112 113 Returns: 114 The section content 115 """ 116 117 return self.fourc_input[key] 118 119 @classmethod 120 def from_4C_yaml( 121 cls, input_file_path: str | _Path, header_only: bool = False 122 ) -> InputFile: 123 """Load 4C yaml file. 124 125 Args: 126 input_file_path: Path to yaml file 127 header_only: Only extract header, i.e., all sections except the legacy ones 128 129 Returns: 130 Initialised object 131 """ 132 133 obj = cls() 134 obj.fourc_input = _FourCInput.from_4C_yaml(input_file_path, header_only) 135 return obj 136 137 @property 138 def sections(self) -> dict: 139 """All the set sections. 140 141 Returns: 142 dict: Set sections 143 """ 144 145 return self.fourc_input.sections 146 147 def pop(self, key: str, default_value: _Any = _NOT_SET) -> _Any: 148 """Pop section of input file. 149 150 Args: 151 key: Section name to pop 152 153 Returns: 154 The section content 155 """ 156 157 return self.fourc_input.pop(key, default_value) 158 159 def add(self, object_to_add, **kwargs): 160 """Add a mesh or a dictionary to the input file. 161 162 Args: 163 object: The object to be added. This can be a mesh or a dictionary. 164 **kwargs: Additional arguments to be passed to the add method. 165 """ 166 167 if isinstance(object_to_add, _Mesh): 168 self.add_mesh_to_input_file(mesh=object_to_add, **kwargs) 169 170 else: 171 self.fourc_input.combine_sections(object_to_add) 172 173 def dump( 174 self, 175 input_file_path: str | _Path, 176 *, 177 nox_xml_file: str | None = None, 178 add_header_default: bool = True, 179 add_header_information: bool = True, 180 add_footer_application_script: bool = True, 181 validate=True, 182 validate_sections_only: bool = False, 183 sort_function: _Callable[[dict], dict] | None = _sort_by_section_names, 184 fourcipp_yaml_style: bool = True, 185 ): 186 """Write the input file to disk. 187 188 Args: 189 input_file_path: 190 Path to the input file that should be created. 191 nox_xml_file: 192 If this is a string, the NOX xml file will be created with this 193 name. If this is None, the NOX xml file will be created with the 194 name of the input file with the extension ".nox.xml". 195 add_header_default: 196 Prepend the default header comment to the input file. 197 add_header_information: 198 If the information header should be exported to the input file 199 Contains creation date, git details of BeamMe, CubitPy and 200 original application which created the input file if available. 201 add_footer_application_script: 202 Append the application script which creates the input files as a 203 comment at the end of the input file. 204 validate: 205 Validate if the created input file is compatible with 4C with FourCIPP. 206 validate_sections_only: 207 Validate each section independently. Required sections are no longer 208 required, but the sections must be valid. 209 sort_function: 210 A function which sorts the sections of the input file. 211 fourcipp_yaml_style: 212 If True, the input file is written in the fourcipp yaml style. 213 """ 214 215 # Make sure the given input file is a Path instance. 216 input_file_path = _Path(input_file_path) 217 218 if self.nox_xml_contents: 219 if nox_xml_file is None: 220 nox_xml_file = input_file_path.name.split(".")[0] + ".nox.xml" 221 222 self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file} 223 224 # Write the xml file to the disc. 225 with open(input_file_path.parent / nox_xml_file, "w") as xml_file: 226 xml_file.write(self.nox_xml_contents) 227 228 # Add information header to the input file 229 if add_header_information: 230 self.add({"TITLE": self._get_header()}) 231 232 self.fourc_input.dump( 233 input_file_path=input_file_path, 234 validate=validate, 235 validate_sections_only=validate_sections_only, 236 convert_to_native_types=False, # conversion already happens during add() 237 sort_function=sort_function, 238 use_fourcipp_yaml_style=fourcipp_yaml_style, 239 ) 240 241 if add_header_default or add_footer_application_script: 242 with open(input_file_path, "r") as input_file: 243 lines = input_file.readlines() 244 245 if add_header_default: 246 lines = ["# " + line + "\n" for line in _INPUT_FILE_HEADER] + lines 247 248 if add_footer_application_script: 249 application_path = _get_application_path() 250 if application_path is not None: 251 lines += self._get_application_script(application_path) 252 253 with open(input_file_path, "w") as input_file: 254 input_file.writelines(lines) 255 256 def add_mesh_to_input_file(self, mesh: _Mesh) -> None: 257 """Add a mesh to the input file. 258 259 Args: 260 mesh: The mesh to be added to the input file. 261 """ 262 263 if _bme.check_overlapping_elements: 264 mesh.check_overlapping_elements() 265 266 # Compute geometry-set starting indices 267 start_indices_geometry_set = { 268 geometry_type: max( 269 (entry["d_id"] for entry in self.sections.get(section_name, [])), 270 default=0, 271 ) 272 for geometry_type, section_name in _INPUT_FILE_MAPPINGS[ 273 "geometry_sets_geometry_to_condition_name" 274 ].items() 275 } 276 277 # Determine global start indices 278 start_index_nodes = len(self.sections.get("NODE COORDS", [])) 279 280 start_index_elements = sum( 281 len(self.sections.get(section, [])) 282 for section in ("FLUID ELEMENTS", "STRUCTURE ELEMENTS") 283 ) 284 285 start_index_functions = max( 286 ( 287 int(section.split("FUNCT")[-1]) 288 for section in self.sections 289 if section.startswith("FUNCT") 290 ), 291 default=0, 292 ) 293 294 start_index_materials = max( 295 (material["MAT"] for material in self.sections.get("MATERIALS", [])), 296 default=0, 297 ) # materials imported from YAML may have arbitrary numbering 298 299 # Add sets from couplings and boundary conditions to a temp container 300 mesh.unlink_nodes() 301 mesh_sets = mesh.get_unique_geometry_sets( 302 geometry_set_start_indices=start_indices_geometry_set 303 ) 304 305 # Assign global indices 306 # Nodes 307 if len(mesh.nodes) != len(set(mesh.nodes)): 308 raise ValueError("Nodes are not unique!") 309 for i, node in enumerate(mesh.nodes, start=start_index_nodes): 310 node.i_global = i 311 312 # Elements 313 if len(mesh.elements) != len(set(mesh.elements)): 314 raise ValueError("Elements are not unique!") 315 i = start_index_elements 316 nurbs_count = 0 317 318 for element in mesh.elements: 319 element.i_global = i 320 if isinstance(element, _NURBSPatch): 321 element.i_nurbs_patch = nurbs_count 322 i += element.get_number_of_elements() 323 nurbs_count += 1 324 continue 325 i += 1 326 327 # Materials: Get a list of all materials in the mesh, 328 # including nested sub-materials. 329 all_materials = [ 330 material 331 for mesh_material in mesh.materials 332 for material in _get_all_contained_materials(mesh_material) 333 ] 334 if len(all_materials) != len(set(all_materials)): 335 raise ValueError("Materials are not unique!") 336 for i, material in enumerate(all_materials, start=start_index_materials): 337 material.i_global = i 338 339 # Functions 340 if len(mesh.functions) != len(set(mesh.functions)): 341 raise ValueError("Functions are not unique!") 342 for i, function in enumerate(mesh.functions, start=start_index_functions): 343 function.i_global = i 344 345 # Dump mesh to input file 346 def _dump(section_name: str, items: _List) -> None: 347 """Dump list of items to a section in the input file. 348 349 Args: 350 section_name: Name of the section 351 items: List of items to be dumped 352 """ 353 if not items: # do not write empty sections 354 return 355 dumped: list[_Any] = [] 356 for item in items: 357 _dump_item_to_list(dumped, item) 358 359 # Go through FourCIPP to convert to native types 360 # TODO this can be simplified/removed by using an internal type converter 361 if section_name in self.sections: 362 existing = self.pop(section_name) 363 existing.extend(dumped) 364 dumped = existing 365 366 self.add({section_name: dumped}) 367 368 # Materials 369 _dump("MATERIALS", all_materials) 370 371 # Functions 372 for function in mesh.functions: 373 self.add({f"FUNCT{function.i_global + 1}": function.data}) 374 375 # Couplings 376 # If there are couplings in the mesh, set the link between the nodes 377 # and elements, so the couplings can decide which DOFs they couple, 378 # depending on the type of the connected beam element. 379 if any( 380 mesh.boundary_conditions.get((key, _bme.geo.point), []) 381 for key in (_bme.bc.point_coupling, _bme.bc.point_coupling_penalty) 382 ): 383 mesh.set_node_links() 384 385 # Boundary conditions 386 for (bc_key, geometry_key), bc_list in mesh.boundary_conditions.items(): 387 if bc_list: 388 section = ( 389 bc_key 390 if isinstance(bc_key, str) 391 else _INPUT_FILE_MAPPINGS["boundary_conditions"][ 392 (bc_key, geometry_key) 393 ] 394 ) 395 _dump(section, bc_list) 396 397 # Additional element sections (NURBS etc.) 398 for element in mesh.elements: 399 _dump_item_to_section(self, element) 400 401 # Geometry sets 402 for geometry_key, items in mesh_sets.items(): 403 _dump( 404 _INPUT_FILE_MAPPINGS["geometry_sets_geometry_to_condition_name"][ 405 geometry_key 406 ], 407 items, 408 ) 409 410 # Nodes 411 _dump("NODE COORDS", mesh.nodes) 412 # Elements 413 _dump("STRUCTURE ELEMENTS", mesh.elements) 414 415 # TODO: reset all links and counters set in this method. 416 417 def _get_header(self) -> dict: 418 """Return the information header for the current BeamMe run. 419 420 Returns: 421 A dictionary with the header information. 422 """ 423 424 header: dict = {"BeamMe": {}} 425 426 header["BeamMe"]["creation_date"] = _datetime.now().isoformat( 427 sep=" ", timespec="seconds" 428 ) 429 430 # application which created the input file 431 application_path = _get_application_path() 432 if application_path is not None: 433 header["BeamMe"]["Application"] = {"path": str(application_path)} 434 435 application_git_sha, application_git_date = _get_git_data( 436 application_path.parent 437 ) 438 if application_git_sha is not None and application_git_date is not None: 439 header["BeamMe"]["Application"].update( 440 { 441 "git_sha": application_git_sha, 442 "git_date": application_git_date, 443 } 444 ) 445 446 # BeamMe information 447 beamme_git_sha, beamme_git_date = _get_git_data( 448 _Path(__file__).resolve().parent 449 ) 450 if beamme_git_sha is not None and beamme_git_date is not None: 451 header["BeamMe"]["BeamMe"] = { 452 "git_SHA": beamme_git_sha, 453 "git_date": beamme_git_date, 454 } 455 456 # CubitPy information 457 if _cubitpy_is_available(): 458 cubitpy_git_sha, cubitpy_git_date = _get_git_data( 459 _os.path.dirname(_cubitpy.__file__) 460 ) 461 462 if cubitpy_git_sha is not None and cubitpy_git_date is not None: 463 header["BeamMe"]["CubitPy"] = { 464 "git_SHA": cubitpy_git_sha, 465 "git_date": cubitpy_git_date, 466 } 467 468 return header 469 470 def _get_application_script(self, application_path: _Path) -> list[str]: 471 """Get the script that created this input file. 472 473 Args: 474 application_path: Path to the script that created this input file. 475 Returns: 476 A list of strings with the script that created this input file. 477 """ 478 479 application_script_lines = [ 480 "# Application script which created this input file:\n" 481 ] 482 483 with open(application_path) as script_file: 484 application_script_lines.extend("# " + line for line in script_file) 485 486 return application_script_lines
class
InputFile:
64class InputFile: 65 """An item that represents a complete 4C input file.""" 66 67 def __init__(self): 68 """Initialize the input file.""" 69 70 self.fourc_input = _FourCInput() 71 72 # Contents of NOX xml file. 73 self.nox_xml_contents = "" 74 75 # Register converters to directly convert non-primitive types 76 # to native Python types via the FourCIPP type converter. 77 self.fourc_input.type_converter.register_numpy_types() 78 self.fourc_input.type_converter.register_type( 79 (_Function, _Material, _Node), lambda converter, obj: obj.i_global + 1 80 ) 81 82 def __contains__(self, key: str) -> bool: 83 """Contains function. 84 85 Allows to use the `in` operator. 86 87 Args: 88 key: Section name to check if it is set 89 90 Returns: 91 True if section is set 92 """ 93 94 return key in self.fourc_input 95 96 def __setitem__(self, key: str, value: _Any) -> None: 97 """Set section. 98 99 Args: 100 key: Section name 101 value: Section entry 102 """ 103 104 self.fourc_input[key] = value 105 106 def __getitem__(self, key: str) -> _Any: 107 """Get section of input file. 108 109 Allows to use the indexing operator. 110 111 Args: 112 key: Section name to get 113 114 Returns: 115 The section content 116 """ 117 118 return self.fourc_input[key] 119 120 @classmethod 121 def from_4C_yaml( 122 cls, input_file_path: str | _Path, header_only: bool = False 123 ) -> InputFile: 124 """Load 4C yaml file. 125 126 Args: 127 input_file_path: Path to yaml file 128 header_only: Only extract header, i.e., all sections except the legacy ones 129 130 Returns: 131 Initialised object 132 """ 133 134 obj = cls() 135 obj.fourc_input = _FourCInput.from_4C_yaml(input_file_path, header_only) 136 return obj 137 138 @property 139 def sections(self) -> dict: 140 """All the set sections. 141 142 Returns: 143 dict: Set sections 144 """ 145 146 return self.fourc_input.sections 147 148 def pop(self, key: str, default_value: _Any = _NOT_SET) -> _Any: 149 """Pop section of input file. 150 151 Args: 152 key: Section name to pop 153 154 Returns: 155 The section content 156 """ 157 158 return self.fourc_input.pop(key, default_value) 159 160 def add(self, object_to_add, **kwargs): 161 """Add a mesh or a dictionary to the input file. 162 163 Args: 164 object: The object to be added. This can be a mesh or a dictionary. 165 **kwargs: Additional arguments to be passed to the add method. 166 """ 167 168 if isinstance(object_to_add, _Mesh): 169 self.add_mesh_to_input_file(mesh=object_to_add, **kwargs) 170 171 else: 172 self.fourc_input.combine_sections(object_to_add) 173 174 def dump( 175 self, 176 input_file_path: str | _Path, 177 *, 178 nox_xml_file: str | None = None, 179 add_header_default: bool = True, 180 add_header_information: bool = True, 181 add_footer_application_script: bool = True, 182 validate=True, 183 validate_sections_only: bool = False, 184 sort_function: _Callable[[dict], dict] | None = _sort_by_section_names, 185 fourcipp_yaml_style: bool = True, 186 ): 187 """Write the input file to disk. 188 189 Args: 190 input_file_path: 191 Path to the input file that should be created. 192 nox_xml_file: 193 If this is a string, the NOX xml file will be created with this 194 name. If this is None, the NOX xml file will be created with the 195 name of the input file with the extension ".nox.xml". 196 add_header_default: 197 Prepend the default header comment to the input file. 198 add_header_information: 199 If the information header should be exported to the input file 200 Contains creation date, git details of BeamMe, CubitPy and 201 original application which created the input file if available. 202 add_footer_application_script: 203 Append the application script which creates the input files as a 204 comment at the end of the input file. 205 validate: 206 Validate if the created input file is compatible with 4C with FourCIPP. 207 validate_sections_only: 208 Validate each section independently. Required sections are no longer 209 required, but the sections must be valid. 210 sort_function: 211 A function which sorts the sections of the input file. 212 fourcipp_yaml_style: 213 If True, the input file is written in the fourcipp yaml style. 214 """ 215 216 # Make sure the given input file is a Path instance. 217 input_file_path = _Path(input_file_path) 218 219 if self.nox_xml_contents: 220 if nox_xml_file is None: 221 nox_xml_file = input_file_path.name.split(".")[0] + ".nox.xml" 222 223 self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file} 224 225 # Write the xml file to the disc. 226 with open(input_file_path.parent / nox_xml_file, "w") as xml_file: 227 xml_file.write(self.nox_xml_contents) 228 229 # Add information header to the input file 230 if add_header_information: 231 self.add({"TITLE": self._get_header()}) 232 233 self.fourc_input.dump( 234 input_file_path=input_file_path, 235 validate=validate, 236 validate_sections_only=validate_sections_only, 237 convert_to_native_types=False, # conversion already happens during add() 238 sort_function=sort_function, 239 use_fourcipp_yaml_style=fourcipp_yaml_style, 240 ) 241 242 if add_header_default or add_footer_application_script: 243 with open(input_file_path, "r") as input_file: 244 lines = input_file.readlines() 245 246 if add_header_default: 247 lines = ["# " + line + "\n" for line in _INPUT_FILE_HEADER] + lines 248 249 if add_footer_application_script: 250 application_path = _get_application_path() 251 if application_path is not None: 252 lines += self._get_application_script(application_path) 253 254 with open(input_file_path, "w") as input_file: 255 input_file.writelines(lines) 256 257 def add_mesh_to_input_file(self, mesh: _Mesh) -> None: 258 """Add a mesh to the input file. 259 260 Args: 261 mesh: The mesh to be added to the input file. 262 """ 263 264 if _bme.check_overlapping_elements: 265 mesh.check_overlapping_elements() 266 267 # Compute geometry-set starting indices 268 start_indices_geometry_set = { 269 geometry_type: max( 270 (entry["d_id"] for entry in self.sections.get(section_name, [])), 271 default=0, 272 ) 273 for geometry_type, section_name in _INPUT_FILE_MAPPINGS[ 274 "geometry_sets_geometry_to_condition_name" 275 ].items() 276 } 277 278 # Determine global start indices 279 start_index_nodes = len(self.sections.get("NODE COORDS", [])) 280 281 start_index_elements = sum( 282 len(self.sections.get(section, [])) 283 for section in ("FLUID ELEMENTS", "STRUCTURE ELEMENTS") 284 ) 285 286 start_index_functions = max( 287 ( 288 int(section.split("FUNCT")[-1]) 289 for section in self.sections 290 if section.startswith("FUNCT") 291 ), 292 default=0, 293 ) 294 295 start_index_materials = max( 296 (material["MAT"] for material in self.sections.get("MATERIALS", [])), 297 default=0, 298 ) # materials imported from YAML may have arbitrary numbering 299 300 # Add sets from couplings and boundary conditions to a temp container 301 mesh.unlink_nodes() 302 mesh_sets = mesh.get_unique_geometry_sets( 303 geometry_set_start_indices=start_indices_geometry_set 304 ) 305 306 # Assign global indices 307 # Nodes 308 if len(mesh.nodes) != len(set(mesh.nodes)): 309 raise ValueError("Nodes are not unique!") 310 for i, node in enumerate(mesh.nodes, start=start_index_nodes): 311 node.i_global = i 312 313 # Elements 314 if len(mesh.elements) != len(set(mesh.elements)): 315 raise ValueError("Elements are not unique!") 316 i = start_index_elements 317 nurbs_count = 0 318 319 for element in mesh.elements: 320 element.i_global = i 321 if isinstance(element, _NURBSPatch): 322 element.i_nurbs_patch = nurbs_count 323 i += element.get_number_of_elements() 324 nurbs_count += 1 325 continue 326 i += 1 327 328 # Materials: Get a list of all materials in the mesh, 329 # including nested sub-materials. 330 all_materials = [ 331 material 332 for mesh_material in mesh.materials 333 for material in _get_all_contained_materials(mesh_material) 334 ] 335 if len(all_materials) != len(set(all_materials)): 336 raise ValueError("Materials are not unique!") 337 for i, material in enumerate(all_materials, start=start_index_materials): 338 material.i_global = i 339 340 # Functions 341 if len(mesh.functions) != len(set(mesh.functions)): 342 raise ValueError("Functions are not unique!") 343 for i, function in enumerate(mesh.functions, start=start_index_functions): 344 function.i_global = i 345 346 # Dump mesh to input file 347 def _dump(section_name: str, items: _List) -> None: 348 """Dump list of items to a section in the input file. 349 350 Args: 351 section_name: Name of the section 352 items: List of items to be dumped 353 """ 354 if not items: # do not write empty sections 355 return 356 dumped: list[_Any] = [] 357 for item in items: 358 _dump_item_to_list(dumped, item) 359 360 # Go through FourCIPP to convert to native types 361 # TODO this can be simplified/removed by using an internal type converter 362 if section_name in self.sections: 363 existing = self.pop(section_name) 364 existing.extend(dumped) 365 dumped = existing 366 367 self.add({section_name: dumped}) 368 369 # Materials 370 _dump("MATERIALS", all_materials) 371 372 # Functions 373 for function in mesh.functions: 374 self.add({f"FUNCT{function.i_global + 1}": function.data}) 375 376 # Couplings 377 # If there are couplings in the mesh, set the link between the nodes 378 # and elements, so the couplings can decide which DOFs they couple, 379 # depending on the type of the connected beam element. 380 if any( 381 mesh.boundary_conditions.get((key, _bme.geo.point), []) 382 for key in (_bme.bc.point_coupling, _bme.bc.point_coupling_penalty) 383 ): 384 mesh.set_node_links() 385 386 # Boundary conditions 387 for (bc_key, geometry_key), bc_list in mesh.boundary_conditions.items(): 388 if bc_list: 389 section = ( 390 bc_key 391 if isinstance(bc_key, str) 392 else _INPUT_FILE_MAPPINGS["boundary_conditions"][ 393 (bc_key, geometry_key) 394 ] 395 ) 396 _dump(section, bc_list) 397 398 # Additional element sections (NURBS etc.) 399 for element in mesh.elements: 400 _dump_item_to_section(self, element) 401 402 # Geometry sets 403 for geometry_key, items in mesh_sets.items(): 404 _dump( 405 _INPUT_FILE_MAPPINGS["geometry_sets_geometry_to_condition_name"][ 406 geometry_key 407 ], 408 items, 409 ) 410 411 # Nodes 412 _dump("NODE COORDS", mesh.nodes) 413 # Elements 414 _dump("STRUCTURE ELEMENTS", mesh.elements) 415 416 # TODO: reset all links and counters set in this method. 417 418 def _get_header(self) -> dict: 419 """Return the information header for the current BeamMe run. 420 421 Returns: 422 A dictionary with the header information. 423 """ 424 425 header: dict = {"BeamMe": {}} 426 427 header["BeamMe"]["creation_date"] = _datetime.now().isoformat( 428 sep=" ", timespec="seconds" 429 ) 430 431 # application which created the input file 432 application_path = _get_application_path() 433 if application_path is not None: 434 header["BeamMe"]["Application"] = {"path": str(application_path)} 435 436 application_git_sha, application_git_date = _get_git_data( 437 application_path.parent 438 ) 439 if application_git_sha is not None and application_git_date is not None: 440 header["BeamMe"]["Application"].update( 441 { 442 "git_sha": application_git_sha, 443 "git_date": application_git_date, 444 } 445 ) 446 447 # BeamMe information 448 beamme_git_sha, beamme_git_date = _get_git_data( 449 _Path(__file__).resolve().parent 450 ) 451 if beamme_git_sha is not None and beamme_git_date is not None: 452 header["BeamMe"]["BeamMe"] = { 453 "git_SHA": beamme_git_sha, 454 "git_date": beamme_git_date, 455 } 456 457 # CubitPy information 458 if _cubitpy_is_available(): 459 cubitpy_git_sha, cubitpy_git_date = _get_git_data( 460 _os.path.dirname(_cubitpy.__file__) 461 ) 462 463 if cubitpy_git_sha is not None and cubitpy_git_date is not None: 464 header["BeamMe"]["CubitPy"] = { 465 "git_SHA": cubitpy_git_sha, 466 "git_date": cubitpy_git_date, 467 } 468 469 return header 470 471 def _get_application_script(self, application_path: _Path) -> list[str]: 472 """Get the script that created this input file. 473 474 Args: 475 application_path: Path to the script that created this input file. 476 Returns: 477 A list of strings with the script that created this input file. 478 """ 479 480 application_script_lines = [ 481 "# Application script which created this input file:\n" 482 ] 483 484 with open(application_path) as script_file: 485 application_script_lines.extend("# " + line for line in script_file) 486 487 return application_script_lines
An item that represents a complete 4C input file.
InputFile()
67 def __init__(self): 68 """Initialize the input file.""" 69 70 self.fourc_input = _FourCInput() 71 72 # Contents of NOX xml file. 73 self.nox_xml_contents = "" 74 75 # Register converters to directly convert non-primitive types 76 # to native Python types via the FourCIPP type converter. 77 self.fourc_input.type_converter.register_numpy_types() 78 self.fourc_input.type_converter.register_type( 79 (_Function, _Material, _Node), lambda converter, obj: obj.i_global + 1 80 )
Initialize the input file.
@classmethod
def
from_4C_yaml( cls, input_file_path: str | pathlib._local.Path, header_only: bool = False) -> InputFile:
120 @classmethod 121 def from_4C_yaml( 122 cls, input_file_path: str | _Path, header_only: bool = False 123 ) -> InputFile: 124 """Load 4C yaml file. 125 126 Args: 127 input_file_path: Path to yaml file 128 header_only: Only extract header, i.e., all sections except the legacy ones 129 130 Returns: 131 Initialised object 132 """ 133 134 obj = cls() 135 obj.fourc_input = _FourCInput.from_4C_yaml(input_file_path, header_only) 136 return obj
Load 4C yaml file.
Arguments:
- input_file_path: Path to yaml file
- header_only: Only extract header, i.e., all sections except the legacy ones
Returns:
Initialised object
sections: dict
138 @property 139 def sections(self) -> dict: 140 """All the set sections. 141 142 Returns: 143 dict: Set sections 144 """ 145 146 return self.fourc_input.sections
All the set sections.
Returns:
dict: Set sections
def
pop(self, key: str, default_value: Any = NotSet(<class 'object'>)) -> Any:
148 def pop(self, key: str, default_value: _Any = _NOT_SET) -> _Any: 149 """Pop section of input file. 150 151 Args: 152 key: Section name to pop 153 154 Returns: 155 The section content 156 """ 157 158 return self.fourc_input.pop(key, default_value)
Pop section of input file.
Arguments:
- key: Section name to pop
Returns:
The section content
def
add(self, object_to_add, **kwargs):
160 def add(self, object_to_add, **kwargs): 161 """Add a mesh or a dictionary to the input file. 162 163 Args: 164 object: The object to be added. This can be a mesh or a dictionary. 165 **kwargs: Additional arguments to be passed to the add method. 166 """ 167 168 if isinstance(object_to_add, _Mesh): 169 self.add_mesh_to_input_file(mesh=object_to_add, **kwargs) 170 171 else: 172 self.fourc_input.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._local.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):
174 def dump( 175 self, 176 input_file_path: str | _Path, 177 *, 178 nox_xml_file: str | None = None, 179 add_header_default: bool = True, 180 add_header_information: bool = True, 181 add_footer_application_script: bool = True, 182 validate=True, 183 validate_sections_only: bool = False, 184 sort_function: _Callable[[dict], dict] | None = _sort_by_section_names, 185 fourcipp_yaml_style: bool = True, 186 ): 187 """Write the input file to disk. 188 189 Args: 190 input_file_path: 191 Path to the input file that should be created. 192 nox_xml_file: 193 If this is a string, the NOX xml file will be created with this 194 name. If this is None, the NOX xml file will be created with the 195 name of the input file with the extension ".nox.xml". 196 add_header_default: 197 Prepend the default header comment to the input file. 198 add_header_information: 199 If the information header should be exported to the input file 200 Contains creation date, git details of BeamMe, CubitPy and 201 original application which created the input file if available. 202 add_footer_application_script: 203 Append the application script which creates the input files as a 204 comment at the end of the input file. 205 validate: 206 Validate if the created input file is compatible with 4C with FourCIPP. 207 validate_sections_only: 208 Validate each section independently. Required sections are no longer 209 required, but the sections must be valid. 210 sort_function: 211 A function which sorts the sections of the input file. 212 fourcipp_yaml_style: 213 If True, the input file is written in the fourcipp yaml style. 214 """ 215 216 # Make sure the given input file is a Path instance. 217 input_file_path = _Path(input_file_path) 218 219 if self.nox_xml_contents: 220 if nox_xml_file is None: 221 nox_xml_file = input_file_path.name.split(".")[0] + ".nox.xml" 222 223 self["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file} 224 225 # Write the xml file to the disc. 226 with open(input_file_path.parent / nox_xml_file, "w") as xml_file: 227 xml_file.write(self.nox_xml_contents) 228 229 # Add information header to the input file 230 if add_header_information: 231 self.add({"TITLE": self._get_header()}) 232 233 self.fourc_input.dump( 234 input_file_path=input_file_path, 235 validate=validate, 236 validate_sections_only=validate_sections_only, 237 convert_to_native_types=False, # conversion already happens during add() 238 sort_function=sort_function, 239 use_fourcipp_yaml_style=fourcipp_yaml_style, 240 ) 241 242 if add_header_default or add_footer_application_script: 243 with open(input_file_path, "r") as input_file: 244 lines = input_file.readlines() 245 246 if add_header_default: 247 lines = ["# " + line + "\n" for line in _INPUT_FILE_HEADER] + lines 248 249 if add_footer_application_script: 250 application_path = _get_application_path() 251 if application_path is not None: 252 lines += self._get_application_script(application_path) 253 254 with open(input_file_path, "w") as input_file: 255 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.
257 def add_mesh_to_input_file(self, mesh: _Mesh) -> None: 258 """Add a mesh to the input file. 259 260 Args: 261 mesh: The mesh to be added to the input file. 262 """ 263 264 if _bme.check_overlapping_elements: 265 mesh.check_overlapping_elements() 266 267 # Compute geometry-set starting indices 268 start_indices_geometry_set = { 269 geometry_type: max( 270 (entry["d_id"] for entry in self.sections.get(section_name, [])), 271 default=0, 272 ) 273 for geometry_type, section_name in _INPUT_FILE_MAPPINGS[ 274 "geometry_sets_geometry_to_condition_name" 275 ].items() 276 } 277 278 # Determine global start indices 279 start_index_nodes = len(self.sections.get("NODE COORDS", [])) 280 281 start_index_elements = sum( 282 len(self.sections.get(section, [])) 283 for section in ("FLUID ELEMENTS", "STRUCTURE ELEMENTS") 284 ) 285 286 start_index_functions = max( 287 ( 288 int(section.split("FUNCT")[-1]) 289 for section in self.sections 290 if section.startswith("FUNCT") 291 ), 292 default=0, 293 ) 294 295 start_index_materials = max( 296 (material["MAT"] for material in self.sections.get("MATERIALS", [])), 297 default=0, 298 ) # materials imported from YAML may have arbitrary numbering 299 300 # Add sets from couplings and boundary conditions to a temp container 301 mesh.unlink_nodes() 302 mesh_sets = mesh.get_unique_geometry_sets( 303 geometry_set_start_indices=start_indices_geometry_set 304 ) 305 306 # Assign global indices 307 # Nodes 308 if len(mesh.nodes) != len(set(mesh.nodes)): 309 raise ValueError("Nodes are not unique!") 310 for i, node in enumerate(mesh.nodes, start=start_index_nodes): 311 node.i_global = i 312 313 # Elements 314 if len(mesh.elements) != len(set(mesh.elements)): 315 raise ValueError("Elements are not unique!") 316 i = start_index_elements 317 nurbs_count = 0 318 319 for element in mesh.elements: 320 element.i_global = i 321 if isinstance(element, _NURBSPatch): 322 element.i_nurbs_patch = nurbs_count 323 i += element.get_number_of_elements() 324 nurbs_count += 1 325 continue 326 i += 1 327 328 # Materials: Get a list of all materials in the mesh, 329 # including nested sub-materials. 330 all_materials = [ 331 material 332 for mesh_material in mesh.materials 333 for material in _get_all_contained_materials(mesh_material) 334 ] 335 if len(all_materials) != len(set(all_materials)): 336 raise ValueError("Materials are not unique!") 337 for i, material in enumerate(all_materials, start=start_index_materials): 338 material.i_global = i 339 340 # Functions 341 if len(mesh.functions) != len(set(mesh.functions)): 342 raise ValueError("Functions are not unique!") 343 for i, function in enumerate(mesh.functions, start=start_index_functions): 344 function.i_global = i 345 346 # Dump mesh to input file 347 def _dump(section_name: str, items: _List) -> None: 348 """Dump list of items to a section in the input file. 349 350 Args: 351 section_name: Name of the section 352 items: List of items to be dumped 353 """ 354 if not items: # do not write empty sections 355 return 356 dumped: list[_Any] = [] 357 for item in items: 358 _dump_item_to_list(dumped, item) 359 360 # Go through FourCIPP to convert to native types 361 # TODO this can be simplified/removed by using an internal type converter 362 if section_name in self.sections: 363 existing = self.pop(section_name) 364 existing.extend(dumped) 365 dumped = existing 366 367 self.add({section_name: dumped}) 368 369 # Materials 370 _dump("MATERIALS", all_materials) 371 372 # Functions 373 for function in mesh.functions: 374 self.add({f"FUNCT{function.i_global + 1}": function.data}) 375 376 # Couplings 377 # If there are couplings in the mesh, set the link between the nodes 378 # and elements, so the couplings can decide which DOFs they couple, 379 # depending on the type of the connected beam element. 380 if any( 381 mesh.boundary_conditions.get((key, _bme.geo.point), []) 382 for key in (_bme.bc.point_coupling, _bme.bc.point_coupling_penalty) 383 ): 384 mesh.set_node_links() 385 386 # Boundary conditions 387 for (bc_key, geometry_key), bc_list in mesh.boundary_conditions.items(): 388 if bc_list: 389 section = ( 390 bc_key 391 if isinstance(bc_key, str) 392 else _INPUT_FILE_MAPPINGS["boundary_conditions"][ 393 (bc_key, geometry_key) 394 ] 395 ) 396 _dump(section, bc_list) 397 398 # Additional element sections (NURBS etc.) 399 for element in mesh.elements: 400 _dump_item_to_section(self, element) 401 402 # Geometry sets 403 for geometry_key, items in mesh_sets.items(): 404 _dump( 405 _INPUT_FILE_MAPPINGS["geometry_sets_geometry_to_condition_name"][ 406 geometry_key 407 ], 408 items, 409 ) 410 411 # Nodes 412 _dump("NODE COORDS", mesh.nodes) 413 # Elements 414 _dump("STRUCTURE ELEMENTS", mesh.elements) 415 416 # 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.