Coverage for pyxfem/solidmechanics/hyperelasticity.py: 55%

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

4This module is dedicated to hyperelasticity 

5Created on Thu April 2024 14:09:29 2024 

6@author: nchevaug 

7""" 

8from __future__ import annotations 

9import re 

10import time 

11from typing import Callable, Tuple, cast 

12import numpy as np 

13from numba import guvectorize, float32, float64, njit, prange 

14from scipy.special import lambertw as sp_lambertw 

15from pyxfem.core import smallsize_tensor 

16from pyxfem.core.config import FloatArray, FloatVars, npfloat 

17from pyxfem.core.smallsize_tensor import det_invt, cross_ijkl, df_num,\ 

18 random_lintransform, random_lintransform2dto3d,\ 

19 metric, det, trace, trace2, inv, invt, dinvt, check_relerr, \ 

20 Sy4, Sp4 

21from pyxfem.core.exec_tools import Profile 

22from pyxfem.solidmechanics.elasticity import MaterialHypothesis 

23#import pytest 

24 

25#from sym_hyperelasticity import II 

26 

27PLANESTRAIN = MaterialHypothesis.PLANESTRAIN 

28PLANESTRESS = MaterialHypothesis.PLANESTRESS 

29DIM3 = MaterialHypothesis.DIM3 

30HYP2DIM = {PLANESTRESS:2, PLANESTRAIN:2, DIM3:3} 

31 

32import numpy as np 

33 

34def real_lambertw(z): 

35 """ 

36 Computes the principal branch of the Lambert W function for real, positive z 

37 using vectorized Halley's method. 

38 """ 

39 # A robust initial guess for z >= 0 

40 w = np.log(1.0 + z) 

41 

42 # Halley's method usually converges to machine precision in 3-4 iterations 

43 for _ in range(5): 

44 ew = np.exp(w) 

45 w1 = w + 1.0 

46 f = w * ew - z 

47 

48 # Halley step 

49 step = f / (ew * w1 - ((w + 2.0) * f) / (2.0 * w1)) 

50 w -= step 

51 

52 # Optional: break early if max error is small enough, 

53 # but for 5 fixed iterations, it's often faster to just loop. 

54 if np.max(np.abs(step)) < 1e-12: 

55 break 

56 

57 return w 

58 

59#lambertw = lambda x : np.real(sp_labertw(x)) 

60lambertw = real_lambertw 

61 

62def vecroots(f:Callable, fprime:Callable, x0:FloatVars, 

63 atol:float=1.e-20, rtol=1.e-12, maxiter:int = 100, verbose = False)->FloatVars: 

64 """ compute roots of an array of independant equations using newton's 

65 method with line search so that all x stay > 0""" 

66 r = f(x0) 

67 if verbose: 

68 print(0, x0, np.abs(r), 1.) 

69 err0 = np.max(np.abs(r)) 

70 err = err0 

71 if err0 < atol: 

72 return x0 

73 it = 0 

74 while it < maxiter: 

75 alpha=1. 

76 j = fprime(x0) 

77 s = -r/j 

78 x = x0 + s 

79 while np.any(x<=0.): 

80 alpha = 0.5*alpha 

81 x = x0 + alpha*s 

82 r = f(x) 

83 while np.max(np.abs(r)) > err: 

84 alpha = 0.5*alpha 

85 x = x0 + alpha*s 

86 r = f(x) 

87 err = np.max(np.abs(r)) 

88 if err < atol or err < rtol*err0 : 

89 return x 

90 x0 = x 

91 it += 1 

92 if verbose: 

93 print(it, x0, np.abs(r), alpha) 

94 msg = f"Failed to converge after {maxiter:d} iterations" 

95 raise RuntimeError(msg) 

96 

97class HyperElasticMaterial(): 

98 """ Virtual base class for HyperElastic material """ 

99 # pylint: disable=C0103 

100 # pylint: disable=W2301 

101 def __init__(self, hyp:MaterialHypothesis=DIM3, rho0 = 1.): 

102 self.hyp, self.rho = hyp, rho0 

103 def potentialF(self, F:FloatArray )-> FloatVars: 

104 """ Compute the Elastic Potential as a Function of F""" 

105 raise NotImplementedError() 

106 def potentialC(self, C:FloatArray )-> FloatVars: 

107 """ Compute the Elastic Potential as a Function of C""" 

108 raise NotImplementedError() 

109 def PKI(self, F:FloatArray)->FloatArray: 

110 """ Compute Piola Kirchhoff I Stress tensor as a function of F """ 

111 raise NotImplementedError() 

112 def PKII(self, C:FloatArray)->FloatArray: 

113 """ Compute Piola Kirchhoff I Stress tensor as a function of C = F^TF """ 

114 raise NotImplementedError() 

115 def Cauchy(self, b:FloatArray)->FloatArray: 

116 """ Compute Cauchy Stress tensor as a function of b = FF^T """ 

117 raise NotImplementedError() 

118 def dPKIdF(self, F:FloatArray)->FloatArray: 

119 """ Compute the deriviatives of the Piola Kirchhoff I Stress tensor 

120 with regard to F 

121 """ 

122 raise NotImplementedError() 

123 def dPKIIdC(self, C:FloatArray)->FloatArray: 

124 """ Compute the deriviatives of the Piola Kirchhoff II Stress tensor 

125 with regard to C 

126 """ 

127 raise NotImplementedError() 

128 

129 def dPKIIdE(self, C:FloatArray)->FloatArray: 

130 """ Compute the deriviatives of the Piola Kirchhoff II Stress tensor 

131 with regard to C 

132 """ 

133 return 2*self.dPKIIdC(C) 

134 

135 def spacial_elasticty_tensor(self, b:FloatArray)->FloatArray: 

136 """ Compute the spacial elasticity tensor (1/J FFFF dPKIIdE) 

137 """ 

138 raise NotImplementedError() 

139 

140 

141# class SimpleLaw0(HyperElasticMaterial): 

142# """ A very simple hyper elastic law. Mostly to check derivatives of I_C """ 

143# # pylint: disable=C0103 

144# def __init__(self): 

145# super().__init__(PLANESTRAIN) 

146# def potential(self, F:FloatArray)-> FloatArray|float: 

147# m = metric(F) 

148# I_C = 1.+trace(m) 

149# return 0.5*I_C 

150# def PKI(self, F:FloatArray)->FloatArray: 

151# return F 

152# def dPKIdF(self, F:FloatArray)->FloatArray: 

153# tshape = F.shape[-2:] 

154# nbfirstaxis = F.ndim-2 

155# I40 = np.eye(tshape[0]*tshape[1]).reshape(tshape+tshape) 

156# return I40[(np.newaxis,)*nbfirstaxis] 

157 

158# class SimpleLaw1(HyperElasticMaterial): 

159# """ A very simple hyper elastic law. Mostly to check derivatives of log(J) """ 

160# # pylint: disable=C0103 

161# def __init__(self): 

162# super().__init__(PLANESTRAIN) 

163# def potential(self, F:FloatArray)-> FloatArray|float: 

164# return 0.5*np.log(det(metric(F))) 

165 

166# def PKI(self, F:FloatArray)->FloatArray: 

167# return invt(F) 

168 

169# def dPKIdF(self, F:FloatArray)->FloatArray: 

170# return dinvt(F) 

171 

172# class SimpleLaw2(HyperElasticMaterial): 

173# """ A very simple hyper elastic law. Mostly to check derivatives of J """ 

174# # pylint: disable=C0103 

175# def __init__(self): 

176# super().__init__(PLANESTRAIN) 

177# def potential(self, F:FloatArray)-> FloatArray|float: 

178# return np.sqrt(det(metric(F))) 

179 

180# def PKI(self, F:FloatArray)->FloatArray: 

181# J, invFT = det_invt(F) 

182# return np.atleast_1d(J)[..., np.newaxis, np.newaxis] * invFT 

183 

184# def dPKIdF(self, F:FloatArray)->FloatArray: 

185# J, invFT = det_invt(F) 

186# J_11 = np.atleast_1d(J)[..., np.newaxis, np.newaxis] 

187# PKI = J_11 * invFT 

188# J_1111 = J_11[..., np.newaxis, np.newaxis] 

189# return cross_ijkl(PKI, invFT) + J_1111*dinvt(F) 

190 

191@njit(inline='always', fastmath=True) 

192def fill_nh_sigma_2d(J, b00, b11, b01, mu, lamb, out_sigma): 

193 """ Remplit la contrainte de Cauchy 2D en place """ 

194 S_mu = mu / J 

195 S_lamb = (lamb * np.log(J)) / J 

196 

197 out_sigma[0, 0] = S_mu * (b00 - 1.0) + S_lamb 

198 out_sigma[1, 1] = S_mu * (b11 - 1.0) + S_lamb 

199 out_sigma[0, 1] = S_mu * b01 

200 out_sigma[1, 0] = out_sigma[0, 1] 

201 

202@njit(inline='always', fastmath=True) 

203def fill_nh_c_2d(J, mu, lamb, out_c): 

204 """ Remplit le tenseur d'élasticité spatial 2x2x2x2 en place """ 

205 S1 = lamb / J 

206 S2 = 2.0 * (mu - lamb * np.log(J)) / J 

207 S2_half = 0.5 * S2 

208 

209 out_c.fill(0.0) 

210 out_c[0, 0, 0, 0] = S1 + S2 

211 out_c[1, 1, 1, 1] = S1 + S2 

212 out_c[0, 0, 1, 1] = S1 

213 out_c[1, 1, 0, 0] = S1 

214 out_c[0, 1, 0, 1] = S2_half 

215 out_c[0, 1, 1, 0] = S2_half 

216 out_c[1, 0, 0, 1] = S2_half 

217 out_c[1, 0, 1, 0] = S2_half 

218 

219 

220@guvectorize([ 

221 (float64[:,:], float64, float64, float64[:,:,:,:]), 

222 (float32[:,:], float32, float32, float32[:,:,:,:]) 

223], '(m,m),(),()->(m,m,m,m)', nopython=True, cache=True) 

224def _NH_spacial_elasticity_plane_strain_guvec(b, lamb, mu, out): 

225 det_b = b[0, 0] * b[1, 1] - b[0, 1] * b[1, 0] 

226 J = np.sqrt(det_b) 

227 fill_nh_c_2d(J, mu, lamb, out) 

228 

229@guvectorize([ 

230 ('float64[:,:], float64, float64, float64[:,:]'), 

231 ('float32[:,:], float32, float32, float32[:,:]') 

232], '(m,m),(),()->(m,m)', nopython=True, cache=True) 

233def _NH_cauchy_plane_strain_guvec(b, lamb, mu, out_sigma): 

234 """ 

235 Calcule la contrainte de Cauchy 2D (Déformation Plane) pour un 

236 matériau Néo-Hookéen Compressible. 

237 

238 Inputs: 

239 b : Tenseur de Cauchy-Green gauche (2x2) 

240 lamb : Paramètre de Lamé lambda (scalaire) 

241 mu : Module de cisaillement mu (scalaire) 

242 Output: 

243 out_sigma : Tenseur des contraintes de Cauchy (2x2) 

244 """ 

245 # 1. Calcul du déterminant et du Jacobien (J = sqrt(det(b))) 

246 det_b = b[0, 0] * b[1, 1] - b[0, 1] * b[1, 0] 

247 J = np.sqrt(det_b) 

248 fill_nh_sigma_2d(J, b[0,0], b[1,1], b[0,1], mu, lamb, out_sigma) 

249 

250 

251@njit(inline='always', fastmath=True, cache=True) 

252def neo_hookean_law_local(F, props, out_sigma, out_c): 

253 """ 

254 Calcule la contrainte de Cauchy et le tenseur d'élasticité spatial 2D. 

255 Zéro allocation, remplissage en place. 

256 """ 

257 mu = props[0] 

258 lamb = props[1] 

259 # --- Cinématique --- 

260 # J = det(F) 

261 detF = F[0, 0] * F[1, 1] - F[0, 1] * F[1, 0] 

262 J = detF 

263 # b = F * F^T (Tenseur de Cauchy-Green gauche) 

264 b00 = F[0, 0]*F[0, 0] + F[0, 1]*F[0, 1] 

265 b01 = F[0, 0]*F[1, 0] + F[0, 1]*F[1, 1] 

266 b11 = F[1, 0]*F[1, 0] + F[1, 1]*F[1, 1] 

267 fill_nh_sigma_2d(J, b00, b11, b01, mu, lamb, out_sigma) 

268 fill_nh_c_2d(J, mu, lamb, out_c) 

269 

270class CompressibleNeoHookeen(HyperElasticMaterial): 

271 """ Implementation of the NeoHookeen Hyperelastic material """ 

272 # pylint: disable=C0103 

273 def __init__(self,*, # pylint: disable=R0913 

274 E:float|None=None, 

275 nu:float|None=None, 

276 lamb:float|None=None, 

277 mu:float|None=None, 

278 rho0:float|None = None, 

279 hyp:MaterialHypothesis=DIM3): 

280 super().__init__(hyp) 

281 if isinstance(E, float) and isinstance(nu, float): 

282 self.E = E 

283 self.nu = nu 

284 self.lamb = E*nu/(1.+nu)/(1.-2*nu) 

285 self.mu = E/2./(1+nu) 

286 elif isinstance(lamb, float) and isinstance(mu, float): 

287 self.lamb = lamb 

288 self.mu = mu 

289 self.E = mu*(3.0*lamb + 2.0*mu)/(lamb + mu) 

290 self.nu = 0.5*lamb/(lamb + mu) 

291 else: 

292 raise ValueError("E and nu or lambda and mu must be set (exclusive)") 

293 self.rho0 = rho0 

294 

295 def _checktensorsize(self, F:FloatArray): 

296 if F.shape[-2:] == (3,3) and self.hyp == DIM3: 

297 return 

298 if F.shape[-2:] in [(2,2), (3,2)]: 

299 if self.hyp in [PLANESTRAIN, PLANESTRESS]: 

300 return 

301 raise ValueError("invalid shape for F. F.shape must be (...2,2) or (...,3,3)or (..., 3,2)") 

302 

303 def l3_planestress(self, J2d:FloatVars)->FloatVars: 

304 """ return the principal elongation lambda_3 in the direction orthogonal to the plane """ 

305 lamb, mu = (self.lamb, self.mu) 

306 a = mu 

307 b = lamb/2 

308 c = lamb*np.log(J2d) -mu 

309 #return np.sqrt(b*np.real(lambertw( a* np.exp(-c/b)/b)) /a) 

310 return np.sqrt(b*lambertw( a* np.exp(-c/b)/b) /a) 

311 

312 # def resl3(l3): 

313 # return lamb*np.log(J2d*l3) + mu*(l3*l3 - 1) 

314 # def jacl3(l3): 

315 # return lamb/l3 + 2*mu*l3 

316 # if isinstance(J2d, float) or isinstance(J2d, int): 

317 # x0 = 1. 

318 # else: 

319 # x0 = np.ones(J2d.shape) 

320 # l3 = vecroots(resl3, jacl3, x0, atol=1.e-20, rtol=1.e-12, maxiter=100) 

321 #return l3 

322 

323 # def _dl3dF2d_planestress(self, l3, invFT2d): 

324 # lamb = self.lamb 

325 # mu = self.mu 

326 # return -l3/(1+2*mu*l3**2/lamb)*invFT2d 

327 

328 # def dl3F2d_planestress(self, F2d): 

329 # """ Compute the derivative of the third principal strain as a fonction of 

330 # the plane part of F (Assuming Plan Stress)""" 

331 # J2d, invFT2d = det_invt(F2d) 

332 # l3 = self.l3_planestress(J2d) 

333 # return self._dl3dF2d_planestress(l3, invFT2d) 

334 

335 def _potential(self, J:FloatVars, I_C:FloatVars)-> FloatVars: 

336 logJ = np.log(J) 

337 return self.mu*(0.5*(I_C-3)- logJ) + self.lamb*logJ**2/2 

338 

339 def _potential_3d(self, F:FloatArray)-> FloatVars: 

340 J = det(F) 

341 I_C = trace2(F) 

342 return self._potential(J, I_C) 

343 

344 def _potential_planestrain(self, F2d:FloatArray)-> FloatVars: 

345 if F2d.shape[-2:] == (3,2): 

346 m = metric(F2d) 

347 J = np.sqrt(det(m)) 

348 I_C = 1.+trace(m) 

349 elif F2d.shape[-2:] == (2,2): 

350 J = det(F2d) 

351 I_C = trace2(F2d) +1 

352 else : 

353 raise ValueError('_potential_planestrain, F2d must be of shape (...,3,2) or (...,2,2)') 

354 return self._potential(J, I_C) 

355 

356 def _potential_planestress(self, F2d:FloatArray)-> FloatVars: 

357 if F2d.shape[-2:] == (3,2): 

358 m = metric(F2d) 

359 J2d = np.sqrt(det(m)) 

360 I_C2d = trace(m) 

361 elif F2d.shape[-2:] == (2,2): 

362 J2d = det(F2d) 

363 I_C2d = trace2(F2d) 

364 else : 

365 raise ValueError('_potential_planestress, F2d must be of shape (...,3,2) or (...,2,2)') 

366 l3 = self.l3_planestress(J2d) 

367 J = l3*J2d 

368 I_C = l3**2+I_C2d 

369 return self._potential(J, I_C) 

370 

371 def _PKI_3d(self, F:FloatArray)->FloatArray: 

372 '''Compute Piola Kirchhoff I Stress tensor as a function of F: 3D case (and Plane Strain)''' 

373 if F.shape[-2:] not in [(3,3), (3,2), (2,2)]: 

374 raise ValueError("F must be a nd.array of shape[-2:] in [(3,3), (3,2), (2,2)") 

375 J, invFT = det_invt(F) 

376 #print("### ", J, F, invFT) 

377 return self._PKI_exp(J, F, invFT) 

378 

379 def _PKI_exp_v0(self, J, F, invFT)->FloatArray: 

380 ''' Compute Piola Kirchhoff I Stress tensor as a function of J, F, invFT''' 

381 return self.mu*(F-invFT) + self.lamb*np.log(J)[..., np.newaxis, np.newaxis]*invFT 

382 

383 def _PKI_exp(self, J, F, invFT) -> FloatArray: 

384 ''' Compute Piola Kirchhoff I Stress tensor as a function of J, F, invFT''' 

385 # 1. Factor out the scalar coefficients to eliminate array subtractions 

386 coef = (self.lamb * np.log(J) - self.mu)[..., np.newaxis, np.newaxis] 

387 # 2. Allocate exact output memory once 

388 ret = np.empty_like(F) 

389 # 3. Compute in-place to bypass temporary array copies 

390 np.multiply(F, self.mu, out=ret) 

391 ret += coef * invFT 

392 return ret 

393 

394 def _PKI_planestress(self, F2d:FloatArray)->FloatArray: 

395 ''' Compute Piola-Kirchhoff I Stress tensor as a function of F : plane stress case)''' 

396 if F2d.shape[-2:] not in[(2,2), (3,2)]: 

397 raise ValueError("F must be a nd.array of shape (...,3,3)") 

398 J2d, invFT2d = det_invt(F2d) 

399 if np.any(J2d<=0.): 

400 raise ValueError("det(F2d) must be > 0.") 

401 l3 = self.l3_planestress(J2d) 

402 J = l3*J2d 

403 return self._PKI_exp(J, F2d, invFT2d) 

404 

405 

406 

407 def _dPKIdF_exp_v0(self, c0, c1, c2, invFT)->FloatArray: 

408 c0 = np.atleast_1d(c0)[(...,) + (np.newaxis,)*4] 

409 c1 = np.atleast_1d(c1)[(...,) + (np.newaxis,)*4] 

410 c2 = np.atleast_1d(c2)[(...,) + (np.newaxis,)*4] 

411 tshape = invFT.shape[-2:] 

412 I4_ = np.eye(tshape[0]*tshape[1]).reshape(tshape+tshape) 

413 I4_ = I4_.reshape((1,)*(invFT.ndim-2)+I4_.shape, copy=False) 

414 c0_I4 = c0*I4_ 

415 invFTxinvFT = cross_ijkl(invFT, invFT) # iFT x d(log(J)) = iFTxiFT 

416 c2_invFTxinvFT = c2*invFTxinvFT 

417 c1_dinvFTdF = -c1*(invFTxinvFT.transpose(list(range(invFTxinvFT.ndim-4)) + [-4,-1,-2,-3])) 

418 #c1_dinvFTdF = c1*dinvt(invFT, invFT) 

419 ret = c0_I4 + c1_dinvFTdF +c2_invFTxinvFT 

420 if invFT.shape[:-2] == (): 

421 return ret.squeeze() 

422 return ret 

423 

424 def _dPKIdF_exp_v1(self, c0, c1, c2, invFT) -> FloatArray: 

425 # 1. Expand scalars/arrays for 4D tensor broadcasting 

426 c0 = np.atleast_1d(c0)[..., None, None, None, None] 

427 c1 = np.atleast_1d(c1)[..., None, None, None, None] 

428 c2 = np.atleast_1d(c2)[..., None, None, None, None] 

429 

430 # 2. Build the 4th-order identity tensor I4 

431 d = invFT.shape[-1] 

432 I4 = np.eye(d * d).reshape((d, d, d, d)) 

433 

434 # 3. Allocate full memory block to match the number of quadrature points 

435 full_shape = invFT.shape[:-2] + (d, d, d, d) 

436 ret = np.empty(full_shape, dtype=invFT.dtype) 

437 ret[...] = c0 * I4 

438 

439 # 4. Compute and subtract the c1 * dinvFTdF term in-place 

440 ret -= c1 * (invFT[..., :, None, None, :] * invFT.swapaxes(-1, -2)[..., None, :, :, None]) 

441 

442 # 5. Compute and add the c2 * invFTxinvFT term in-place 

443 ret += c2 * (invFT[..., :, :, None, None] * invFT[..., None, None, :, :]) 

444 

445 if invFT.ndim == 2: 

446 return ret.squeeze() 

447 return ret 

448 

449 def _dPKIdF_exp_v2(self, F)->FloatArray: 

450 mu, lamb = self.mu, self.lamb 

451 tshape = F.shape[-2:] 

452 I4_ = np.eye(tshape[0]*tshape[1]).reshape(tshape+tshape) 

453 I4_.shape = (1,)*(F.ndim-2)+I4_.shape 

454 J, iFT = det_invt(F) 

455 J_1111 = np.atleast_1d(J)[(...,) + (np.newaxis,)*4] 

456 return mu*I4_ + lamb*cross_ijkl(iFT, iFT) + (lamb*np.log(J_1111)-mu)*dinvt(F) 

457 

458 def _dPKIdF_exp_v3(self, c0, c1, c2, invFT) -> FloatArray: 

459 d = invFT.shape[-1] 

460 I4 = np.eye(d * d).reshape((d, d, d, d)) 

461 

462 # Expand constants for broadcasting 

463 c0_b = np.atleast_1d(c0)[..., None, None, None, None] 

464 c1_b = np.atleast_1d(c1)[..., None, None, None, None] 

465 c2_b = np.atleast_1d(c2)[..., None, None, None, None] 

466 

467 # 1. Base allocation (c0 * I4) 

468 full_shape = invFT.shape[:-2] + (d, d, d, d) 

469 ret = np.empty(full_shape, dtype=invFT.dtype) 

470 ret[...] = c0_b * I4 

471 

472 # 2. Term: -c1 * invFT_il * invFT_jk 

473 # Directly computes the transposed term in C without copying memory 

474 ret -= c1_b * np.einsum('...il,...kj->...ijkl', invFT, invFT) 

475 

476 # 3. Term: c2 * invFT_ij * invFT_kl 

477 # Replaces cross_ijkl with direct C-loop 

478 ret += c2_b * np.einsum('...ij,...kl->...ijkl', invFT, invFT) 

479 

480 if invFT.ndim == 2: 

481 return ret.squeeze() 

482 return ret 

483 

484 @staticmethod 

485 @njit(parallel=True, fastmath=True, cache = True) 

486 def _dPKIdF_numba_core(c0, c1, c2, invFT, ret): 

487 N = invFT.shape[0] 

488 d = invFT.shape[1] 

489 

490 for g in prange(N): 

491 c0_g = c0[g] 

492 c1_g = c1[g] 

493 c2_g = c2[g] 

494 

495 for i in range(d): 

496 for j in range(d): 

497 for k in range(d): 

498 for l in range(d): 

499 val = 0.0 

500 if i == k and j == l: 

501 val += c0_g 

502 val -= c1_g * invFT[g, i, l] * invFT[g, k, j] 

503 val += c2_g * invFT[g, i, j] * invFT[g, k, l] 

504 

505 ret[g, i, j, k, l] = val 

506 

507 def _dPKIdF_exp(self, c0, c1, c2, invFT): 

508 orig_shape = invFT.shape 

509 d = orig_shape[-1] 

510 

511 if invFT.ndim > 2: 

512 N = np.prod(orig_shape[:-2], dtype=int) 

513 else: 

514 N = 1 

515 

516 invFT_flat = invFT.reshape(N, d, d) 

517 

518 c0_flat = np.broadcast_to(c0, (N,)).astype(invFT.dtype) 

519 c1_flat = np.broadcast_to(c1, (N,)).astype(invFT.dtype) 

520 c2_flat = np.broadcast_to(c2, (N,)).astype(invFT.dtype) 

521 

522 ret_flat = np.empty((N, d, d, d, d), dtype=invFT.dtype) 

523 

524 # Call the static method via self 

525 self._dPKIdF_numba_core(c0_flat, c1_flat, c2_flat, invFT_flat, ret_flat) 

526 

527 if invFT.ndim == 2: 

528 return ret_flat.squeeze() 

529 

530 return ret_flat.reshape(orig_shape[:-2] + (d, d, d, d)) 

531 

532 def potentialF(self, F:FloatArray)-> FloatVars: 

533 """ return the Hyperelastic potential as a function of F""" 

534 self._checktensorsize(F) 

535 if self.hyp == DIM3: 

536 return self._potential_3d(F) 

537 if self.hyp == PLANESTRAIN: 

538 return self._potential_planestrain(F) 

539 if self.hyp == PLANESTRESS: 

540 return self._potential_planestress(F) 

541 raise ValueError("MaterialHypothesis" + str(self.hyp) +"Not implemented") 

542 

543 def PKI(self, F:FloatArray)->FloatArray: 

544 ''' Compute Piola Kirchhoff I Stress tensor as a function of F''' 

545 if self.hyp in [PLANESTRAIN, DIM3]: 

546 return self._PKI_3d(F) 

547 return self._PKI_planestress(F) 

548 

549 def dPKIdF(self, F:FloatArray)->FloatArray: 

550 ''' Compute the deriviatives of the Piola Kirchhoff I Stress tensor 

551 with regard to F at 

552 ''' 

553 self._checktensorsize(F) 

554 J, invFT = det_invt(F) 

555 #print('invFT shap', invFT.shape) 

556 mu =self.mu 

557 lamb = self.lamb 

558 c0 = mu 

559 if self.hyp in [DIM3, PLANESTRAIN]: 

560 c1 = lamb*np.log(J)-mu 

561 c2 = lamb 

562 elif self.hyp == PLANESTRESS : 

563 J2d = J 

564 l3 = self.l3_planestress(J2d) 

565 J = l3*J2d 

566 c1 = lamb*np.log(J)-mu 

567 c2 = lamb*(1.-lamb/(lamb+2*mu*l3*l3)) 

568 else: 

569 raise ValueError(f"dPKIdF not implemented for {str(self.hyp):}") 

570 if F.shape[-2:] == (3,2): 

571 I4_ = np.eye(6).reshape((1,)*(F.ndim-2)+(3,2,3,2), copy = False) 

572 c1 = np.atleast_1d(c1)[(...,)+ (np.newaxis,)*4] 

573 c2 = np.atleast_1d(c2)[(...,)+ (np.newaxis,)*4] 

574 return c0*I4_ + c2*cross_ijkl(invFT, invFT) + c1*dinvt(F) 

575 return self._dPKIdF_exp(c0, c1, c2, invFT) 

576 

577 def C33_planestress(self, II_C2D:FloatVars) -> FloatVars: 

578 """ Return the square of the thichkess dilatation in plane stress Case, 

579 knowing the secons invariant of the planar componant of C 

580 mu(C33 -1) + kappa ln(J) = sig33 

581 """ 

582 mu, lamb= (self.mu, self.lamb) 

583 a = 0.5*lamb/mu 

584 ia = 1/a 

585 #b = np.log(II_C2D) 

586 # r0 = a*np.real(lambertw( np.exp(ia-b)*ia )) 

587 # r1 = a*np.real(lambertw( ia/II_C2D*np.exp(ia)) 

588 # print("ga", r0-r1) 

589 return a*np.real(lambertw( ia/II_C2D*np.exp(ia))) 

590 

591 def dC33dII_planestress(self, II_C2D:FloatVars, C33:FloatVars|None = None) -> FloatVars: 

592 """ Return the square of the thichkess dilatation in plane stress Case, 

593 knowing the secons invariant of the planar componant of C """ 

594 mu, lamb= (self.mu, self.lamb) 

595 

596 II = II_C2D 

597 if C33 is None : 

598 C33 = self.C33_planestress(II) 

599 a = cast(npfloat, 0.5*lamb/mu) 

600 dC33dII = -a*C33/(II*(a+C33)) 

601 return dC33dII 

602 

603 def potentialC(self, C:FloatArray) -> FloatArray: 

604 """ Compute The elastic potential as a function of C""" 

605 mu, lamb= (self.mu, self.lamb) 

606 if C.shape[-2:] == (3,3): 

607 I = trace(C) 

608 logIII = np.log(np.linalg.det(C)) 

609 elif C.shape[-2:] == (2,2): 

610 I_C2D = trace(C) 

611 II_C2D = det(C) 

612 if self.hyp == MaterialHypothesis.PLANESTRESS : 

613 C33 = self.C33_planestress(II_C2D) 

614 elif self.hyp == MaterialHypothesis.PLANESTRAIN: 

615 C33 = 1. 

616 else: 

617 raise ValueError("self.hyp must be either PLANESTRESS or PLANESTRAIN") 

618 I = I_C2D + C33 

619 logIII = np.log(II_C2D*C33) 

620 else : 

621 raise ValueError("C must be of shape (..., 2,2) or (..., 3, 3)") 

622 return mu*(I - 3 - logIII)/2. + lamb*logIII**2/8 

623 

624 def PKII(self, C:FloatArray) -> FloatArray: 

625 """ Return second Piola Kirchoff Stres as a function of C """ 

626 lamb, mu = (self.lamb, self.mu) 

627 if C.shape[-2:] == (3,3) or \ 

628 (C.shape[-2:] == (2,2) and self.hyp == MaterialHypothesis.PLANESTRAIN) : 

629 dim = C.shape[-1] 

630 III = np.linalg.det(C).reshape( C.shape[:-2] + (1,1)) 

631 logIII = np.log(III) 

632 Id = np.eye(dim).reshape( (1,)*(C.ndim-2) + (dim, dim) ) 

633 iCT = invt(C, III) 

634 

635 kappa = 0.5*lamb*logIII - mu 

636 # print("fff", mu*Id[0,2,2], kappa[0], iCT[0,2,2]) 

637 elif C.shape[-2:] == (2,2) and self.hyp == MaterialHypothesis.PLANESTRESS : 

638 Id = np.eye(2).reshape ((1,)*(C.ndim-2) + (2,2)) 

639 II_2D = det(C).reshape(C.shape[:-2]+(1,1)) 

640 iCT = invt(C, II_2D) 

641 C33 = self.C33_planestress(II_2D) 

642 logIII = np.log(C33*II_2D) 

643 kappa = mu*C33*(lamb*(logIII-1) - 2*mu )/(2*mu*C33+lamb) 

644 else : 

645 raise NotImplementedError() 

646 

647 

648 return mu*Id + kappa*iCT 

649 

650 def dPKIIdC(self, C:FloatArray) -> FloatArray: 

651 lamb, mu = (self.lamb, self.mu) 

652 iC = inv(C) 

653 #iCxiC = cross_ijkl(iC, iC) 

654 #diCdC = dinv(C, iA= iC) 

655 diCTdC = dinvt(C) 

656 iCT = smallsize_tensor.trans(iC) 

657 iCTxiCT = cross_ijkl(iCT,iCT) 

658 

659 #diCdC = dinv_sym(C, iA= iC) 

660 

661 #diCTdC = dinvt(C, iCT) 

662 if C.shape[-2:] == (3,3) or \ 

663 (C.shape[-2:] == (2,2) and self.hyp == MaterialHypothesis.PLANESTRAIN) : 

664 III = det(C).reshape( C.shape[:-2] + (1,1,1,1)) 

665 logIII = np.log(III) 

666 # kappa = 0.5*lamb*logIII- mu 

667 # IIIdkappadIII = 0.5*lamb 

668 # dkappa = IIIdkappadIII 

669 return (0.5*lamb)*iCTxiCT + (0.5*lamb*logIII- mu)*diCTdC 

670 elif C.shape[-2:] == (2,2) and self.hyp == MaterialHypothesis.PLANESTRESS: 

671 II_2D = det(C).reshape(C.shape[:-2]+(1,1,1,1)) 

672 C33 = self.C33_planestress(II_2D) 

673 III = C33*II_2D 

674 logIII = np.log(III) 

675 a = 2*mu*C33+lamb 

676 kappa = mu*C33*(lamb*(logIII-1) - 2*mu)/a 

677 II_2DxdkappadII_2D = mu*lamb*C33*( a**2 - 2*mu*lamb*(C33-1) -lamb**2*(logIII) ) /a**3 

678 dkappa = II_2DxdkappadII_2D 

679 #II_2DxdkappadII_2D = mu*lamb*C33*( a**2 - 2*mu*lamb*(C33-1) -lamb**2*(logIII) ) /a**3 

680 return dkappa*iCTxiCT + kappa*diCTdC 

681 #return dkappa*iCxiC + kappa*diCdC 

682 

683 else : 

684 raise ValueError("incorrect size for C (..., 2,2) or (..., (3,3))") 

685 

686 def Cauchy(self, b:FloatArray, out_sigma:FloatArray|None = None)->FloatArray: 

687 lamb, mu = (self.lamb, self.mu) 

688 if b.shape[-2:] == (2,2) and self.hyp == MaterialHypothesis.PLANESTRAIN: 

689 if out_sigma is None : 

690 out_sigma = np.empty(b.shape[:-2] + (2,2)) 

691 _NH_cauchy_plane_strain_guvec(b, lamb, mu, out_sigma) 

692 return out_sigma 

693 if b.shape[-2:] == (3,3): 

694 dim = b.shape[-1] 

695 J = np.sqrt(det(b).reshape( b.shape[:-2] + (1,1))) 

696 Id = np.eye(dim).reshape( (1,)*(b.ndim-2) + (dim, dim) ) 

697 elif b.shape[-2:] == (2,2) and self.hyp == MaterialHypothesis.PLANESTRESS : 

698 II_2D = det(b).reshape(b.shape[:-2]+(1,1)) 

699 Id = np.eye(2).reshape ((1,)*(b.ndim-2) + (2,2)) 

700 b33 = self.C33_planestress(II_2D) 

701 J = np.sqrt(b33*II_2D) 

702 else : 

703 raise NotImplementedError(f"Cauchy not implemented for \ 

704 {str(b.shape[-2:])} and {self.hyp.name}" ) 

705 return mu/J*(b-Id) + lamb*np.log(J)/J*Id 

706 

707 def spacial_elasticty_tensor(self, b:FloatArray, out_c: FloatArray|None = None)->FloatArray: 

708 """ 

709 Compute the spatial (Eulerian) elasticity tensor for a Compressible Neo-Hookean material. 

710 

711 This tensor (c) relates the Lie derivative of the Kirchhoff stress to the 

712 rate of deformation tensor. It is derived from the push-forward of the 

713 material elasticity tensor to the current configuration. 

714 

715 The formula used is: 

716 c = (lamb / J) * (I ⊗ I) + 2 * (mu - lamb * ln(J)) / J * Is 

717 where J = det(F) = sqrt(det(b)), I ⊗ I is the identity for the volumetric 

718 part, and Is is the symmetric fourth-order identity tensor. 

719 

720 Args: 

721 b (ndarray): Left Cauchy-Green deformation tensor (Finger tensor). 

722 Shape: (dim, dim) or (batch_dims, dim, dim). 

723 Supported dims: 3D or 2D (under Plane Strain hypothesis). 

724 

725 Returns: 

726 ndarray: Fourth-order spatial elasticity tensor. 

727 Shape: (dim, dim, dim, dim) or (batch_dims, dim, dim, dim, dim). 

728 

729 Raises: 

730 NotImplementedError: If the dimensions are not (3,3) or if (2,2) is 

731 provided without Plane Strain hypothesis. 

732 """ 

733 lamb, mu = (self.lamb, self.mu) 

734 if (b.shape[-2:] == (2,2) and self.hyp == MaterialHypothesis.PLANESTRAIN) : 

735 if out_c is None: 

736 batch_shape = b.shape[:-2] 

737 out_c = np.empty(batch_shape + (2,2,2,2), dtype=b.dtype) 

738 _NH_spacial_elasticity_plane_strain_guvec(b, self.lamb, self.mu, \ 

739 out=out_c) # type: ignore 

740 return out_c 

741 if b.shape[-2:] == (3,3): 

742 nbtens = b.shape[:-2] 

743 bcshape = (1,)*len(nbtens) 

744 J = np.sqrt(det(b)).reshape((-1,1,1,1,1)) 

745 IxI = Sp4[3].reshape(bcshape+(3,3,3,3)) 

746 Is = Sy4[3].reshape(bcshape+(3,3,3,3)) 

747 c = lamb/J*IxI + 2*(mu-lamb*np.log(J))/J*Is 

748 if b.ndim == 2 : 

749 return c.squeeze() 

750 return c 

751 else : 

752 raise NotImplementedError() 

753 

754 def get_properties(self): 

755 """ return material properies mu and lambda """ 

756 return np.array([self.mu, self.lamb], dtype = npfloat) 

757 

758 @staticmethod 

759 @njit(inline='always', fastmath=True, cache=True) 

760 def sigma_c_kernel(F, props, out_sigma, out_c): 

761 """ 

762 Calcule la contrainte de Cauchy et le tenseur d'élasticité spatial 2D. 

763 Zéro allocation, remplissage en place. 

764 """ 

765 mu = props[0] 

766 lamb = props[1] 

767 # --- Cinématique --- 

768 # J = det(F) 

769 J = F[0, 0] * F[1, 1] - F[0, 1] * F[1, 0] 

770 # b = F * F^T (Tenseur de Cauchy-Green gauche) 

771 b00 = F[0, 0]*F[0, 0] + F[0, 1]*F[0, 1] 

772 b01 = F[0, 0]*F[1, 0] + F[0, 1]*F[1, 1] 

773 b11 = F[1, 0]*F[1, 0] + F[1, 1]*F[1, 1] 

774 fill_nh_sigma_2d(J, b00, b11, b01, mu, lamb, out_sigma) 

775 fill_nh_c_2d(J, mu, lamb, out_c) 

776 

777def check_material_law_F(law:HyperElasticMaterial, F:FloatArray, verb = True)->Tuple[bool, bool]: 

778 ''' check PKI(F) and dPKIdF(F) return PKI_ok and dPKIDF_ok 

779 PKI_ok and dPKIdF_ok, respectively True if PKI (dPKIdF) is close to 

780 the finite difference approximation, False otherwise. 

781 ''' 

782 # pylint: disable=C0103 

783 #law = law 

784 PKI = law.PKI(F) 

785 PKI_num = df_num(law.potentialF, F, F.shape[-2:], ()) 

786 PKI_ok = check_relerr(PKI, PKI_num, PKI.shape[-2:] ) 

787 dPKIdF = law.dPKIdF(F) 

788 dPKIdF_num = df_num(law.PKI, F, F.shape[-2:], F.shape[-2:]) 

789 dPKIdF_ok = check_relerr(dPKIdF, dPKIdF_num, PKI.shape[-2:] + F.shape[-2:], verbose= verb) 

790 return PKI_ok, dPKIdF_ok 

791 

792def check_material_law_C(law:HyperElasticMaterial, C:FloatArray, verb = True)->Tuple[bool, bool]: 

793 ''' check PKII(C) and dPKIIdC(C) return PKII_ok and dPKIIDC_ok 

794 PKII_ok and dPKIIdC_ok, respectively True if PKII (dPKIIdC) is close to 

795 the finite difference approximation, False otherwise. 

796 ''' 

797 # pylint: disable=C0103 

798 #verb = True 

799 PKII = law.PKII(C) 

800 shape = C.shape[-2:] 

801 PKII_num = 2.*df_num(law.potentialC, C, shape, ()) 

802 PKII_ok = check_relerr(PKII, PKII_num, PKII.shape[-2:] , verbose= verb) 

803 dPKIIdC = law.dPKIIdC(C) 

804 dPKIIdC_num = df_num(law.PKII, C, shape, shape) 

805 #dPKIIdC_num2 = df_num(lambda C : law.PKII( C.swapaxes(-1,-2)), C, shape, shape) 

806 #dPKIIdC_num = 0.5*(dPKIIdC_num + dPKIIdC_num2) 

807 dPKIIdC_ok = check_relerr(dPKIIdC, dPKIIdC_num, PKII.shape[-2:] + C.shape[-2:], verbose = verb) 

808 return PKII_ok, dPKIIdC_ok 

809 

810def benchmark(law:HyperElasticMaterial, n:int, mem = False, prof = True): 

811 """ benchmark : compute and profile law.PKI and law.dPKIDF """ 

812 # pylint: disable=C0103 

813 dim = HYP2DIM[law.hyp] 

814 if law.hyp in [PLANESTRESS, PLANESTRAIN] and mem: 

815 F = random_lintransform2dto3d(n) 

816 print(f'computing PKI and dPKIDF for {n:.3e} F32, in in {law.hyp}') 

817 else : 

818 gen = random_lintransform 

819 F = gen(n, dim) 

820 print(f'computing PKI and dPKIDF for {n:.3e} F, in {law.hyp}') 

821 Profile.doprofile = prof 

822 Profile.enable() 

823 start = time.time() 

824 __ = law.PKI(F) 

825 __ = law.dPKIdF(F) 

826 end = time.time() 

827 Profile.disable() 

828 Profile.print_stats() 

829 print('time : ', end - start) 

830 

831if __name__ == "__main__": 

832 lawb = CompressibleNeoHookeen(E = 1., nu = 0.3, hyp = PLANESTRESS) 

833 benchmark(lawb, 1000000, prof=True) 

834 

835 # computing PKI and dPKIDF for 1.000e+06 F, in MaterialHypothesis.DIM3 

836 # 550 function calls (532 primitive calls) in 1.540 seconds 

837 # lawPSTRAIN = CompressibleNeoHookeen(E=1.2, nu = 0.3, hyp=PLANESTRAIN) 

838 # lawPSTRESS = CompressibleNeoHookeen(E=1.2, nu = 0.3, hyp=PLANESTRESS) 

839 # # benchmark(lawPSTRESS, 10000, mem =True, prof =True) 

840 # law1 = SimpleLaw0() 

841 # law2 = SimpleLaw1() 

842 # law3 = SimpleLaw2() 

843 # F2d0 = np.array([[2.,0.1],[1.,1.3]]) 

844 # F2d1 = np.array([[[2.,0.1],[1.,1.3]]]) 

845 # F2d2 = np.array([[[2.,0.1],[1.,1.3]], 

846 # [[2.,0.5],[3.,1.1]] 

847 # ]) 

848 # Fm = np.array( [ 

849 # [[1.,0.],[0.,1.],[0,0]], 

850 # [[1.,0.],[0.,0.],[0,1]] 

851 # ]) 

852 # #Fm = np.array([[1.,0.],[0.,1.],[0,0]]) 

853 # Fm2d = np.array([[2.,0.1],[1.,1.3],[0.,0.]]) 

854 # Fm = np.array([[[2.,0.], [0.,1.], [0,0]], 

855 # [[2.,0.1],[0.1,1.],[0,0]]]) 

856 # Fm0= np.array([[[3.,0.1], [1., 0.1], [1.,1.]], 

857 # [[2.,0.1],[0.1,1.],[0.1,0.2]]]) 

858 # Fm1= np.array([[[3.,0.1], [1., 0.1], [1.,1.]], 

859 # [[2.,0.1],[0.1,1.],[0.1,0.2]]]) 

860 # lawu = lawPSTRESS 

861 # Fu = Fm1 

862 # print(test_material_law(lawu, Fu)) 

863 

864# computing PKI and dPKIDF for 1.000e+06 F, in MaterialHypothesis.DIM3 

865# 62 function calls in 1.243 seconds 

866# Ordered by: cumulative time 

867# ncalls tottime percall cumtime percall filename:lineno(function) 

868# 1 0.013 0.013 1.031 1.031 hyperelasticity.py:221(dPKIdF) 

869# 1 0.682 0.682 0.865 0.865 hyperelasticity.py:186(_dPKIdF_exp) 

870# 2 0.035 0.017 0.310 0.155 smallsize_tensor.py:69(det_invt) 

871# 2 0.262 0.131 0.262 0.131 smallsize_tensor.py:25(_cofac) 

872# 1 0.000 0.000 0.211 0.211 hyperelasticity.py:214(PKI) 

873# 1 0.003 0.003 0.211 0.211 hyperelasticity.py:164(_PKI_3d) 

874# 1 0.182 0.182 0.182 0.182 smallsize_tensor.py:79(cross_ijkl) 

875# 1 0.052 0.052 0.052 0.052 hyperelasticity.py:171(_PKI_exp) 

876# 2 0.014 0.007 0.014 0.007 smallsize_tensor.py:41(_det)