Coverage for pyxfem/solidmechanics/mechanics_solver.py: 0%

247 statements  

« prev     ^ index     » next       coverage.py v7.17.0, created at 2026-10-24 07:17 +0000

1#!/usr/bin/env python3 

2# -*- coding: utf-8 -*- 

3""" 

4Created on Tue Feb 27 07:43:08 2024 

5 

6@author: nchevaug 

7""" 

8from __future__ import annotations 

9import os 

10 

11# # Set the maximum number of threads for OpenMP and related backends 

12os.environ["OMP_NUM_THREADS"] = "8" # Change '4' to your desired core count 

13os.environ["MKL_NUM_THREADS"] = "8" 

14os.environ["OPENBLAS_NUM_THREADS"] = "8" 

15 

16from warnings import warn 

17import time 

18import unittest 

19import numpy as np 

20import scipy as sp 

21try: 

22 import sksparse.cholmod # pylint: disable=E0401 

23 have_cholmod = True 

24except ModuleNotFoundError: 

25 have_cholmod = False 

26 warn('cholmod not installed, using default scipy spsolve') 

27 

28import gmsh 

29 

30import pyxfem.core.function_spaces as fs 

31import pyxfem.core.quadrature as quad 

32import pyxfem.core.sparse_tools as st 

33from pyxfem.core.mesh_simplex_one_level import SMesh, gmshmodel2smesh 

34import pyxfem.postpro.gmsh_post_pro_interface as gp 

35from pyxfem.core.exec_tools import Profile 

36import pyxfem.core.gmsh_mesh_generator as mg 

37from pyxfem.solidmechanics.boundary_conditions import compute_dirichlet, compute_neumann, \ 

38 get_dofs_and_interpolation_points, compute_dirichlet_fem_p1_vect2d 

39from pyxfem.solidmechanics.elasticity import planeStrainC2222CSR, YoungPoisson2LambdaMu 

40 

41def getSpaceEntity(edgeGroupeNames, space): 

42 ''' get the space's edge id corresponding to the edgeGroupe Name''' 

43 edgeGroupeNames2EId = {} 

44 for name in edgeGroupeNames: 

45 edgeGroupeNames2EId[name] = space.m.getEdgeGroupe(name)[0] 

46 return edgeGroupeNames2EId 

47 

48def computeVolumeLoad(space, source): 

49 """ compute the vector F such as F.U = int_Omega f.u_h dOmega_h """ 

50 assert isinstance(source, np.ndarray) 

51 quadSource = quad.T3_gauss(0) 

52 N = space.operator_dof2val_tri(quadSource.uv) 

53 J = space.get_tris_mapping().J 

54 wSource = quadSource.w[0]*source[np.newaxis, :]*J[:, np.newaxis] 

55 Fint = N.T.dot(wSource.flatten()) 

56 return Fint 

57 

58class ElasticitySolver(): 

59 """ A class to set up and compute a 2d linear elasticity problem with FEM method """ 

60 def __init__(self, mesh:SMesh, C=planeStrainC2222CSR(lam =12., mu =7.), 

61 node_id_to_imposed_value:None|dict=None, 

62 physName2Dirichlet:None|dict=None, 

63 physName2Neumann:None|dict=None, 

64 source=np.array([0., 0.]), 

65 space_constructor:type[fs.FEMSpaceP1]|type[fs.FEMSpaceP1NC]=fs.FEMSpaceP1, 

66 stab=0.): 

67 self.quadBilinForm = quad.T3_gauss(0) 

68 self.quadAtCorners = quad.T3_nodes() 

69 self.C = C 

70 self.stab = stab 

71 self.mesh = mesh 

72 self.setModel(space_constructor) 

73 if node_id_to_imposed_value is not None : 

74 if physName2Dirichlet is not None : 

75 warn("can't impose dirichlet BC both from physName and list of nodes, ignoring physName") 

76 if space_constructor != fs.FEMSpaceP1 : 

77 warn("imposing dirichlet at node value make sens only for p1 fem space. Expect errors ...") 

78 nv = mesh.nv() 

79 idvx = node_id_to_imposed_value.get("idvx", np.array([], dtype = np.int64)) 

80 idvy = node_id_to_imposed_value.get("idvy", np.array([], dtype = np.int64)) 

81 vx = node_id_to_imposed_value.get("vx", np.array([], dtype = np.int64)) 

82 vy = node_id_to_imposed_value.get("vy", np.array([], dtype = np.int64)) 

83 self.freeDofs, self.fixedDofs, self.fixedValues = \ 

84 compute_dirichlet_fem_p1_vect2d(nvertex=nv, nodex= idvx, valuex= vx, nodey = idvy, valuey=vy) 

85 else: 

86 self.physName2Dirichlet:dict = {} if physName2Dirichlet is None else physName2Dirichlet 

87 gname2dof, gname2dofxy = get_dofs_and_interpolation_points(mesh, 

88 self.physName2Dirichlet.keys(), self.space) 

89 self.freeDofs, self.fixedDofs, self.fixedValues = \ 

90 compute_dirichlet(self.space, self.physName2Dirichlet, gname2dof, gname2dofxy) 

91 self.physName2Neumann:dict = {} if physName2Neumann is None else physName2Neumann 

92 self.Fneumann = compute_neumann(self.space, self.physName2Neumann) 

93 self.Fint = computeVolumeLoad(self.space, source) 

94 self.Bfree = self.B[:, self.freeDofs] 

95 self.Bfixed = self.B[:, self.fixedDofs] 

96 nbfixed = len(self.fixedDofs) 

97 if nbfixed < 3: 

98 warn(f' Only {nbfixed:d} fixed dofs: not enough to prevent rigid-body motion in {compute_dirichlet.__name__}') 

99 if isinstance(self.space, fs.FEMSpaceP1NC): 

100 self.setStabilisation(self.stab, quad.Edge_gauss(2)) 

101 def setD(self): 

102 ''' set the sparse stifness operator (K = BtDB)''' 

103 J = self.space.get_tris_mapping().J 

104 nf = len(J) 

105 C = self.C.todense().flatten() 

106 wD = np.einsum("i, ij -> ij", self.quadBilinForm.w[0]*J, 

107 C[np.newaxis, :]).reshape((-1, 4, 4)) 

108 self.wD = st.regular_block_diag_to_csr(wD) 

109 D = np.broadcast_to(C, (nf, 16)).reshape((-1, 4, 4)) 

110 self.D = st.regular_block_diag_to_csr(D) 

111 def setModel(self, spaceConstructor:type[fs.FEMSpaceP1]|type[fs.FEMSpaceP1NC]): 

112 self.space = spaceConstructor("DisplacementSpace", self.mesh, vecdim=2) 

113 self.B = self.space.operator_dof2grad_tri(self.quadBilinForm.uv) 

114 self.NatVertices = self.space.operator_dof2val_tri(self.quadAtCorners.uv) 

115 self.setD() 

116 def setStabilisation(self, s, quadEdge): 

117 if not isinstance(self.space, fs.FEMSpaceP1NC): 

118 raise ValueError("Stabilisation term only implemented for P1NC") 

119 space = self.space 

120 lE = space.m.elength() 

121 sTri2edge = space.operator_trival2edge().dot(0.5*space.get_tris_mapping().J).reshape((-1, 2)) 

122 hE = np.sum(sTri2edge, axis=1)/2/lE 

123 N = space.operator_edgetrace(quadEdge) 

124 self.jumpOp = N[1::2] - N[:-1:2] 

125 weights = s*((lE/hE)[:, np.newaxis] * quadEdge.w).reshape(-1) 

126 DjumpBlock = (weights[:, np.newaxis]*np.eye(2).flatten()[np.newaxis, :]).reshape(-1, 2, 2) 

127 self.Djump = st.regular_block_diag_to_csr(DjumpBlock) 

128 internalEdges = self.space.m.intern_edges() 

129 vecDim = self.space.vecdim 

130 nptPerEdge = quadEdge.s.size 

131 rowsPerEdge = vecDim*nptPerEdge 

132 internalEdgesDofs = (vecDim*internalEdges[:, np.newaxis] 

133 +np.arange(vecDim)[np.newaxis, : ]).flatten() 

134 stabDofs = np.concatenate([internalEdgesDofs, self.fixedDofs]) 

135 stabRows = np.sort(((rowsPerEdge*(stabDofs//vecDim) 

136 +stabDofs%vecDim)[:, np.newaxis] 

137 +nptPerEdge*np.arange(vecDim)[np.newaxis, :]).flatten()) 

138 Nedges = self.jumpOp[stabRows, :] 

139 self.NedgesFree = Nedges[:, self.freeDofs] 

140 self.NedgesFixed = Nedges[:, self.fixedDofs] 

141 self.Djump = self.Djump[stabRows, :][:, stabRows] 

142 self.SFree = self.NedgesFree.T.dot(self.Djump.dot(self.NedgesFree)) 

143 

144 def assembleK(self): 

145 if not hasattr(self, "Kfree"): 

146 Bfree = self.Bfree 

147 self.Kfree = Bfree.T.dot(self.wD).dot(Bfree) 

148 if isinstance(self.space, fs.FEMSpaceP1NC): 

149 self.Kfree = self.Kfree + self.SFree 

150 return self.Kfree 

151 

152 def assembleFU(self): 

153 Fneumann = self.Fneumann 

154 U = np.zeros(self.space.size()) 

155 U[self.fixedDofs] = self.fixedValues 

156 Bfree = self.Bfree 

157 Bfixed = self.Bfixed 

158 gradUdiri = Bfixed.dot(self.fixedValues) 

159 Fdiri = -Bfree.T.dot(self.wD.dot(gradUdiri)) 

160 F = Fdiri + self.Fneumann[self.freeDofs] + self.Fint[self.freeDofs] 

161 if isinstance(self.space, fs.FEMSpaceP1NC): 

162 SDiri = -self.NedgesFree.T.dot(self.Djump.dot(self.NedgesFixed.dot(self.fixedValues))) 

163 F = F+SDiri 

164 return F,U 

165 

166 def assemble(self): 

167 Kfree = self.assembleK() 

168 F,U = self.assembleFU() 

169 return Kfree, F, U 

170 # def assembleMass(self): 

171 

172 def solve(self, solver="default"): 

173 """ solve the problem using the solver to choose between "default" : spsolve or "cholmod" : "sksparse.cholmod.cholesky" """ 

174 Kfree, F, U = self.assemble() 

175 def lin_solve(): 

176 if solver == "default" or not have_cholmod: 

177 U[self.freeDofs] = sp.sparse.linalg.spsolve(Kfree, F, use_umfpack=False) 

178 elif solver == "umfpack": 

179 U[self.freeDofs] = sp.sparse.linalg.spsolve(Kfree, F) 

180 elif solver == "cholmod": 

181 print("cholmod solver") 

182 if not have_cholmod : 

183 raise ValueError 

184 chol = sksparse.cholmod.cho_factor(Kfree) 

185 U[self.freeDofs] = chol.solve(F) 

186 lin_solve() 

187 # if compute_rcond: 

188 # diag = chol.D() 

189 # rcond = (np.min(diag)/np.max(diag))**2 

190 return U 

191 def postProDisplacement(self, U): 

192 return self.mesh.t2v_coord(), self.NatVertices.dot(U).reshape((-1, 3, 2)) 

193 def postProStress(self, U): 

194 return self.mesh.t2v_coord(), self.getStress(U) 

195 def getStress(self, U): 

196 ''' return per element stress /per gp stress ''' 

197 return self.D.dot(self.B.dot(U)).reshape((-1, 2, 2)) 

198 def postProStrain(self, U): 

199 return self.mesh.t2v_coord(), self.B.dot(U).reshape((-1, 2, 2)) 

200 def L2StressError(self, U, exactStressFun, quadrature=quad.T3_gauss(0)): 

201 uv = quadrature.uv 

202 weight = quadrature.w 

203 npt = len(uv) 

204 B = self.space.operator_dof2grad_tri(uv) 

205 gradNum = B.dot(U).reshape((-1, npt, 2, 2)) 

206 C = self.C.todense().reshape((1,1,2,2,2,2)) 

207 stressNum = np.einsum('egijkl, egkl-> egij', C, gradNum) 

208 NP1 = fs.FEMSpaceP1.N(uv) 

209 trisQuadPoints = np.einsum('ijk, ikl-> ijl', NP1[np.newaxis, :, :], self.mesh.t2v_coord()) 

210 stressExact = exactStressFun(trisQuadPoints) 

211 stressError2 = np.sum((stressNum - stressExact)**2, axis = (2,3)) 

212 J = self.space.get_tris_mapping().J 

213 wStressError2 = J[:, np.newaxis]*weight[np.newaxis, :]*stressError2 

214 return np.sqrt(np.sum(wStressError2)) 

215 

216class testElasticity(unittest.TestCase): 

217 def testPatch(self): 

218 """ - patch test on a squared load by Dirchlet and then Neumann """ 

219 gmsh.initialize() 

220 gmsh.option.setNumber('General.Terminal', 0) 

221 L = 0.7 

222 E = 12.2 

223 nu = 0.3 

224 ux = 0.98 

225 lamb, mu = YoungPoisson2LambdaMu(E, nu) 

226 C = planeStrainC2222CSR(lam = lamb, mu =mu) 

227 phys2Dirichlet = {'left': {'x':0.}, 'bottom': {'y': 0.}, 'right': {'x': ux}} 

228 modelName, physical_dict = mg.square(length=L, relh=0.3) 

229 mesh = gmshmodel2smesh(modelName, phys2Dirichlet.keys()) 

230 pb = ElasticitySolver(mesh, C, physName2Dirichlet=phys2Dirichlet, 

231 space_constructor=fs.FEMSpaceP1) 

232 U = pb.solve() 

233 ___, stress = pb.postProStress(U) 

234 e11 = ux/L 

235 s11 = 4*mu*(lamb+mu)/(lamb+2*mu)*e11 

236 stressExact = np.array([[s11, 0.], [0.,0.]]) 

237 error = np.linalg.norm(stress - stressExact[np.newaxis, np.newaxis] ) 

238 self.assertTrue(error < 1.e-9) 

239 phys2Dirichlet = {'left': {'x':0.}, 'bottom': {'y': 0.}} 

240 phys2Neumann = {'right': {'x':s11}} 

241 pb = ElasticitySolver(mesh, C, physName2Dirichlet=phys2Dirichlet, physName2Neumann=phys2Neumann, 

242 space_constructor=fs.FEMSpaceP1) 

243 e22 = -lamb*e11/(lamb+2*mu) 

244 strainExact = np.array([[e11, 0.], [0., e22]]) 

245 U = pb.solve() 

246 ___, strain = pb.postProStrain(U) 

247 error = np.linalg.norm(strain - strainExact[np.newaxis, np.newaxis] ) 

248 self.assertTrue(error < 1.e-9) 

249 gmsh.finalize() 

250 

251def main(*, element_size=0.05, profile=True, solver = "default", popUpGmsh = True): 

252 """ main Program and example : elasticity on L shape domain """ 

253 E = 1. 

254 nu = 0.4999 

255 lamb, mu = YoungPoisson2LambdaMu(E, nu) 

256 meshgenerator = mg.l_shape 

257 #meshgenerator = mg.square 

258 

259 phys2Dirichlet = {'dOmega_left': {'x':0., 'y':0.}} 

260 phys2Neumann = {'dOmega_right':{'x': 0.01}} 

261 #phys2Dirichlet = {'left': {'x':0., 'y':0.}, 'right':{'x': 0.01, 'y':0.}} 

262 #phys2Neumann = {} 

263 spaceConstructors = [fs.FEMSpaceP1, fs.FEMSpaceP1NC] 

264 Profile.doprofile = profile 

265 gmsh.initialize() 

266 gmsh.option.setNumber('PostProcessing.AnimationCycle', 1) 

267 gmsh.option.setNumber('Mesh.SurfaceEdges', 1 if element_size > 0.1 else 0) 

268 gmsh.option.setNumber('General.Terminal', False) 

269 modelName, physical_dict = meshgenerator(element_size) 

270 edgeGroupeNames = list(phys2Dirichlet.keys())+ list(phys2Neumann.keys()) 

271 mesh = gmshmodel2smesh(modelName, edgeGroupeNames) 

272 nv, ne, nt = mesh.nv(), mesh.nedges(), mesh.ntris() 

273 print('nv {:d}, ne {:d}, nf {:d}'.format(nv, ne, nt)) 

274 C = planeStrainC2222CSR(lam = lamb, mu = mu) 

275 for spaceConstructor in spaceConstructors: 

276 spaceName = spaceConstructor.__name__+'_' 

277 Profile.enable() 

278 tic = time.perf_counter() 

279 pb = ElasticitySolver(mesh, C, physName2Dirichlet=phys2Dirichlet, 

280 physName2Neumann=phys2Neumann, 

281 space_constructor=spaceConstructor, 

282 stab=2*mu*0.5) 

283 

284 

285 U = pb.solve(solver) 

286 toc = time.perf_counter() 

287 print(f"assemble +solve: {toc - tic:0.4f} seconds") 

288 Profile.disable() 

289 gp.listPlotFieldTri(*pb.postProStress(U), P0=True, viewname=spaceName+'Stress') 

290 gp.listPlotFieldTri(*pb.postProStrain(U), P0=True, viewname=spaceName+'Strain') 

291 gp.listPlotFieldTri(*pb.postProDisplacement(U), P0=False, 

292 viewname=spaceName+'Displacement', VectorType="Displacement", 

293 DisplacementFactor=1.) 

294 

295 stress = pb.postProStress(U)[1] 

296 neumannEdges, neumannEdgeFlip = mesh.get_edgegroupe('dOmega_other') 

297 if neumannEdges.size: 

298 e2t = mesh.e2t() 

299 stressNeumann = stress[e2t.data[e2t.index[neumannEdges]]] 

300 normals = mesh.enormal(neumannEdges, neumannEdgeFlip) 

301 tractionNeumann = np.einsum("i...jk,i...k->i...j", stressNeumann, normals) 

302 gp.listPlotFieldLine(mesh.e2v_coord()[neumannEdges], tractionNeumann, 

303 P0=True, viewname=spaceName+'bcTraction', VectorType="Arrow") 

304 print('Max neumann norm ', np.max(np.linalg.norm(tractionNeumann.squeeze(), axis=1))) 

305 

306 if popUpGmsh: 

307 gmsh.fltk.run() 

308 gmsh.finalize() 

309 Profile.print_stats() 

310 

311if __name__ == '__main__': 

312 #example running just one of the unittest : 

313 # import unittest 

314 

315 # # Create a test suite and add your specific test method 

316 # suite = unittest.TestSuite() 

317 # suite.addTest(testElasticity('testPatch')) 

318 

319 # # Run the suite using the standard text test runner 

320 # runner = unittest.TextTestRunner() 

321 # runner.run(suite) 

322 

323 # for solver in ["default", "cholmod", "umfpack"] : 

324 # print(f"### run with Solver {solver}") 

325 # main(element_size=0.05, solver = solver, profile = False, popUpGmsh=False) 

326 

327 main(element_size=0.008, solver = "cholmod", profile = False, popUpGmsh=True)