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

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

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

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_material_and_all_contained_sub_materials( 

68 item, _visited_materials 

69 ) 

70 ) 

71 

72 return contained_materials 

73 

74 

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. 

79 

80 Args: 

81 materials: A list of all materials in the mesh. 

82 

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 

97 

98 

99class MaterialReissner(_MaterialBeamBase): 

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

101 

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" 

117 

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

119 

120 # Shear factor for Reissner beam. 

121 self.shear_correction = shear_correction 

122 

123 self.by_modes = by_modes 

124 

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 

130 

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 ) 

143 

144 def dump_to_list(self): 

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

146 

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 ) 

151 

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 ) 

175 

176 if self.by_modes: 

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

178 

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 } 

193 

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 } 

205 

206 if self.interaction_radius is not None: 

207 data["INTERACTIONRADIUS"] = self.interaction_radius 

208 

209 return {"MAT": self, self.material_string: data} 

210 

211 

212class MaterialReissnerElastoplastic(MaterialReissner): 

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

214 

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" 

225 

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 ) 

231 

232 self.yield_moment = yield_moment 

233 self.isohardening_modulus_moment = isohardening_modulus_moment 

234 self.torsion_plasticity = torsion_plasticity 

235 

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 

244 

245 

246class MaterialKirchhoff(_MaterialBeamBase): 

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

248 

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

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

251 self.is_fad = is_fad 

252 

253 def dump_to_list(self): 

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

255 

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 ) 

260 

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} 

297 

298 

299class MaterialEulerBernoulli(_MaterialBeamBase): 

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

301 

302 def __init__(self, **kwargs): 

303 super().__init__( 

304 material_string="MAT_BeamKirchhoffTorsionFreeElastHyper", **kwargs 

305 ) 

306 

307 def dump_to_list(self): 

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

309 

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 ) 

314 

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} 

334 

335 

336class MaterialSolid(_MaterialSolidBase): 

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

338 

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) 

343 

344 def dump_to_list(self): 

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

346 

347 return {"MAT": self, self.material_string: self.data} 

348 

349 

350class MaterialStVenantKirchhoff(MaterialSolid): 

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

352 

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 )