Coverage for src/beamme/four_c/input_file.py: 85%
124 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-28 15:20 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-07-28 15:20 +0000
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 4C."""
24import os as _os
25from collections import defaultdict as _defaultdict
26from collections.abc import Callable as _Callable
27from datetime import datetime as _datetime
28from pathlib import Path as _Path
29from typing import Any as _Any
30from typing import Self as _Self
32from fourcipp.fourc_input import FourCInput as _FourCInput
33from fourcipp.fourc_input import sort_by_section_names as _sort_by_section_names
34from fourcipp.utils.not_set import NOT_SET as _NOT_SET
36from beamme.core.conf import INPUT_FILE_HEADER as _INPUT_FILE_HEADER
37from beamme.core.mesh import Mesh as _Mesh
38from beamme.core.mesh_representation import MeshRepresentation as _MeshRepresentation
39from beamme.four_c.boundary_condition_data import (
40 FourCBoundaryConditionData as _FourCBoundaryConditionData,
41)
42from beamme.four_c.element_data import FourCElementData as _FourCElementData
43from beamme.four_c.input_file_dump_functions import (
44 dump_mesh_representation_to_input_file_vtu as _dump_mesh_representation_to_input_file_vtu,
45)
46from beamme.four_c.input_file_dump_functions import (
47 dump_mesh_representation_to_input_file_yaml as _dump_mesh_representation_to_input_file_yaml,
48)
49from beamme.four_c.input_file_dump_functions import (
50 dump_mesh_to_input_file as _dump_mesh_to_input_file,
51)
52from beamme.utils.environment import cubitpy_is_available as _cubitpy_is_available
53from beamme.utils.environment import get_application_path as _get_application_path
54from beamme.utils.environment import get_git_data as _get_git_data
56if _cubitpy_is_available():
57 import cubitpy as _cubitpy
60class InputFile:
61 """An item that represents a complete 4C input file."""
63 def __init__(self) -> None:
64 """Initialize the input file."""
65 self.fourc_input = _FourCInput()
67 # Register converters to directly convert non-primitive types
68 # to native Python types via the FourCIPP type converter.
69 self.fourc_input.type_converter.register_numpy_types()
71 # Contents of NOX xml file.
72 self.nox_xml_contents = ""
74 # Mesh representation for this input file.
75 self.mesh_representation = _MeshRepresentation()
76 self.element_type_id_to_data: dict[_Any, _FourCElementData] = {}
78 # Boundary conditions
79 self.boundary_conditions: dict[str, list[_FourCBoundaryConditionData]] = (
80 _defaultdict(list)
81 )
83 def __contains__(self, key: str) -> bool:
84 """Contains function.
86 Allows to use the `in` operator.
88 Args:
89 key: Section name to check if it is set
91 Returns:
92 True if section is set
93 """
94 return key in self.fourc_input
96 def __setitem__(self, key: str, value: _Any) -> None:
97 """Set section.
99 Args:
100 key: Section name
101 value: Section entry
102 """
103 self.fourc_input[key] = value
105 def __getitem__(self, key: str) -> _Any:
106 """Get section of input file.
108 Allows to use the indexing operator.
110 Args:
111 key: Section name to get
113 Returns:
114 The section content
115 """
116 return self.fourc_input[key]
118 @classmethod
119 def from_4C_yaml(
120 cls, input_file_path: str | _Path, header_only: bool = False
121 ) -> _Self:
122 """Load 4C yaml file.
124 Args:
125 input_file_path: Path to yaml file
126 header_only: Only extract header, i.e., all sections except the legacy ones
128 Returns:
129 Initialised object
130 """
131 obj = cls()
132 obj.fourc_input = _FourCInput.from_4C_yaml(input_file_path, header_only)
133 return obj
135 @property
136 def sections(self) -> dict:
137 """All the set sections.
139 Returns:
140 dict: Set sections
141 """
142 return self.fourc_input.sections
144 def pop(self, key: str, default_value: _Any = _NOT_SET) -> _Any:
145 """Pop section of input file.
147 Args:
148 key: Section name to pop
150 Returns:
151 The section content
152 """
153 return self.fourc_input.pop(key, default_value)
155 def add(self, object_to_add, **kwargs):
156 """Add a mesh or a dictionary to the input file.
158 Args:
159 object: The object to be added. This can be a mesh or a dictionary.
160 **kwargs: Additional arguments to be passed to the add method.
161 """
162 if isinstance(object_to_add, _Mesh):
163 _dump_mesh_to_input_file(self, mesh=object_to_add, **kwargs)
165 else:
166 self.fourc_input.combine_sections(object_to_add)
168 def get_fourcipp_input_with_mesh(self) -> _FourCInput:
169 """Return a copy of the FourCIPP input file with the contents of the mesh
170 representation dumped to the yaml sections."""
171 fourc_input = self.fourc_input.copy()
172 _dump_mesh_representation_to_input_file_yaml(
173 fourc_input,
174 self.mesh_representation,
175 self.element_type_id_to_data,
176 self.boundary_conditions,
177 )
178 return fourc_input
180 def dump(
181 self,
182 input_file_path: str | _Path,
183 *,
184 mesh_format: str = "vtu",
185 vtu_binary: bool = True,
186 nox_xml_file: str | None = None,
187 add_header_default: bool = True,
188 add_header_information: bool = True,
189 add_footer_application_script: bool = True,
190 validate=True,
191 validate_sections_only: bool = False,
192 sort_function: _Callable[[dict], dict] | None = _sort_by_section_names,
193 fourcipp_yaml_style: bool = True,
194 ):
195 """Write the input file to disk.
197 Args:
198 input_file_path:
199 Path to the input file that should be created.
200 mesh_format:
201 The format in which the mesh information should be written.
202 Currently, "vtu" and "yaml" are supported.
203 vtu_binary:
204 Only relevant if mesh_format is "vtu". If True, the vtu file will
205 be written in binary format. Otherwise, it will be written in ascii format.
206 nox_xml_file:
207 If this is a string, the NOX xml file will be created with this
208 name. If this is None, the NOX xml file will be created with the
209 name of the input file with the extension ".nox.xml".
210 add_header_default:
211 Prepend the default header comment to the input file.
212 add_header_information:
213 If the information header should be exported to the input file
214 Contains creation date, git details of BeamMe, CubitPy and
215 original application which created the input file if available.
216 add_footer_application_script:
217 Append the application script which creates the input files as a
218 comment at the end of the input file.
219 validate:
220 Validate if the created input file is compatible with 4C with FourCIPP.
221 validate_sections_only:
222 Validate each section independently. Required sections are no longer
223 required, but the sections must be valid.
224 sort_function:
225 A function which sorts the sections of the input file.
226 fourcipp_yaml_style:
227 If True, the input file is written in the fourcipp yaml style.
228 """
229 # Make sure the given input file is a Path instance.
230 input_file_path = _Path(input_file_path)
232 # Base name of the input file without extension.
233 if not input_file_path.name.endswith(".4C.yaml"):
234 raise ValueError(
235 "Input file must have a .4C.yaml extension, but got the "
236 f"path {input_file_path}"
237 )
238 input_file_base_name = input_file_path.name.removesuffix(".4C.yaml")
240 # Create a deep copy of the existing input sections - this function should not alter
241 # the present instance of InputFile
242 fourc_input = self.fourc_input.copy()
244 # Add the mesh representation, either directly to the yaml input file or via an
245 # external mesh format.
246 if mesh_format == "vtu":
247 vtu_file_path = input_file_path.parent / (
248 input_file_base_name + ".mesh.vtu"
249 )
250 vtu_grid = _dump_mesh_representation_to_input_file_vtu(
251 fourc_input,
252 self.mesh_representation,
253 self.element_type_id_to_data,
254 self.boundary_conditions,
255 )
256 # Save the grid and add the file name to the input file
257 vtu_grid.save(vtu_file_path, binary=vtu_binary)
258 fourc_input["STRUCTURE GEOMETRY"]["FILE"] = vtu_file_path.name
259 elif mesh_format == "yaml":
260 _dump_mesh_representation_to_input_file_yaml(
261 fourc_input,
262 self.mesh_representation,
263 self.element_type_id_to_data,
264 self.boundary_conditions,
265 )
266 else:
267 raise ValueError(f"Unsupported mesh format: {mesh_format}.")
269 if self.nox_xml_contents:
270 if nox_xml_file is None:
271 nox_xml_file = input_file_base_name + ".nox.xml"
273 fourc_input["STRUCT NOX/Status Test"] = {"XML File": nox_xml_file}
275 # Write the xml file to the disc.
276 with open(input_file_path.parent / nox_xml_file, "w") as xml_file:
277 xml_file.write(self.nox_xml_contents)
279 # Add information header to the input file
280 if add_header_information:
281 fourc_input.combine_sections({"TITLE": self._get_header()})
283 fourc_input.dump(
284 input_file_path=input_file_path,
285 validate=validate,
286 validate_sections_only=validate_sections_only,
287 convert_to_native_types=False, # conversion already happens during add()
288 sort_function=sort_function,
289 use_fourcipp_yaml_style=fourcipp_yaml_style,
290 )
292 if add_header_default or add_footer_application_script:
293 with open(input_file_path, "r") as input_file:
294 lines = input_file.readlines()
296 if add_header_default:
297 lines = ["# " + line + "\n" for line in _INPUT_FILE_HEADER] + lines
299 if add_footer_application_script:
300 application_path = _get_application_path()
301 if application_path is not None:
302 lines += self._get_application_script(application_path)
304 with open(input_file_path, "w") as input_file:
305 input_file.writelines(lines)
307 def _get_header(self) -> dict:
308 """Return the information header for the current BeamMe run.
310 Returns:
311 A dictionary with the header information.
312 """
313 header: dict = {"BeamMe": {}}
315 header["BeamMe"]["creation_date"] = _datetime.now().isoformat(
316 sep=" ", timespec="seconds"
317 )
319 # application which created the input file
320 application_path = _get_application_path()
321 if application_path is not None:
322 header["BeamMe"]["Application"] = {"path": str(application_path)}
324 application_git_sha, application_git_date = _get_git_data(
325 application_path.parent
326 )
327 if application_git_sha is not None and application_git_date is not None:
328 header["BeamMe"]["Application"].update(
329 {
330 "git_sha": application_git_sha,
331 "git_date": application_git_date,
332 }
333 )
335 # BeamMe information
336 beamme_git_sha, beamme_git_date = _get_git_data(
337 _Path(__file__).resolve().parent
338 )
339 if beamme_git_sha is not None and beamme_git_date is not None:
340 header["BeamMe"]["BeamMe"] = {
341 "git_SHA": beamme_git_sha,
342 "git_date": beamme_git_date,
343 }
345 # CubitPy information
346 if _cubitpy_is_available():
347 cubitpy_git_sha, cubitpy_git_date = _get_git_data(
348 _os.path.dirname(_cubitpy.__file__)
349 )
351 if cubitpy_git_sha is not None and cubitpy_git_date is not None:
352 header["BeamMe"]["CubitPy"] = {
353 "git_SHA": cubitpy_git_sha,
354 "git_date": cubitpy_git_date,
355 }
357 return header
359 def _get_application_script(self, application_path: _Path) -> list[str]:
360 """Get the script that created this input file.
362 Args:
363 application_path: Path to the script that created this input file.
364 Returns:
365 A list of strings with the script that created this input file.
366 """
367 application_script_lines = [
368 "# Application script which created this input file:\n"
369 ]
371 with open(application_path) as script_file:
372 application_script_lines.extend("# " + line for line in script_file)
374 return application_script_lines
376 def _contains_mesh_based_geometry(self, allowed_extensions: list[str]) -> bool:
377 """Check if the input file contains mesh-based geometry.
379 Args:
380 allowed_extensions: List of allowed file extensions for mesh files.
382 Returns:
383 True if the input file contains mesh-based geometry of given type,
384 False otherwise.
385 """
386 structure_geometry_section = self.fourc_input.sections.get(
387 "STRUCTURE GEOMETRY", None
388 )
389 if structure_geometry_section is not None:
390 file_name = structure_geometry_section.get("FILE", None)
391 if file_name is not None:
392 mesh_file_name = _Path(file_name)
393 if mesh_file_name.suffix.lower() in allowed_extensions:
394 return True
395 return False
397 def contains_mesh_based_geometry_exodus(self) -> bool:
398 """Check if the input file contains exodus mesh-based geometry.
400 Returns:
401 True if the input file contains exodus mesh-based geometry, False otherwise.
402 """
403 return self._contains_mesh_based_geometry([".exo", ".e"])
405 def contains_mesh_based_geometry_vtu(self) -> bool:
406 """Check if the input file contains VTU mesh-based geometry.
408 Returns:
409 True if the input file contains VTU mesh-based geometry, False otherwise.
410 """
411 return self._contains_mesh_based_geometry([".vtu"])