Coverage for src/beamme/four_c/material.py: 86%
120 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 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 nested
35 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 """
52 if _visited_materials is None:
53 _visited_materials = set()
55 material_id = id(material)
56 if material_id in _visited_materials:
57 raise ValueError("Circular material reference detected!")
58 _visited_materials.add(material_id)
60 contained_materials = [material]
62 if "MATIDS" in material.data:
63 for item in material.data["MATIDS"]:
64 if isinstance(item, _Material):
65 contained_materials.extend(
66 get_material_and_all_contained_sub_materials(
67 item, _visited_materials
68 )
69 )
71 return contained_materials
74def get_material_to_i_global_mapping(
75 materials: list[_Material],
76) -> dict[_Material, int]:
77 """Get a mapping of all materials in the mesh to their global IDs.
79 Args:
80 materials: A list of all materials in the mesh.
82 Returns:
83 A dictionary mapping each material to its global ID. This also includes
84 sub-materials contained within other materials.
85 """
86 all_materials = [
87 material
88 for mesh_material in materials
89 for material in get_material_and_all_contained_sub_materials(mesh_material)
90 ]
91 material_to_i_global: dict[_Material, int] = {}
92 for material in all_materials:
93 if material not in material_to_i_global:
94 material_to_i_global[material] = len(material_to_i_global)
95 return material_to_i_global
98class MaterialReissner(_MaterialBeamBase):
99 """Holds material definition for Reissner beams."""
101 def __init__(
102 self,
103 shear_correction=1.0,
104 *,
105 by_modes=False,
106 scale_axial_rigidity=1.0,
107 scale_shear_rigidity=1.0,
108 scale_torsional_rigidity=1.0,
109 scale_bending_rigidity=1.0,
110 **kwargs,
111 ):
112 if by_modes:
113 mat_string = "MAT_BeamReissnerElastHyper_ByModes"
114 else:
115 mat_string = "MAT_BeamReissnerElastHyper"
117 super().__init__(material_string=mat_string, **kwargs)
119 # Shear factor for Reissner beam.
120 self.shear_correction = shear_correction
122 self.by_modes = by_modes
124 # Scaling factors to influence a single stiffness independently
125 self.scale_axial_rigidity = scale_axial_rigidity
126 self.scale_shear_rigidity = scale_shear_rigidity
127 self.scale_torsional_rigidity = scale_torsional_rigidity
128 self.scale_bending_rigidity = scale_bending_rigidity
130 if not by_modes and not all(
131 _np.isclose(x, 1.0)
132 for x in (
133 scale_axial_rigidity,
134 scale_shear_rigidity,
135 scale_torsional_rigidity,
136 scale_bending_rigidity,
137 )
138 ):
139 raise ValueError(
140 "Scaling factors are only supported for MAT_BeamReissnerElastHyper_ByModes"
141 )
143 def dump_to_list(self):
144 """Return a list with the (single) item representing this material."""
145 if self.radius is None or self.youngs_modulus is None:
146 raise ValueError(
147 "Radius and Young's modulus must be provided for beam materials."
148 )
150 if (
151 self.area is None
152 and self.mom2 is None
153 and self.mom3 is None
154 and self.polar is None
155 ):
156 area, mom2, mom3, polar = self.calc_area_stiffness()
157 elif (
158 self.area is not None
159 and self.mom2 is not None
160 and self.mom3 is not None
161 and self.polar is not None
162 ):
163 area = self.area
164 mom2 = self.mom2
165 mom3 = self.mom3
166 polar = self.polar
167 else:
168 raise ValueError(
169 "Either all relevant material parameters are set "
170 "by the user, or a circular cross-section will be assumed. "
171 "A combination is not possible"
172 )
174 if self.by_modes:
175 shear_modulus = self.youngs_modulus / (2.0 * (1.0 + self.nu))
177 data = {
178 "EA": (self.youngs_modulus * area) * self.scale_axial_rigidity,
179 "GA2": (shear_modulus * area * self.shear_correction)
180 * self.scale_shear_rigidity,
181 "GA3": (shear_modulus * area * self.shear_correction)
182 * self.scale_shear_rigidity,
183 "GI_T": (shear_modulus * polar) * self.scale_torsional_rigidity,
184 "EI2": (self.youngs_modulus * mom2) * self.scale_bending_rigidity,
185 "EI3": (self.youngs_modulus * mom3) * self.scale_bending_rigidity,
186 "RhoA": self.density * area,
187 "MASSMOMINPOL": self.density * (mom2 + mom3),
188 "MASSMOMIN2": self.density * mom2,
189 "MASSMOMIN3": self.density * mom3,
190 }
192 else:
193 data = {
194 "YOUNG": self.youngs_modulus,
195 "POISSONRATIO": self.nu,
196 "DENS": self.density,
197 "CROSSAREA": area,
198 "SHEARCORR": self.shear_correction,
199 "MOMINPOL": polar,
200 "MOMIN2": mom2,
201 "MOMIN3": mom3,
202 }
204 if self.interaction_radius is not None:
205 data["INTERACTIONRADIUS"] = self.interaction_radius
207 return {"MAT": self, self.material_string: data}
210class MaterialReissnerElastoplastic(MaterialReissner):
211 """Holds elasto-plastic material definition for Reissner beams."""
213 def __init__(
214 self,
215 *,
216 yield_moment=None,
217 isohardening_modulus_moment=None,
218 torsion_plasticity=False,
219 **kwargs,
220 ):
221 super().__init__(**kwargs)
222 self.material_string = "MAT_BeamReissnerElastPlastic"
224 if yield_moment is None or isohardening_modulus_moment is None:
225 raise ValueError(
226 "The yield moment and the isohardening modulus for moments must be specified "
227 "for plasticity."
228 )
230 self.yield_moment = yield_moment
231 self.isohardening_modulus_moment = isohardening_modulus_moment
232 self.torsion_plasticity = torsion_plasticity
234 def dump_to_list(self):
235 """Return a list with the (single) item representing this material."""
236 super_list = super().dump_to_list()
237 mat_dict = super_list[self.material_string]
238 mat_dict["YIELDM"] = self.yield_moment
239 mat_dict["ISOHARDM"] = self.isohardening_modulus_moment
240 mat_dict["TORSIONPLAST"] = self.torsion_plasticity
241 return super_list
244class MaterialKirchhoff(_MaterialBeamBase):
245 """Holds material definition for Kirchhoff beams."""
247 def __init__(self, is_fad=False, **kwargs):
248 super().__init__(material_string="MAT_BeamKirchhoffElastHyper", **kwargs)
249 self.is_fad = is_fad
251 def dump_to_list(self):
252 """Return a list with the (single) item representing this material."""
253 if self.radius is None or self.youngs_modulus is None:
254 raise ValueError(
255 "Radius and Young's modulus must be provided for beam materials."
256 )
258 if (
259 self.area is None
260 and self.mom2 is None
261 and self.mom3 is None
262 and self.polar is None
263 ):
264 area, mom2, mom3, polar = self.calc_area_stiffness()
265 elif (
266 self.area is not None
267 and self.mom2 is not None
268 and self.mom3 is not None
269 and self.polar is not None
270 ):
271 area = self.area
272 mom2 = self.mom2
273 mom3 = self.mom3
274 polar = self.polar
275 else:
276 raise ValueError(
277 "Either all relevant material parameters are set "
278 "by the user, or a circular cross-section will be assumed. "
279 "A combination is not possible"
280 )
281 data = {
282 "YOUNG": self.youngs_modulus,
283 "SHEARMOD": self.youngs_modulus / (2.0 * (1.0 + self.nu)),
284 "DENS": self.density,
285 "CROSSAREA": area,
286 "MOMINPOL": polar,
287 "MOMIN2": mom2,
288 "MOMIN3": mom3,
289 "FAD": self.is_fad,
290 }
291 if self.interaction_radius is not None:
292 data["INTERACTIONRADIUS"] = self.interaction_radius
293 return {"MAT": self, self.material_string: data}
296class MaterialEulerBernoulli(_MaterialBeamBase):
297 """Holds material definition for Euler Bernoulli beams."""
299 def __init__(self, **kwargs):
300 super().__init__(
301 material_string="MAT_BeamKirchhoffTorsionFreeElastHyper", **kwargs
302 )
304 def dump_to_list(self):
305 """Return a list with the (single) item representing this material."""
306 if self.radius is None or self.youngs_modulus is None:
307 raise ValueError(
308 "Radius and Young's modulus must be provided for beam materials."
309 )
311 area, mom2, _, _ = self.calc_area_stiffness()
312 if self.area is None and self.mom2 is None:
313 area, mom2, _, _ = self.calc_area_stiffness()
314 elif self.area is not None and self.mom2 is not None:
315 area = self.area
316 mom2 = self.mom2
317 else:
318 raise ValueError(
319 "Either all relevant material parameters are set "
320 "by the user, or a circular cross-section will be assumed. "
321 "A combination is not possible"
322 )
323 data = {
324 "YOUNG": self.youngs_modulus,
325 "DENS": self.density,
326 "CROSSAREA": area,
327 "MOMIN": mom2,
328 }
329 return {"MAT": self, self.material_string: data}
332class MaterialSolid(_MaterialSolidBase):
333 """Base class for a material for solids."""
335 def __init__(self, material_string=None, **kwargs):
336 """Set the material values for a solid."""
337 self.material_string = material_string
338 super().__init__(**kwargs)
340 def dump_to_list(self):
341 """Return a list with the (single) item representing this material."""
342 return {"MAT": self, self.material_string: self.data}
345class MaterialStVenantKirchhoff(MaterialSolid):
346 """Holds material definition for StVenant Kirchhoff solids."""
348 def __init__(self, youngs_modulus=None, nu=None, density=None):
349 if youngs_modulus is None or nu is None:
350 raise ValueError(
351 "Young's modulus and Poisson's ratio must be provided "
352 "for StVenant Kirchhoff solid materials."
353 )
354 data = {"YOUNG": youngs_modulus, "NUE": nu}
355 if density is not None:
356 data["DENS"] = density
357 super().__init__(
358 material_string="MAT_Struct_StVenantKirchhoff",
359 data=data,
360 )