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

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.""" 

23 

24import numpy as _np 

25 

26from beamme.core.material import Material as _Material 

27from beamme.core.material import MaterialBeamBase as _MaterialBeamBase 

28from beamme.core.material import MaterialSolidBase as _MaterialSolidBase 

29 

30 

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. 

36 

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. 

44 

45 Returns: 

46 A flat list containing the given material and all nested materials. 

47 

48 Raises: 

49 ValueError: 

50 If a circular material reference is detected. 

51 """ 

52 

53 if _visited_materials is None: 

54 _visited_materials = set() 

55 

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) 

60 

61 contained_materials = [material] 

62 

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 ) 

69 

70 return contained_materials 

71 

72 

73class MaterialReissner(_MaterialBeamBase): 

74 """Holds material definition for Reissner beams.""" 

75 

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" 

91 

92 super().__init__(material_string=mat_string, **kwargs) 

93 

94 # Shear factor for Reissner beam. 

95 self.shear_correction = shear_correction 

96 

97 self.by_modes = by_modes 

98 

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 

104 

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 ) 

117 

118 def dump_to_list(self): 

119 """Return a list with the (single) item representing this material.""" 

120 

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 ) 

125 

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 ) 

149 

150 if self.by_modes: 

151 shear_modulus = self.youngs_modulus / (2.0 * (1.0 + self.nu)) 

152 

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 } 

167 

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 } 

179 

180 if self.interaction_radius is not None: 

181 data["INTERACTIONRADIUS"] = self.interaction_radius 

182 

183 return {"MAT": self.i_global + 1, self.material_string: data} 

184 

185 

186class MaterialReissnerElastoplastic(MaterialReissner): 

187 """Holds elasto-plastic material definition for Reissner beams.""" 

188 

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" 

199 

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 ) 

205 

206 self.yield_moment = yield_moment 

207 self.isohardening_modulus_moment = isohardening_modulus_moment 

208 self.torsion_plasticity = torsion_plasticity 

209 

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 

218 

219 

220class MaterialKirchhoff(_MaterialBeamBase): 

221 """Holds material definition for Kirchhoff beams.""" 

222 

223 def __init__(self, is_fad=False, **kwargs): 

224 super().__init__(material_string="MAT_BeamKirchhoffElastHyper", **kwargs) 

225 self.is_fad = is_fad 

226 

227 def dump_to_list(self): 

228 """Return a list with the (single) item representing this material.""" 

229 

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 ) 

234 

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} 

271 

272 

273class MaterialEulerBernoulli(_MaterialBeamBase): 

274 """Holds material definition for Euler Bernoulli beams.""" 

275 

276 def __init__(self, **kwargs): 

277 super().__init__( 

278 material_string="MAT_BeamKirchhoffTorsionFreeElastHyper", **kwargs 

279 ) 

280 

281 def dump_to_list(self): 

282 """Return a list with the (single) item representing this material.""" 

283 

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 ) 

288 

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} 

308 

309 

310class MaterialSolid(_MaterialSolidBase): 

311 """Base class for a material for solids.""" 

312 

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) 

317 

318 def dump_to_list(self): 

319 """Return a list with the (single) item representing this material.""" 

320 

321 return {"MAT": self.i_global + 1, self.material_string: self.data} 

322 

323 

324class MaterialStVenantKirchhoff(MaterialSolid): 

325 """Holds material definition for StVenant Kirchhoff solids.""" 

326 

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 )