Coverage for src / beamme / four_c / material.py: 85%
113 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-06 06:24 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-06 06:24 +0000
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 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_all_contained_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_all_contained_materials(item, _visited_materials)
68 )
70 return contained_materials
73class MaterialReissner(_MaterialBeamBase):
74 """Holds material definition for Reissner beams."""
76 def __init__(
77 self,
78 shear_correction=1.0,
79 *,
80 by_modes=False,
81 scale_axial_rigidity=1.0,
82 scale_shear_rigidity=1.0,
83 scale_torsional_rigidity=1.0,
84 scale_bending_rigidity=1.0,
85 **kwargs,
86 ):
87 if by_modes:
88 mat_string = "MAT_BeamReissnerElastHyper_ByModes"
89 else:
90 mat_string = "MAT_BeamReissnerElastHyper"
92 super().__init__(material_string=mat_string, **kwargs)
94 # Shear factor for Reissner beam.
95 self.shear_correction = shear_correction
97 self.by_modes = by_modes
99 # Scaling factors to influence a single stiffness independently
100 self.scale_axial_rigidity = scale_axial_rigidity
101 self.scale_shear_rigidity = scale_shear_rigidity
102 self.scale_torsional_rigidity = scale_torsional_rigidity
103 self.scale_bending_rigidity = scale_bending_rigidity
105 if not by_modes and not all(
106 _np.isclose(x, 1.0)
107 for x in (
108 scale_axial_rigidity,
109 scale_shear_rigidity,
110 scale_torsional_rigidity,
111 scale_bending_rigidity,
112 )
113 ):
114 raise ValueError(
115 "Scaling factors are only supported for MAT_BeamReissnerElastHyper_ByModes"
116 )
118 def dump_to_list(self):
119 """Return a list with the (single) item representing this material."""
121 if self.radius is None or self.youngs_modulus is None:
122 raise ValueError(
123 "Radius and Young's modulus must be provided for beam materials."
124 )
126 if (
127 self.area is None
128 and self.mom2 is None
129 and self.mom3 is None
130 and self.polar is None
131 ):
132 area, mom2, mom3, polar = self.calc_area_stiffness()
133 elif (
134 self.area is not None
135 and self.mom2 is not None
136 and self.mom3 is not None
137 and self.polar is not None
138 ):
139 area = self.area
140 mom2 = self.mom2
141 mom3 = self.mom3
142 polar = self.polar
143 else:
144 raise ValueError(
145 "Either all relevant material parameters are set "
146 "by the user, or a circular cross-section will be assumed. "
147 "A combination is not possible"
148 )
150 if self.by_modes:
151 shear_modulus = self.youngs_modulus / (2.0 * (1.0 + self.nu))
153 data = {
154 "EA": (self.youngs_modulus * area) * self.scale_axial_rigidity,
155 "GA2": (shear_modulus * area * self.shear_correction)
156 * self.scale_shear_rigidity,
157 "GA3": (shear_modulus * area * self.shear_correction)
158 * self.scale_shear_rigidity,
159 "GI_T": (shear_modulus * polar) * self.scale_torsional_rigidity,
160 "EI2": (self.youngs_modulus * mom2) * self.scale_bending_rigidity,
161 "EI3": (self.youngs_modulus * mom3) * self.scale_bending_rigidity,
162 "RhoA": self.density * area,
163 "MASSMOMINPOL": self.density * (mom2 + mom3),
164 "MASSMOMIN2": self.density * mom2,
165 "MASSMOMIN3": self.density * mom3,
166 }
168 else:
169 data = {
170 "YOUNG": self.youngs_modulus,
171 "POISSONRATIO": self.nu,
172 "DENS": self.density,
173 "CROSSAREA": area,
174 "SHEARCORR": self.shear_correction,
175 "MOMINPOL": polar,
176 "MOMIN2": mom2,
177 "MOMIN3": mom3,
178 }
180 if self.interaction_radius is not None:
181 data["INTERACTIONRADIUS"] = self.interaction_radius
183 return {"MAT": self.i_global + 1, self.material_string: data}
186class MaterialReissnerElastoplastic(MaterialReissner):
187 """Holds elasto-plastic material definition for Reissner beams."""
189 def __init__(
190 self,
191 *,
192 yield_moment=None,
193 isohardening_modulus_moment=None,
194 torsion_plasticity=False,
195 **kwargs,
196 ):
197 super().__init__(**kwargs)
198 self.material_string = "MAT_BeamReissnerElastPlastic"
200 if yield_moment is None or isohardening_modulus_moment is None:
201 raise ValueError(
202 "The yield moment and the isohardening modulus for moments must be specified "
203 "for plasticity."
204 )
206 self.yield_moment = yield_moment
207 self.isohardening_modulus_moment = isohardening_modulus_moment
208 self.torsion_plasticity = torsion_plasticity
210 def dump_to_list(self):
211 """Return a list with the (single) item representing this material."""
212 super_list = super().dump_to_list()
213 mat_dict = super_list[self.material_string]
214 mat_dict["YIELDM"] = self.yield_moment
215 mat_dict["ISOHARDM"] = self.isohardening_modulus_moment
216 mat_dict["TORSIONPLAST"] = self.torsion_plasticity
217 return super_list
220class MaterialKirchhoff(_MaterialBeamBase):
221 """Holds material definition for Kirchhoff beams."""
223 def __init__(self, is_fad=False, **kwargs):
224 super().__init__(material_string="MAT_BeamKirchhoffElastHyper", **kwargs)
225 self.is_fad = is_fad
227 def dump_to_list(self):
228 """Return a list with the (single) item representing this material."""
230 if self.radius is None or self.youngs_modulus is None:
231 raise ValueError(
232 "Radius and Young's modulus must be provided for beam materials."
233 )
235 if (
236 self.area is None
237 and self.mom2 is None
238 and self.mom3 is None
239 and self.polar is None
240 ):
241 area, mom2, mom3, polar = self.calc_area_stiffness()
242 elif (
243 self.area is not None
244 and self.mom2 is not None
245 and self.mom3 is not None
246 and self.polar is not None
247 ):
248 area = self.area
249 mom2 = self.mom2
250 mom3 = self.mom3
251 polar = self.polar
252 else:
253 raise ValueError(
254 "Either all relevant material parameters are set "
255 "by the user, or a circular cross-section will be assumed. "
256 "A combination is not possible"
257 )
258 data = {
259 "YOUNG": self.youngs_modulus,
260 "SHEARMOD": self.youngs_modulus / (2.0 * (1.0 + self.nu)),
261 "DENS": self.density,
262 "CROSSAREA": area,
263 "MOMINPOL": polar,
264 "MOMIN2": mom2,
265 "MOMIN3": mom3,
266 "FAD": self.is_fad,
267 }
268 if self.interaction_radius is not None:
269 data["INTERACTIONRADIUS"] = self.interaction_radius
270 return {"MAT": self.i_global + 1, self.material_string: data}
273class MaterialEulerBernoulli(_MaterialBeamBase):
274 """Holds material definition for Euler Bernoulli beams."""
276 def __init__(self, **kwargs):
277 super().__init__(
278 material_string="MAT_BeamKirchhoffTorsionFreeElastHyper", **kwargs
279 )
281 def dump_to_list(self):
282 """Return a list with the (single) item representing this material."""
284 if self.radius is None or self.youngs_modulus is None:
285 raise ValueError(
286 "Radius and Young's modulus must be provided for beam materials."
287 )
289 area, mom2, _, _ = self.calc_area_stiffness()
290 if self.area is None and self.mom2 is None:
291 area, mom2, _, _ = self.calc_area_stiffness()
292 elif self.area is not None and self.mom2 is not None:
293 area = self.area
294 mom2 = self.mom2
295 else:
296 raise ValueError(
297 "Either all relevant material parameters are set "
298 "by the user, or a circular cross-section will be assumed. "
299 "A combination is not possible"
300 )
301 data = {
302 "YOUNG": self.youngs_modulus,
303 "DENS": self.density,
304 "CROSSAREA": area,
305 "MOMIN": mom2,
306 }
307 return {"MAT": self.i_global + 1, self.material_string: data}
310class MaterialSolid(_MaterialSolidBase):
311 """Base class for a material for solids."""
313 def __init__(self, material_string=None, **kwargs):
314 """Set the material values for a solid."""
315 self.material_string = material_string
316 super().__init__(**kwargs)
318 def dump_to_list(self):
319 """Return a list with the (single) item representing this material."""
321 return {"MAT": self.i_global + 1, self.material_string: self.data}
324class MaterialStVenantKirchhoff(MaterialSolid):
325 """Holds material definition for StVenant Kirchhoff solids."""
327 def __init__(self, youngs_modulus=None, nu=None, density=None):
328 if youngs_modulus is None or nu is None:
329 raise ValueError(
330 "Young's modulus and Poisson's ratio must be provided "
331 "for StVenant Kirchhoff solid materials."
332 )
333 data = {"YOUNG": youngs_modulus, "NUE": nu}
334 if density is not None:
335 data["DENS"] = density
336 super().__init__(
337 material_string="MAT_Struct_StVenantKirchhoff",
338 data=data,
339 )