Coverage for src/beamme/four_c/material.py: 86%
120 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-08 11:03 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-08 11:03 +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 file implements materials for 4C beams and solids."""
24import numpy as _np
26from beamme.core.material import Material as _Material
27from beamme.core.material import MaterialBeamBase as _MaterialBeamBase
28from beamme.core.material import MaterialSolidBase as _MaterialSolidBase
31def get_material_and_all_contained_sub_materials(
32 material: _Material, _visited_materials: set[int] | None = None
33) -> list[_Material]:
34 """Recursively collect all materials contained within a material, including
35 nested ones.
37 Args:
38 material:
39 The root material from which to collect contained materials.
40 _visited_materials:
41 Internal parameter used to track visited materials and prevent
42 infinite recursion in case of circular references.
43 Users should not pass this manually.
45 Returns:
46 A flat list containing the given material and all nested materials.
48 Raises:
49 ValueError:
50 If a circular material reference is detected.
51 """
53 if _visited_materials is None:
54 _visited_materials = set()
56 material_id = id(material)
57 if material_id in _visited_materials:
58 raise ValueError("Circular material reference detected!")
59 _visited_materials.add(material_id)
61 contained_materials = [material]
63 if "MATIDS" in material.data:
64 for item in material.data["MATIDS"]:
65 if isinstance(item, _Material):
66 contained_materials.extend(
67 get_material_and_all_contained_sub_materials(
68 item, _visited_materials
69 )
70 )
72 return contained_materials
75def get_material_to_i_global_mapping(
76 materials: list[_Material],
77) -> dict[_Material, int]:
78 """Get a mapping of all materials in the mesh to their global IDs.
80 Args:
81 materials: A list of all materials in the mesh.
83 Returns:
84 A dictionary mapping each material to its global ID. This also includes
85 sub-materials contained within other materials.
86 """
87 all_materials = [
88 material
89 for mesh_material in materials
90 for material in get_material_and_all_contained_sub_materials(mesh_material)
91 ]
92 material_to_i_global: dict[_Material, int] = {}
93 for material in all_materials:
94 if material not in material_to_i_global:
95 material_to_i_global[material] = len(material_to_i_global)
96 return material_to_i_global
99class MaterialReissner(_MaterialBeamBase):
100 """Holds material definition for Reissner beams."""
102 def __init__(
103 self,
104 shear_correction=1.0,
105 *,
106 by_modes=False,
107 scale_axial_rigidity=1.0,
108 scale_shear_rigidity=1.0,
109 scale_torsional_rigidity=1.0,
110 scale_bending_rigidity=1.0,
111 **kwargs,
112 ):
113 if by_modes:
114 mat_string = "MAT_BeamReissnerElastHyper_ByModes"
115 else:
116 mat_string = "MAT_BeamReissnerElastHyper"
118 super().__init__(material_string=mat_string, **kwargs)
120 # Shear factor for Reissner beam.
121 self.shear_correction = shear_correction
123 self.by_modes = by_modes
125 # Scaling factors to influence a single stiffness independently
126 self.scale_axial_rigidity = scale_axial_rigidity
127 self.scale_shear_rigidity = scale_shear_rigidity
128 self.scale_torsional_rigidity = scale_torsional_rigidity
129 self.scale_bending_rigidity = scale_bending_rigidity
131 if not by_modes and not all(
132 _np.isclose(x, 1.0)
133 for x in (
134 scale_axial_rigidity,
135 scale_shear_rigidity,
136 scale_torsional_rigidity,
137 scale_bending_rigidity,
138 )
139 ):
140 raise ValueError(
141 "Scaling factors are only supported for MAT_BeamReissnerElastHyper_ByModes"
142 )
144 def dump_to_list(self):
145 """Return a list with the (single) item representing this material."""
147 if self.radius is None or self.youngs_modulus is None:
148 raise ValueError(
149 "Radius and Young's modulus must be provided for beam materials."
150 )
152 if (
153 self.area is None
154 and self.mom2 is None
155 and self.mom3 is None
156 and self.polar is None
157 ):
158 area, mom2, mom3, polar = self.calc_area_stiffness()
159 elif (
160 self.area is not None
161 and self.mom2 is not None
162 and self.mom3 is not None
163 and self.polar is not None
164 ):
165 area = self.area
166 mom2 = self.mom2
167 mom3 = self.mom3
168 polar = self.polar
169 else:
170 raise ValueError(
171 "Either all relevant material parameters are set "
172 "by the user, or a circular cross-section will be assumed. "
173 "A combination is not possible"
174 )
176 if self.by_modes:
177 shear_modulus = self.youngs_modulus / (2.0 * (1.0 + self.nu))
179 data = {
180 "EA": (self.youngs_modulus * area) * self.scale_axial_rigidity,
181 "GA2": (shear_modulus * area * self.shear_correction)
182 * self.scale_shear_rigidity,
183 "GA3": (shear_modulus * area * self.shear_correction)
184 * self.scale_shear_rigidity,
185 "GI_T": (shear_modulus * polar) * self.scale_torsional_rigidity,
186 "EI2": (self.youngs_modulus * mom2) * self.scale_bending_rigidity,
187 "EI3": (self.youngs_modulus * mom3) * self.scale_bending_rigidity,
188 "RhoA": self.density * area,
189 "MASSMOMINPOL": self.density * (mom2 + mom3),
190 "MASSMOMIN2": self.density * mom2,
191 "MASSMOMIN3": self.density * mom3,
192 }
194 else:
195 data = {
196 "YOUNG": self.youngs_modulus,
197 "POISSONRATIO": self.nu,
198 "DENS": self.density,
199 "CROSSAREA": area,
200 "SHEARCORR": self.shear_correction,
201 "MOMINPOL": polar,
202 "MOMIN2": mom2,
203 "MOMIN3": mom3,
204 }
206 if self.interaction_radius is not None:
207 data["INTERACTIONRADIUS"] = self.interaction_radius
209 return {"MAT": self, self.material_string: data}
212class MaterialReissnerElastoplastic(MaterialReissner):
213 """Holds elasto-plastic material definition for Reissner beams."""
215 def __init__(
216 self,
217 *,
218 yield_moment=None,
219 isohardening_modulus_moment=None,
220 torsion_plasticity=False,
221 **kwargs,
222 ):
223 super().__init__(**kwargs)
224 self.material_string = "MAT_BeamReissnerElastPlastic"
226 if yield_moment is None or isohardening_modulus_moment is None:
227 raise ValueError(
228 "The yield moment and the isohardening modulus for moments must be specified "
229 "for plasticity."
230 )
232 self.yield_moment = yield_moment
233 self.isohardening_modulus_moment = isohardening_modulus_moment
234 self.torsion_plasticity = torsion_plasticity
236 def dump_to_list(self):
237 """Return a list with the (single) item representing this material."""
238 super_list = super().dump_to_list()
239 mat_dict = super_list[self.material_string]
240 mat_dict["YIELDM"] = self.yield_moment
241 mat_dict["ISOHARDM"] = self.isohardening_modulus_moment
242 mat_dict["TORSIONPLAST"] = self.torsion_plasticity
243 return super_list
246class MaterialKirchhoff(_MaterialBeamBase):
247 """Holds material definition for Kirchhoff beams."""
249 def __init__(self, is_fad=False, **kwargs):
250 super().__init__(material_string="MAT_BeamKirchhoffElastHyper", **kwargs)
251 self.is_fad = is_fad
253 def dump_to_list(self):
254 """Return a list with the (single) item representing this material."""
256 if self.radius is None or self.youngs_modulus is None:
257 raise ValueError(
258 "Radius and Young's modulus must be provided for beam materials."
259 )
261 if (
262 self.area is None
263 and self.mom2 is None
264 and self.mom3 is None
265 and self.polar is None
266 ):
267 area, mom2, mom3, polar = self.calc_area_stiffness()
268 elif (
269 self.area is not None
270 and self.mom2 is not None
271 and self.mom3 is not None
272 and self.polar is not None
273 ):
274 area = self.area
275 mom2 = self.mom2
276 mom3 = self.mom3
277 polar = self.polar
278 else:
279 raise ValueError(
280 "Either all relevant material parameters are set "
281 "by the user, or a circular cross-section will be assumed. "
282 "A combination is not possible"
283 )
284 data = {
285 "YOUNG": self.youngs_modulus,
286 "SHEARMOD": self.youngs_modulus / (2.0 * (1.0 + self.nu)),
287 "DENS": self.density,
288 "CROSSAREA": area,
289 "MOMINPOL": polar,
290 "MOMIN2": mom2,
291 "MOMIN3": mom3,
292 "FAD": self.is_fad,
293 }
294 if self.interaction_radius is not None:
295 data["INTERACTIONRADIUS"] = self.interaction_radius
296 return {"MAT": self, self.material_string: data}
299class MaterialEulerBernoulli(_MaterialBeamBase):
300 """Holds material definition for Euler Bernoulli beams."""
302 def __init__(self, **kwargs):
303 super().__init__(
304 material_string="MAT_BeamKirchhoffTorsionFreeElastHyper", **kwargs
305 )
307 def dump_to_list(self):
308 """Return a list with the (single) item representing this material."""
310 if self.radius is None or self.youngs_modulus is None:
311 raise ValueError(
312 "Radius and Young's modulus must be provided for beam materials."
313 )
315 area, mom2, _, _ = self.calc_area_stiffness()
316 if self.area is None and self.mom2 is None:
317 area, mom2, _, _ = self.calc_area_stiffness()
318 elif self.area is not None and self.mom2 is not None:
319 area = self.area
320 mom2 = self.mom2
321 else:
322 raise ValueError(
323 "Either all relevant material parameters are set "
324 "by the user, or a circular cross-section will be assumed. "
325 "A combination is not possible"
326 )
327 data = {
328 "YOUNG": self.youngs_modulus,
329 "DENS": self.density,
330 "CROSSAREA": area,
331 "MOMIN": mom2,
332 }
333 return {"MAT": self, self.material_string: data}
336class MaterialSolid(_MaterialSolidBase):
337 """Base class for a material for solids."""
339 def __init__(self, material_string=None, **kwargs):
340 """Set the material values for a solid."""
341 self.material_string = material_string
342 super().__init__(**kwargs)
344 def dump_to_list(self):
345 """Return a list with the (single) item representing this material."""
347 return {"MAT": self, self.material_string: self.data}
350class MaterialStVenantKirchhoff(MaterialSolid):
351 """Holds material definition for StVenant Kirchhoff solids."""
353 def __init__(self, youngs_modulus=None, nu=None, density=None):
354 if youngs_modulus is None or nu is None:
355 raise ValueError(
356 "Young's modulus and Poisson's ratio must be provided "
357 "for StVenant Kirchhoff solid materials."
358 )
359 data = {"YOUNG": youngs_modulus, "NUE": nu}
360 if density is not None:
361 data["DENS"] = density
362 super().__init__(
363 material_string="MAT_Struct_StVenantKirchhoff",
364 data=data,
365 )