Coverage for pyxfem/core/smallsize_tensor.py: 65%

540 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# pylint: disable=invalid-name 

4""" 

5This module i 

6s dedicated to implement faster functionnality for tensor on spaces of dim 2 and 3 

7than the equivalent numpy call 

8Created on May1 2024 

9@author: nchevaug 

10""" 

11 

12from types import NoneType 

13from typing import Tuple, Callable, Literal, Optional 

14from warnings import warn 

15import numpy as np 

16import scipy as sp 

17from numba import float32, float64, guvectorize 

18import numba as nb 

19from pyxfem.core.config import FASTMATH, npfloat, FloatArray 

20 

21 

22HAS_MYGRAD = False 

23 

24# o2 idetity Id2.u = u 

25Id2_2D = np.eye(2) 

26Id2_3D = np.eye(3) 

27Id2 = {2:Id2_2D, 3:Id2_3D} 

28# o4 identity Id4:A = A 

29Id4_3D = np.eye(9).reshape((3,3,3,3)) 

30Id4_2D = np.eye(4).reshape((2,2,2,2)) 

31Id4 = {2:Id4_2D, 3:Id4_3D} 

32# o4 Transposition Tr4:A = A^T 

33Tr4_2D = Id4_2D.swapaxes(2,3) #O4 transposr 

34Tr4_3D = Id4_3D.swapaxes(2,3) #O4 transposr 

35Tr4 = {2:Tr4_2D, 3:Tr4_3D} 

36# o4 Symetrizer Sy4:A = (A +A^T)/2. 

37Sy4_2D = 0.5*(Tr4_2D+Id4_2D) # O4 symetrizer 

38Sy4_3D = 0.5*(Tr4_3D+Id4_3D) # O4 symetrizer 

39Sy4 = {2:Sy4_2D, 3:Sy4_3D} 

40# o4 spherificator Sp4:A = trace(A) I 

41Sp4_2D = np.tensordot(Id2_2D , Id2_2D, axes = 0 ) 

42Sp4_3D = np.tensordot(Id2_3D , Id2_3D, axes = 0 ) 

43Sp4 = {2:Sp4_2D, 3:Sp4_3D} 

44 

45 

46@nb.njit(parallel = True, cache = True) 

47def axpy_pnjit(alpha, x, y): 

48 """Compute vector scale-and-add operation in parallel using Numba. 

49 

50 Evaluates the in-place operation y[j] += alpha * x[j] over the first axis. 

51 

52 Parameters 

53 ---------- 

54 alpha : float or scalar 

55 Scalar multiplier. 

56 x : ndarray 

57 Input array. 

58 y : ndarray 

59 Output array modified in place. 

60 """ 

61 for j in nb.prange(x.shape[0]): # pylint: disable=not-an-iterable 

62 y[j] += alpha*x[j] 

63 

64def axpy(alpha, x, y, parallel = False): 

65 """Compute the scaled vector addition y = y + alpha * x. 

66 

67 Parameters 

68 ---------- 

69 alpha : float or scalar 

70 Scalar scaling factor. 

71 x : ndarray 

72 Input array. 

73 y : ndarray 

74 Target array modified in place. 

75 parallel : bool, optional 

76 If True, executes the multithreaded Numba implementation `axpy_pnjit`. 

77 Defaults to False. 

78 """ 

79 if parallel : 

80 axpy_pnjit(alpha, x, y) 

81 else: 

82 y += alpha*x 

83 

84def random_orthtransform(n:int, dim:Literal[2,3], 

85 random_state = np.random.default_rng(seed=None)) -> FloatArray: 

86 """Generate a batch of random orthogonal transformation matrices with det(Q) >= 0. 

87 

88 Parameters 

89 ---------- 

90 n : int 

91 Number of orthogonal transformations to generate. 

92 dim : {2, 3} 

93 Spatial dimension (2 for planar rotations, 3 for 3D rotations via orthonormal bases). 

94 random_state : np.random.Generator, optional 

95 Random number generator instance. Defaults to `np.random.default_rng()`. 

96 

97 Returns 

98 ------- 

99 FloatArray 

100 Array of shape `(n, dim, dim)` containing proper orthogonal rotation matrices. 

101 """ 

102 if dim==2 : 

103 alpha = random_state.random((n))*2*np.pi 

104 sin = np.sin(alpha) 

105 cos = np.cos(alpha) 

106 Q = np.zeros((n, 2, 2)) 

107 Q[:,0,0] = cos 

108 Q[:,0,1] = -sin 

109 Q[:,1,0] = sin 

110 Q[:,1,1] = cos 

111 return Q 

112 if dim == 3 : 

113 Q = random_state.random((n, dim, dim)) 

114 Q[...,:,0] = Q[...,:,0]/np.linalg.norm(Q[...,:,0], axis = -1, keepdims=True) 

115 Q[...,:,1] = np.cross(Q[...,:,2], Q[...,:,0]) 

116 Q[...,:,1] = Q[...,:,1]/np.linalg.norm(Q[...,:,1], axis = -1, keepdims=True) 

117 Q[...,:,2] = np.cross(Q[...,:,0], Q[...,:,1]) 

118 return Q 

119 

120def vecprodmat(n:FloatArray) -> FloatArray: 

121 """Construct skew-symmetric cross-product matrices from 3D vectors. 

122 

123 For each 3D vector n, constructs the tensor Q such that Q @ x = n x x. 

124 

125 Parameters 

126 ---------- 

127 n : FloatArray 

128 Array of 3D vectors with trailing dimension of length 3 (shape `(..., 3)`). 

129 

130 Returns 

131 ------- 

132 FloatArray 

133 Array of skew-symmetric 3x3 matrices with shape `(..., 3, 3)`. 

134 """ 

135 assert n.shape[-1] == 3 

136 Q = np.zeros(n.shape[:-1] + (3,3)) 

137 Q[...,0,1] = -n[...,2] 

138 Q[...,0,2] = n[...,1] 

139 Q[...,1,0] = n[...,2] 

140 Q[...,1,2] = -n[...,0] 

141 Q[...,2,0] = -n[...,1] 

142 Q[...,2,1] = n[...,0] 

143 return Q 

144 

145@guvectorize([ 

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

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

148], '(d)->()', nopython=True, cache=True) 

149def vec_norm2(v, out): 

150 """Compute the Euclidean L2 norm of 1D vector slices via Numba Generalized Ufunc. 

151 

152 Parameters 

153 ---------- 

154 v : ndarray 

155 1D vector or array slice of dimension `(d,)`. 

156 out : ndarray 

157 Output scalar buffer receiving the computed norm. 

158 """ 

159 s = 0.0 

160 for i in range(v.shape[0]): 

161 s += v[i]**2 

162 out[0] = np.sqrt(s) 

163 

164def random_lintransform(n:int, dim:int, 

165 random_state = np.random.default_rng(seed=None) ) -> FloatArray: 

166 """Generate a batch of random linear transformation tensors with det(F) >= 0. 

167 

168 Filters near-singular transformations (|det| < 1e-2) and reflects tensors 

169 with negative determinants to enforce orientation preservation. 

170 

171 Parameters 

172 ---------- 

173 n : int 

174 Number of transformation matrices. 

175 dim : int 

176 Matrix dimension (e.g., 2 or 3). 

177 random_state : np.random.Generator, optional 

178 Random number generator instance. Defaults to `np.random.default_rng()`. 

179 

180 Returns 

181 ------- 

182 FloatArray 

183 Array of transformation matrices of shape `(n, dim, dim)`. 

184 """ 

185 F = np.zeros((n, dim, dim)) 

186 J = np.zeros(n) 

187 while True: 

188 indexes = np.where(np.abs(J)< 1.e-2)[0] 

189 k = len(indexes) 

190 if k == 0 : 

191 break 

192 F[indexes] = random_state.random((len(indexes), dim, dim)) 

193 J[indexes] = det(F[indexes]) 

194 

195 Jneg = np.sign(J) <= 0. 

196 F[Jneg,0,:] = -F[Jneg,0,:] 

197 return F 

198 

199def random_lintransform2dto3d(n:int, random_state = np.random.default_rng(seed=None)) -> FloatArray: 

200 """Generate random transformation matrices F mapping 2D vectors to 3D space. 

201 

202 Parameters 

203 ---------- 

204 n : int 

205 Number of transformations. 

206 random_state : np.random.Generator, optional 

207 Random number generator instance. Defaults to `np.random.default_rng()`. 

208 

209 Returns 

210 ------- 

211 FloatArray 

212 Array of matrices of shape `(n, 3, 2)`. 

213 """ 

214 F =random_state.random((n, 3, 2)) 

215 return F 

216 

217def metric(F:FloatArray) ->FloatArray: 

218 """Compute the right Cauchy-Green deformation tensor / Riemannian metric C = F^T @ F. 

219 

220 Parameters 

221 ---------- 

222 F : FloatArray 

223 Deformation gradient or transformation tensor of shape `(..., M, N)`. 

224 

225 Returns 

226 ------- 

227 FloatArray 

228 Metric tensor array of shape `(..., N, N)`. 

229 """ 

230 return trans(F)@F 

231 

232def dmetric(F:FloatArray) -> FloatArray: 

233 """Compute the first derivative of the metric tensor m = F^T @ F with respect to F. 

234 

235 Returns the fourth-order tensor R[..., a, b, i, c] = dm_{ab} / dF_{ic}. 

236 

237 Parameters 

238 ---------- 

239 F : FloatArray 

240 Transformation tensor of shape `(..., dim_out, dim_in)`. 

241 

242 Returns 

243 ------- 

244 FloatArray 

245 Fourth-order gradient tensor collection of shape `(..., dim_in, dim_in, dim_out, dim_in)`. 

246 """ 

247 dim = F.shape[-1] 

248 I = np.eye(dim) 

249 return np.einsum("...ac,...ib->...abic", I, F) + np.einsum("...bc,...ia->...abic", I, F) 

250 

251def d2metric(F:FloatArray) -> FloatArray: 

252 """Compute the second derivative of the metric tensor m with respect to F. 

253 

254 Returns the constant sixth-order tensor R[..., a, b, i, c, j, d] = d^2 m_{ab} / (dF_{ic} dF_{jd}). 

255 

256 Parameters 

257 ---------- 

258 F : FloatArray 

259 Transformation tensor of shape `(..., dim_out, dim_in)`. 

260 

261 Returns 

262 ------- 

263 FloatArray 

264 Sixth-order tensor of shape `(..., dim_in, dim_in, dim_out, dim_in, dim_out, dim_in)`. 

265 """ 

266 I0 = np.eye(F.shape[-2]) 

267 I1 = np.eye(F.shape[-1]) 

268 d2m = np.einsum("...ac, ...ij, ...bd ->...abicjd", I1, I0, I1) \ 

269 + np.einsum("...bc, ...ij, ...ad ->...abicjd", I1, I0, I1) 

270 d2m = d2m.reshape((1,)*(F.ndim-2) + d2m.shape, copy=False) 

271 return d2m 

272 

273def invmetric(F:FloatArray)->FloatArray: 

274 """Compute the inverse of the metric tensor m = F^T @ F. 

275 

276 Parameters 

277 ---------- 

278 F : FloatArray 

279 Transformation tensor of shape `(..., M, N)`. 

280 

281 Returns 

282 ------- 

283 FloatArray 

284 Inverse metric tensor array of shape `(..., N, N)`. 

285 """ 

286 return inv(metric(F)) 

287 

288def dinvmetric(F): 

289 """Compute the derivative of the inverse metric tensor with respect to F. 

290 

291 Evaluates d(m^{-1})_{ab} / dF_{ic}. 

292 

293 Parameters 

294 ---------- 

295 F : FloatArray 

296 Transformation tensor of shape `(..., M, N)`. 

297 

298 Returns 

299 ------- 

300 FloatArray 

301 Fourth-order derivative tensor of shape `(..., N, N, M, N)`. 

302 """ 

303 im = invmetric(F) 

304 return -np.einsum("...ac,...ib->...abic", im, F@im)-np.einsum("...cb,...ia->...abic", im, F@im) 

305 

306def trace(A:FloatArray) -> FloatArray: 

307 """Compute the trace of a batch of square second-order tensors. 

308 

309 Parameters 

310 ---------- 

311 A : FloatArray 

312 Square tensor collection of shape `(..., N, N)`. 

313 

314 Returns 

315 ------- 

316 FloatArray 

317 Trace values array of shape `(...)`. 

318 

319 Raises 

320 ------ 

321 ValueError 

322 If the trailing dimensions are not square. 

323 """ 

324 if A.shape[-1] != A.shape[-2]: 

325 raise ValueError("in trace, each array in the collection must be square") 

326 dim = A.shape[-1] 

327 if dim == 1: 

328 tr = A[...,0,0] 

329 elif dim == 2: 

330 tr = A[...,0,0] + A[...,1,1] 

331 elif dim == 3: 

332 tr = A[...,0,0] + A[...,1,1] + A[...,2,2] 

333 else: 

334 tr = np.einsum('...ii', A) 

335 return tr 

336 

337def trace2(A:FloatArray) -> FloatArray: 

338 """Compute the Frobenius squared norm / trace(A^T @ A) for a batch of tensors. 

339 

340 Parameters 

341 ---------- 

342 A : FloatArray 

343 Tensor collection of shape `(..., N, N)`. 

344 

345 Returns 

346 ------- 

347 FloatArray 

348 Array containing the sum of squared components for each tensor. 

349 

350 Raises 

351 ------ 

352 ValueError 

353 If the trailing dimensions are not square. 

354 """ 

355 if A.shape[-1] != A.shape[-2]: 

356 raise ValueError("in trace, each array in the collection must be square") 

357 return np.sum(np.square(A), axis = (-1,-2)) 

358 

359def trans(A:FloatArray)-> FloatArray: 

360 """ return the transposes of the input collection of second order tensors """ 

361 """Transpose the last two dimensions of an array of matrices. 

362 

363 Parameters 

364 ---------- 

365 A : FloatArray 

366 Tensor array of shape `(..., M, N)`. 

367 

368 Returns 

369 ------- 

370 FloatArray 

371 Transposed tensor array of shape `(..., N, M)`. 

372 """ 

373 return A.swapaxes(-1,-2) 

374 

375def contract(A:FloatArray, B:FloatArray)-> FloatArray: 

376 """Compute the double contraction (Frobenius inner product) A : B = A_{ij} B_{ij}. 

377 

378 Parameters 

379 ---------- 

380 A : FloatArray 

381 First batch of tensors of shape `(..., M, N)`. 

382 B : FloatArray 

383 Second batch of tensors of matching shape `(..., M, N)`. 

384 

385 Returns 

386 ------- 

387 FloatArray 

388 Contracted scalar field of shape `(...)`. 

389 """ 

390 assert A.shape == B.shape 

391 assert A.ndim >= 2 

392 if A.shape[:-2] == (2,2): 

393 return A[...,0,0]*B[...,0,0]+A[...,0,1]*B[...,0,1] \ 

394 + A[...,1,0]*B[...,1,0]+ A[...,1,1]*B[...,1,1] 

395 return np.einsum("...ij, ...ij-> ...", A, B) 

396 

397def _cofac(A:FloatArray) -> FloatArray: 

398 """compute cofac matrix for stack of 2x2 or 3x3 matrices """ 

399 """Compute the cofactor matrix for batches of 2x2 or 3x3 matrices. 

400 

401 Parameters 

402 ---------- 

403 A : FloatArray 

404 Tensor array of shape `(..., 2, 2)` or `(..., 3, 3)`. 

405 

406 Returns 

407 ------- 

408 FloatArray 

409 Cofactor matrix array with identical shape to `A`. 

410 

411 Raises 

412 ------ 

413 ValueError 

414 If the trailing dimensions are neither (2, 2) nor (3, 3). 

415 """ 

416 if A.shape[-2:] == (2,2): 

417 cA = np.empty_like(A) 

418 cA[..., 0, 0] = A[..., 1, 1] 

419 cA[..., 0, 1] = -A[..., 1, 0] 

420 cA[..., 1, 0] = -A[..., 0, 1] 

421 cA[..., 1, 1] = A[..., 0, 0] 

422 return cA 

423 if A.shape[-2:] == (3,3): 

424 cA = np.empty_like(A) 

425 cA[...,0,:] = cross(A[...,1,:], A[...,2,:]) 

426 cA[...,1,:] = cross(A[...,2,:], A[...,0,:]) 

427 cA[...,2,:] = cross(A[...,0,:], A[...,1,:]) 

428 return cA 

429 raise ValueError("A must be a (..,2,2) or (...,3,3) array_like") 

430 

431@guvectorize([ 

432 (float64[:,:], float64[:]), 

433 (float32[:,:], float32[:]) 

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

435def det22_numba(A, out): 

436 """Compute the determinant of a single 2x2 matrix via Numba Generalized Ufunc. 

437 

438 Parameters 

439 ---------- 

440 A : ndarray 

441 2x2 input matrix slice of shape `(2, 2)`. 

442 out : ndarray 

443 Output buffer receiving the determinant scalar. 

444 """ 

445 out[0] = A[0,0]*A[1,1]- A[0,1]*A[1,0] 

446 

447def _det(A:FloatArray, out:NoneType|FloatArray=None) -> FloatArray|npfloat: 

448 """Compute the determinant for batches of 2x2, 3x3, or 3x2 (metric surface) matrices. 

449 

450 Parameters 

451 ---------- 

452 A : FloatArray 

453 Matrix or batch of matrices of shape `(..., 2, 2)`, `(..., 3, 3)`, or `(..., 3, 2)`. 

454 out : FloatArray or None, optional 

455 Pre-allocated output destination buffer. Defaults to None. 

456 

457 Returns 

458 ------- 

459 FloatArray or npfloat 

460 Determinants array of shape `(...)`. For 3x2, returns sqrt(det(F^T F)). 

461 

462 Raises 

463 ------ 

464 ValueError 

465 If matrix dimensions are not supported. 

466 """ 

467 if out is None : 

468 out = np.empty_like(A[...,0,0]) 

469 if A.shape[-2:] == (3,2): 

470 m = metric(A) 

471 _det(m, out) 

472 return np.sqrt(out, out) 

473 if A.shape[-2:] == (2,2): 

474 det22_numba(A, out) 

475 return out 

476 #return A[...,0,0]*A[...,1,1]- A[...,0,1]*A[...,1,0] 

477 if A.shape[-2:] == (3,3): 

478 a= A[...,0,0] 

479 b= A[...,0,1] 

480 c= A[...,0,2] 

481 d= A[...,1,0] 

482 e= A[...,1,1] 

483 f= A[...,1,2] 

484 g= A[...,2,0] 

485 h= A[...,2,1] 

486 i= A[...,2,2] 

487 if np.isscalar(out): 

488 out = a*e*i +b*f*g +c*d*h - c*e*g -b*d*i -a*f*h 

489 else: 

490 out[...] = a*e*i +b*f*g +c*d*h - c*e*g -b*d*i -a*f*h 

491 return out 

492 raise ValueError(f"Size error {str(A.shape)}") 

493 

494def det_invt(A:FloatArray) -> Tuple[FloatArray|npfloat, FloatArray]: 

495 """Compute both the determinant and the inverse-transpose A^{-T} simultaneously. 

496 

497 Also handles the 3x2 membrane configuration where A^{-T} is computed as A @ (A^T A)^{-1}. 

498 

499 Parameters 

500 ---------- 

501 A : FloatArray 

502 Tensor array of shape `(..., 2, 2)`, `(..., 3, 3)`, or `(..., 3, 2)`. 

503 

504 Returns 

505 ------- 

506 tuple of (FloatArray or npfloat, FloatArray) 

507 Tuple (detA, invAT) containing determinants and inverse-transposed matrices. 

508 

509 Raises 

510 ------ 

511 ValueError 

512 If trailing dimensions are not (2, 2), (3, 3), or (3, 2). 

513 """ 

514 if A.shape[-2:] == (3,2): 

515 m = metric(A) 

516 detm = det(m) 

517 invAT = A@inv(m) 

518 return np.sqrt(detm), invAT 

519 if A.shape[-2:] in [(2,2), (3,3)]: 

520 detA = det(A) 

521 adjA = cofac(A) 

522 if A.shape[:-2]== (): 

523 invAT = adjA/detA 

524 else : 

525 invAT = adjA / np.atleast_1d(detA)[..., None, None] 

526 return detA, invAT 

527 raise ValueError("A.shape[-2,:] should be (2, 2) or (3, 3) or (3,2)"+str(A.shape) ) 

528 

529det = _det 

530def ddet(A:FloatArray)->FloatArray: 

531 """Compute the derivative of the determinant with respect to the matrix A. 

532 

533 Returns d(det(A)) / dA = cofac(A) for square matrices, or det(A) * A^{-T}. 

534 

535 Parameters 

536 ---------- 

537 A : FloatArray 

538 Input tensor collection of shape `(..., M, N)`. 

539 

540 Returns 

541 ------- 

542 FloatArray 

543 Derivative array having the identical shape of `A`. 

544 """ 

545 if A.shape[-1]==A.shape[-2]: 

546 return cofac(A) 

547 J, invAT = det_invt(A) 

548 return ((np.atleast_1d(J)[:, np.newaxis, np.newaxis])*invAT).reshape(A.shape) 

549 

550cofac = _cofac 

551 

552def cross(A:FloatArray, B:FloatArray) -> FloatArray: 

553 """Compute the 3D vector cross product between arrays of vectors. 

554 

555 Parameters 

556 ---------- 

557 A : FloatArray 

558 First collection of 3D vectors of shape `(..., 3)`. 

559 B : FloatArray 

560 Second collection of 3D vectors of shape `(..., 3)`. 

561 

562 Returns 

563 ------- 

564 FloatArray 

565 Cross product vector array of shape `(..., 3)`. 

566 

567 Raises 

568 ------ 

569 ValueError 

570 If the trailing dimension of A or B is not 3. 

571 """ 

572 if isinstance(A, np.ndarray) and isinstance(B, np.ndarray): 

573 return np.cross(A,B) 

574 # if A.shape[-1] != 3 or B.shape[-1] !=3 : 

575 # raise ValueError("A and be most be of shape (..., 3) ") 

576 # C = np.empty_like(A) 

577 # C[...,0] = A[..., 1] * B[...,2] - A[..., 2] * B[...,1] 

578 # C[...,1] = -A[..., 0] * B[...,2] + A[..., 2] * B[...,0] 

579 # C[...,2] = A[..., 0] * B[...,1] - A[..., 1] * B[...,0] 

580 # return C 

581 

582def invariants(A:FloatArray) -> FloatArray: 

583 """Compute principal invariants for a batch of 2D or 3D second-order tensors. 

584 

585 For 2D: [tr(A), det(A)]. 

586 For 3D: [tr(A), 0.5 * (tr(A)^2 - tr(A^T A)), det(A)]. 

587 

588 Parameters 

589 ---------- 

590 A : FloatArray 

591 Square tensor collection of shape `(..., 2, 2)` or `(..., 3, 3)`. 

592 

593 Returns 

594 ------- 

595 FloatArray 

596 Invariants array with trailing dimension 2 (for 2D) or 3 (for 3D). 

597 

598 Raises 

599 ------ 

600 ValueError 

601 If trailing dimensions are not (2, 2) or (3, 3). 

602 """ 

603 if A.shape[-2:] == (2,2): 

604 r = np.empty( A.shape[:-2]+(2,)) 

605 r[...,0] = trace(A) 

606 r[...,1] = det(A) 

607 return r 

608 if A.shape[-2:] == (3,3): 

609 r = np.empty( A.shape[:-2]+(3,)) 

610 trA = trace(A) 

611 r[...,0] = trA 

612 r[...,1] = 0.5*( np.square(trA) - trace(trans(A)@A) ) 

613 r[...,2] = det(A) 

614 return r 

615 raise ValueError("A must be of size (..., 2, 2) or (..., 3, 3)") 

616 

617def dinvariants(A:FloatArray) -> FloatArray: 

618 """Compute derivatives of principal invariants with respect to tensor A. 

619 

620 Parameters 

621 ---------- 

622 A : FloatArray 

623 Square tensor collection of shape `(..., 2, 2)` or `(..., 3, 3)`. 

624 

625 Returns 

626 ------- 

627 FloatArray 

628 Derivatives tensor of shape `(..., 2, 2, 2)` for 2D or `(..., 3, 3, 3)` for 3D. 

629 

630 Raises 

631 ------ 

632 ValueError 

633 If trailing dimensions are not (2, 2) or (3, 3). 

634 """ 

635 if A.shape[-2:] == (2,2): 

636 r = np.empty( A.shape[:-2]+(2,2,2)) 

637 I = np.eye(2)[(np.newaxis,)*len(A.shape[:-2]) ] 

638 r[...,0, :, :] = I 

639 r[...,1, : ,:] = cofac(A) 

640 return r 

641 if A.shape[-2:] == (3,3): 

642 nfirstdim = len(A.shape[:-2]) 

643 r = np.empty( A.shape[:-2]+(3,3,3)) 

644 I = np.eye(3)[(np.newaxis,)*nfirstdim ] 

645 trA = trace(A)[..., None, None] 

646 r[...,0,:,:] = I 

647 r[...,1,:,:] = trA*I-A 

648 r[...,2,:,:] = cofac(A) 

649 return r 

650 raise ValueError("A must be of size (..., 2, 2) or (..., 3, 3)") 

651 

652def invariants2Dto3D(invariant2D): 

653 """ from the invariant of a "2.5d tensor", compute the invariants of the equivalent 3D tensor 

654 what I mean by a 2.5d tensor is for example the gradiant of the 

655 transformation in plan stress case : 

656 F = F2D_{a,b} e_a x e_b + l2 e_2 x e_2 , where a, b in [0,1] and e_2 = e_0 vec e_1 

657 the invarariant 2D are strored in a (..., 3) ndarray, where 

658 invariant2D[..., 0] is an array containing I(F2D) = trace(F2D) 

659 invariant2D[..., 1] is an array containing 

660 II(F2D) = 0.5(trace(F2D)^2 - trace(F2D.T@F2D)) = det(F2D) 

661 invariant2D[..., 2] is an array containing l2 

662 Parameters 

663 ---------- 

664 invariant2D : ndarray 

665 Array of shape `(..., 3)` containing: 

666 - [..., 0]: I_2D = trace(F_2D) 

667 - [..., 1]: II_2D = det(F_2D) 

668 - [..., 2]: lambda_2 (out-of-plane stretch) 

669 

670 Returns 

671 ------- 

672 ndarray 

673 Array of shape `(..., 3)` with the three corresponding 3D invariants. 

674 """ 

675 invariant3D = np.empty(invariant2D.shape) 

676 I2D = invariant2D[...,0] 

677 II2D = invariant2D[...,1] 

678 l2 = invariant2D[...,2] 

679 invariant3D[..., 0] = I2D + l2 

680 invariant3D[..., 1] = II2D + l2*I2D 

681 invariant3D[..., 2] = II2D * l2 

682 return invariant3D 

683 

684def dinvariants2Dto3D(invariant2D:FloatArray) -> FloatArray: 

685 """ Compute the matrix of the derivatives of the 3D invariants of tensor A 

686 computed as a function of the 2D invariant. 

687 input invariant2D an FloatArray of shape[-1] = 3 where : 

688 invariant2D[..., 0] = I2D = trace(A2D) 

689 invariant2D[..., 1] = II2D = det(A2D) = 0.5*(trace(A2D)**2 - trace(A2D@A2D**2) 

690 invariant2D[..., 2] = A33 

691 return dinvariant3D an FNDarray of shape (..., 3, 3) 

692 where dinvariant3D[..., i, j] contain the derivative of the ith 3d invarariant with regard 

693 to the jth 2d invariant 

694 Parameters 

695 ---------- 

696 invariant2D : FloatArray 

697 Array of shape `(..., 3)` holding [I_2D, II_2D, lambda_2]. 

698 

699 Returns 

700 ------- 

701 FloatArray 

702 Jacobian tensor of shape `(..., 3, 3)` where entry `[..., i, j]` is d(I_3D, i) / d(I_2D, j). 

703 """ 

704 I2D = invariant2D[...,0] 

705 II2D = invariant2D[...,1] 

706 l2 = invariant2D[...,2] 

707 dinvariant3D = np.ones(invariant2D.shape + (3,)) 

708 dinvariant3D[..., 0, 1] = 0. 

709 dinvariant3D[..., 1, 0] = l2 

710 dinvariant3D[..., 1, 2] = I2D 

711 dinvariant3D[..., 2, 0] = 0. 

712 dinvariant3D[..., 2, 1] = l2 

713 dinvariant3D[..., 2, 2] = II2D 

714 return dinvariant3D 

715 

716def d2invariants2Dto3D(invariant2D): 

717 """ Compute the matrix of the second derivatives of the 3D invariants of tensor A (Hessian) 

718 computed as a function of the 2D invariants. 

719 input invariant2D an FloatArray of shape[-1] = 3 where : 

720 invariant2D[..., 0] = I2D = trace(A2D) 

721 invariant2D[..., 1] = II2D = det(A2D) = 0.5*(trace(A2D)**2 - trace(A2D@A2D**2) 

722 invariant2D[..., 2] = A33 

723 return dinvariant3D an FNDarray of shape (..., 3, 3, 3) 

724 where dinvariant3D[..., i, j, k] contain the derivative of the ith 3d invarariant 

725 with regard to the jth 2d invariant and the kth 2d invarariant. 

726 Note that the resuluting 27 component matrix contain only 4 non zero terms ... 

727 

728 Parameters 

729 ---------- 

730 invariant2D : ndarray 

731 Array of shape `(..., 3)` holding [I_2D, II_2D, lambda_2]. 

732 

733 Returns 

734 ------- 

735 ndarray 

736 Second derivative tensor of shape `(..., 3, 3, 3)` containing d^2(I_3D, i) / (d(I_2D, j) d(I_2D, k)). 

737 """ 

738 d2invariant3D = np.zeros(invariant2D.shape + (3, 3)) 

739 d2invariant3D[..., 1, 0, 2] = 1. 

740 d2invariant3D[..., 1, 2, 0] = 1. 

741 d2invariant3D[..., 2, 1, 2] = 1. 

742 d2invariant3D[..., 2, 2, 1] = 1. 

743 return d2invariant3D 

744 

745def inv(A:FloatArray, detA: Optional[FloatArray|npfloat] = None, 

746 out: Optional[FloatArray] =None ) -> FloatArray: 

747 """compute inverse matrix for stack of 2x2 or 3x3 matrices """ 

748 """Compute matrix inverse for batches of 2x2, 3x3, or pseudo-inverse for 3x2 matrices. 

749 

750 Parameters 

751 ---------- 

752 A : FloatArray 

753 Tensor collection of shape `(..., 2, 2)`, `(..., 3, 3)`, or `(..., 3, 2)`. 

754 detA : FloatArray or npfloat, optional 

755 Precomputed determinant of A to accelerate evaluation. Defaults to None. 

756 out : FloatArray, optional 

757 Output buffer receiving the inverted matrix. Defaults to None. 

758 

759 Returns 

760 ------- 

761 FloatArray 

762 Inverted matrix collection. 

763 """ 

764 if A.shape[-2:] == (3,2): 

765 out = np.empty(A.shape[:-2] + (2,3)) if out is None else out 

766 m = metric(A) 

767 np.matmul(inv(m), trans(A), out) 

768 return out 

769 if detA is None : 

770 detA = det(A) 

771 if not isinstance(detA, (float, int)): 

772 detA = detA.reshape(A.shape[:-2]+(1,1)) 

773 iA = np.empty_like(A) if out is None else out 

774 if A.shape[-2:] == (2,2): 

775 iA[..., 0,0] = A[...,1,1] 

776 iA[..., 0,1] = -A[...,0,1] 

777 iA[..., 1,0] = -A[...,1,0] 

778 iA[..., 1,1] = A[...,0,0] 

779 elif A.shape[-2:] == (3,3): 

780 warn("3d case not fully optimized in inv") 

781 iA[...,:,0] = cross(A[...,1,:], A[...,2,:]) 

782 iA[...,:,1] = cross(A[...,2,:], A[...,0,:]) 

783 iA[...,:,2] = cross(A[...,0,:], A[...,1,:]) 

784 if A.shape[:-2] == (): 

785 iA = iA.squeeze() 

786 iA /= detA 

787 return iA 

788 

789def dinv(A:FloatArray, iA:FloatArray|None = None )-> FloatArray: 

790 """Compute the derivative of the inverse matrix with respect to A. 

791 

792 Returns R[..., i, j, k, l] = d(A^{-1}_{ij}) / dA_{kl}. 

793 

794 Parameters 

795 ---------- 

796 A : FloatArray 

797 Tensor collection of shape `(..., 2, 2)`, `(..., 3, 3)`, or `(..., 3, 2)`. 

798 iA : FloatArray or None, optional 

799 Precalculated inverse of A. Defaults to None. 

800 

801 Returns 

802 ------- 

803 FloatArray 

804 Fourth-order derivative tensor. 

805 """ 

806 if A.shape[-2:] == (3,2): 

807 im = inv(metric(A)) 

808 diA = np.empty(A.shape[:-2] + (2,3,3,2)) 

809 diA = -np.einsum("...ab,...de,...jd,...ie->...aijb", im, im, A, A ) 

810 diA -= np.einsum("...ad,...be,...jd,...ie->...aijb", im, im, A, A ) 

811 diA += np.einsum("...ab, ...ij -> ...aijb", im, np.eye(3)) 

812 return diA 

813 iA = inv(A) 

814 return -np.einsum("...ac,...db->...abcd", iA, iA) 

815 

816def invt(A:FloatArray, detA:FloatArray|npfloat|None = None) -> FloatArray: 

817 """Compute the inverse-transpose (or pseudo-inverse transpose) of a batch of matrices. 

818 

819 Parameters 

820 ---------- 

821 A : FloatArray 

822 Tensor array of shape `(..., 2, 2)`, `(..., 3, 3)`, or `(..., 3, 2)`. 

823 detA : FloatArray or npfloat, optional 

824 Precomputed determinant. Defaults to None. 

825 

826 Returns 

827 ------- 

828 FloatArray 

829 Inverse transposed matrices of shape `(..., N, M)`. 

830 """ 

831 return np.swapaxes(inv(A, detA), -1,-2) 

832 

833def dinv_sym(A, iA = None) -> FloatArray: 

834 """Compute the derivative of A^{-1} with respect to A assuming A is symmetric. 

835 

836 Parameters 

837 ---------- 

838 A : FloatArray 

839 Symmetric tensor collection of shape `(..., 2, 2)` or `(..., 3, 3)`. 

840 iA : FloatArray or None, optional 

841 Precomputed inverse of A. Defaults to None. 

842 

843 Returns 

844 ------- 

845 FloatArray 

846 Symmetric fourth-order derivative tensor. 

847 

848 Raises 

849 ------ 

850 ValueError 

851 If trailing dimensions are not (2, 2) or (3, 3). 

852 """ 

853 if A.shape[-2:] in [(3,3), (2,2)]: 

854 if iA is None : 

855 iA = inv(A) 

856 return -0.5*cross_iklj(iA, iA) -0.5*cross_ilkj(iA, iA) 

857 raise ValueError(" A.shape[-2:] must be (2,2) or (3,3)") 

858 

859def dinvt(F, iFT =None, im = None, dimdF = None)->FloatArray: 

860 """Compute the derivative of the inverse-transpose tensor F^{-T} with respect to F. 

861 

862 Parameters 

863 ---------- 

864 F : FloatArray 

865 Input deformation tensor of shape `(..., 2, 2)`, `(..., 3, 3)`, or `(..., 3, 2)`. 

866 iFT : FloatArray, optional 

867 Precomputed F^{-T}. Defaults to None. 

868 im : FloatArray, optional 

869 Precomputed inverse metric (F^T F)^{-1} for 3x2 tensors. Defaults to None. 

870 dimdF : FloatArray, optional 

871 Precomputed derivative of the inverse metric. Defaults to None. 

872 

873 Returns 

874 ------- 

875 FloatArray 

876 Fourth-order derivative tensor. 

877 """ 

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

879 I = np.eye(3) 

880 im = invmetric(F) if im is None else im 

881 dimdF = dinvmetric(F) if dimdF is None else dimdF 

882 return np.einsum("...ij, ...ba -> ...iajb", I, im) \ 

883 + np.einsum("...il, ...lajb-> ...iajb", F, dimdF) 

884 iFT = invt(F) if iFT is None else iFT 

885 return -cross_ilkj(iFT, iFT) 

886 

887def sqrt22_numpy(A: np.ndarray, out=None) -> np.ndarray: 

888 """Compute tensor square roots of 2x2 matrices using vectorized NumPy views. 

889 

890 Parameters 

891 ---------- 

892 A : np.ndarray 

893 Array of 2x2 matrices of shape `(..., 2, 2)`. 

894 out : np.ndarray, optional 

895 Output buffer modified in place. Defaults to None. 

896 

897 Returns 

898 ------- 

899 np.ndarray 

900 Tensor square roots of shape `(..., 2, 2)`. 

901 

902 Raises 

903 ------ 

904 ValueError 

905 If trailing dimensions are not (2, 2). 

906 """ 

907 if A.shape[-2:] != (2, 2): 

908 raise ValueError("A.shape[-2:] should be (2, 2)") 

909 if out is None: 

910 out = np.empty_like(A) 

911 A00, A01 = A[..., 0, 0], A[..., 0, 1] 

912 A10, A11 = A[..., 1, 0], A[..., 1, 1] 

913 

914 detA = A00 * A11 - A01 * A10 

915 traA = A00 + A11 

916 

917 s = np.sqrt(detA) 

918 t = np.sqrt(traA + 2.0 * s) 

919 

920 # Ajout d'axes pour le broadcasting de la division 

921 # s et t passent de shape (Batch,) à (Batch, 1, 1) 

922 t_expand = t[..., np.newaxis, np.newaxis] 

923 

924 # Calcul en place 

925 # On commence par copier A dans out pour ne pas l'altérer 

926 out[...] = A 

927 # On ajoute s sur la diagonale 

928 out[..., 0, 0] += s 

929 out[..., 1, 1] += s 

930 # On divise tout le tenseur par t 

931 out[...] /= t_expand 

932 return out 

933 

934@nb.njit(cache=True, fastmath = FASTMATH) 

935def sqrt22_kernel(A22, out): 

936 """Numba scalar kernel computing the square root of an individual 2x2 matrix. 

937 

938 Parameters 

939 ---------- 

940 A22 : ndarray 

941 Single 2x2 matrix slice. 

942 out : ndarray 

943 Output 2x2 buffer receiving the result. 

944 """ 

945 A00 = A22[0, 0] 

946 A01 = A22[0, 1] 

947 A10 = A22[1, 0] 

948 A11 = A22[1, 1] 

949 # 1. Déterminant et s 

950 detA = A00 * A11 - A01 * A10 

951 s = np.sqrt(detA) 

952 # 2. Trace et t 

953 traA = A00 + A11 

954 t = np.sqrt(traA + 2.0 * s) 

955 # 3. Calcul final : B = (A + s * I) / t 

956 out[0, 0] = (A00 + s) / t 

957 out[0, 1] = A01 / t 

958 out[1, 0] = A10 / t 

959 out[1, 1] = (A11 + s) / t 

960 

961@guvectorize([ 

962 (float64[:,:], float64[:,:]), 

963 (float32[:,:], float32[:,:]) 

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

965def _sqrt22_guvec(A, out): 

966 """Numba Generalized Ufunc wrapper for batch 2x2 tensor square root. 

967 

968 Parameters 

969 ---------- 

970 A : ndarray 

971 Input matrix slices of shape `(m, m)`. 

972 out : ndarray 

973 Output matrix slices of shape `(m, m)`. 

974 """ 

975 sqrt22_kernel(A, out) 

976 

977def sqrt22_guvec(A: np.ndarray, out=None) -> np.ndarray: 

978 """Compute tensor square roots of 2x2 matrices using Numba guvectorize. 

979 

980 Parameters 

981 ---------- 

982 A : np.ndarray 

983 Array of matrices of shape `(..., 2, 2)`. 

984 out : np.ndarray, optional 

985 Preallocated destination array. Defaults to None. 

986 

987 Returns 

988 ------- 

989 np.ndarray 

990 Tensor square roots of shape `(..., 2, 2)`. 

991 

992 Raises 

993 ------ 

994 ValueError 

995 If trailing dimensions are not (2, 2). 

996 """ 

997 if A.shape[-2:] != (2, 2): 

998 raise ValueError("A.shape[-2:] should be (2, 2)") 

999 if out is None: 

1000 out = np.empty_like(A) 

1001 _sqrt22_guvec(A, out=out) # type: ignore 

1002 return out 

1003 

1004 

1005def sqrt22_njit(A: np.ndarray, out=None) -> np.ndarray: 

1006 """ 

1007 Calcule la racine carrée d'un batch de tenseurs 2x2. 

1008 A = B.B 

1009 """"""Compute tensor square roots of 2x2 matrices using multithreaded Numba NJIT. 

1010 

1011 Parameters 

1012 ---------- 

1013 A : np.ndarray 

1014 C-contiguous array of matrices of shape `(..., 2, 2)`. 

1015 out : np.ndarray, optional 

1016 Preallocated destination array. Defaults to None. 

1017 

1018 Returns 

1019 ------- 

1020 np.ndarray 

1021 Tensor square roots of shape `(..., 2, 2)`. 

1022 

1023 Raises 

1024 ------ 

1025 ValueError 

1026 If A is not C-contiguous or its shape is not `(..., 2, 2)`. 

1027 """ 

1028 if not A.flags.c_contiguous : 

1029 raise ValueError("A not contiguous") 

1030 if A.shape[-2:] != (2, 2): 

1031 raise ValueError("A.shape[-2:] should be (2, 2)") 

1032 

1033 if out is None: 

1034 out = np.empty_like(A) 

1035 Aview = A.reshape((-1, 2, 2)) 

1036 outview = out.reshape((-1, 2, 2)) 

1037 _sqrt22_pnjit(Aview, out=outview) # type: ignore 

1038 return out 

1039 

1040@nb.njit(parallel=True, cache = True, fastmath = FASTMATH) 

1041def _sqrt22_pnjit(A, out): 

1042 """Numba parallel loop over flattened batch of 2x2 matrices. 

1043 

1044 Parameters 

1045 ---------- 

1046 A : ndarray 

1047 Array of shape `(N, 2, 2)`. 

1048 out : ndarray 

1049 Output array of shape `(N, 2, 2)`. 

1050 

1051 Returns 

1052 ------- 

1053 ndarray 

1054 Output buffer `out`. 

1055 """ 

1056 for i in nb.prange(len(A)): # type ignore # pylint: disable=E1133 

1057 sqrt22_kernel(A[i], out[i]) 

1058 return out 

1059 

1060def sqrt22(A, out = None): 

1061 """Compute square roots for a batch of 2x2 matrices (dispatcher). 

1062 

1063 Parameters 

1064 ---------- 

1065 A : np.ndarray 

1066 Array of matrices of shape `(..., 2, 2)`. 

1067 out : np.ndarray, optional 

1068 Destination buffer. Defaults to None. 

1069 

1070 Returns 

1071 ------- 

1072 np.ndarray 

1073 Tensor square root array. 

1074 """ 

1075 return sqrt22_njit(A, out) 

1076 #return sqrt22_guvec(A, out) 

1077 

1078@nb.njit(cache =True) 

1079def sysmv22_kernel(A: FloatArray, b: FloatArray, out : FloatArray): 

1080 """Solve a single symmetric 2x2 linear system A @ x = b via Cramer's rule. 

1081 

1082 Parameters 

1083 ---------- 

1084 A : FloatArray 

1085 Symmetric 2x2 matrix. 

1086 b : FloatArray 

1087 Right-hand side 2D vector. 

1088 out : FloatArray 

1089 Output 2D vector receiving solution x. 

1090 """ 

1091 A00, A01, A11 = A[0,0], A[0,1], A[1,1] 

1092 b0, b1 = b 

1093 detA = A00*A11 - A01*A01 

1094 out[0] = (A11*b0 - A01*b1)/detA 

1095 out[1] = (A00*b1 - A01*b0)/detA 

1096 

1097@guvectorize([ 

1098 (float64[:,:], float64[:], float64[:]), 

1099 (float32[:,:], float32[:], float32[:]) 

1100], '(dim, dim),(dim), (dim)', nopython=True, cache=True) 

1101def sysmv22_guvec(A, b, out : FloatArray): 

1102 """Generalized Ufunc solving batches of symmetric 2x2 systems A @ x = b. 

1103 

1104 Parameters 

1105 ---------- 

1106 A : ndarray 

1107 Batch of 2x2 matrices of shape `(..., 2, 2)`. 

1108 b : ndarray 

1109 Batch of vectors of shape `(..., 2)`. 

1110 out : ndarray 

1111 Buffer receiving solutions of shape `(..., 2)`. 

1112 """ 

1113 sysmv22_kernel(A, b, out) 

1114 

1115@nb.njit(parallel = True, cache =True) 

1116def sysmv22_pnjit(A: FloatArray, b: FloatArray, out : FloatArray): 

1117 """Parallel Numba kernel solving batches of symmetric 2x2 systems A @ x = b. 

1118 

1119 Parameters 

1120 ---------- 

1121 A : FloatArray 

1122 Batch of 2x2 matrices of shape `(N, 2, 2)`. 

1123 b : FloatArray 

1124 Batch of right-hand side vectors of shape `(N, 2)`. 

1125 out : FloatArray 

1126 Output buffer receiving solutions of shape `(N, 2)`. 

1127 """ 

1128 for j in nb.prange(A.shape[0]): # pylint: disable=not-an-iterable 

1129 sysmv22_kernel(A[j], b[j], out[j]) 

1130 

1131def sysmv22(A: FloatArray, b: FloatArray, 

1132 out:Optional[FloatArray] = None, parallel = False) -> FloatArray: 

1133 r""" 

1134 Solves the linear system $A x = b$ for a symmetric 2x2 matrix 

1135 using explicit Cramer's rule inversion. 

1136 Math: $x = A^{-1} b$ 

1137 

1138 Parameters 

1139 ---------- 

1140 A : FloatArray 

1141 Batch of symmetric 2x2 matrices of shape `(..., 2, 2)`. 

1142 b : FloatArray 

1143 Right-hand side vectors of shape `(..., 2)`. 

1144 out : FloatArray, optional 

1145 Preallocated output buffer. Defaults to None. 

1146 parallel : bool, optional 

1147 If True, evaluates using multithreaded OpenMP-style execution. Defaults to False. 

1148 

1149 Returns 

1150 ------- 

1151 FloatArray 

1152 Solution vectors of shape `(..., 2)`. 

1153 

1154 Raises 

1155 ------ 

1156 ValueError 

1157 If trailing dimensions of A are not (2, 2). 

1158 """ 

1159 if A.shape[-2:] != (2,2) : 

1160 raise ValueError('sysmv22 is for 2x2 batch of tensors' ) 

1161 out = np.empty( b.shape, npfloat) if out is None else out 

1162 if parallel : 

1163 sysmv22_pnjit(A, b, out) 

1164 else : 

1165 sysmv22_guvec(A, b, out) 

1166 return out 

1167 

1168def gemm22(A: FloatArray, B: FloatArray, out:Optional[FloatArray] = None) -> FloatArray: 

1169 r""" 

1170 Compute matrix-matrix products C = A @ B for batches of 2x2 matrices. 

1171 

1172 Optimized for zero heap reallocations using views and in-place accumulations. 

1173 

1174 Parameters 

1175 ---------- 

1176 A : FloatArray 

1177 First batch of 2x2 matrices of shape `(..., 2, 2)`. 

1178 B : FloatArray 

1179 Second batch of 2x2 matrices of shape `(..., 2, 2)`. 

1180 out : FloatArray, optional 

1181 Preallocated array receiving the result. Defaults to None. 

1182 

1183 Returns 

1184 ------- 

1185 FloatArray 

1186 Matrix product array of shape `(..., 2, 2)`. 

1187 

1188 Raises 

1189 ------ 

1190 ValueError 

1191 If trailing dimensions are not (2, 2). 

1192 """ 

1193 if A.shape[-2:] != (2,2) : 

1194 raise ValueError('gemm22 is for 2x2 batch of tensors' ) 

1195 C = np.empty_like(A) if out is None else out 

1196 # 1. Create Views (0 allocations) 

1197 A00, A01 = A[..., 0, 0], A[..., 0, 1] 

1198 A10, A11 = A[..., 1, 0], A[..., 1, 1] 

1199 B00, B01 = B[..., 0, 0], B[..., 0, 1] 

1200 B10, B11 = B[..., 1, 0], B[..., 1, 1] 

1201 # 2. Allocate EXACTLY ONE temporary buffer for the batch 

1202 # This shape matches the batch dimensions (e.g., if A is 1000x2x2, temp is 1000) 

1203 temp = np.empty_like(A00) 

1204 # --- Compute C00 --- 

1205 # C00 = A00*B00 

1206 np.multiply(A00, B00, out=C[..., 0, 0]) 

1207 # temp = A01*B10 

1208 np.multiply(A01, B10, out=temp) 

1209 # C00 += temp (In-place addition!) 

1210 C[..., 0, 0] += temp 

1211 # --- Compute C01 --- 

1212 np.multiply(A00, B01, out=C[..., 0, 1]) 

1213 np.multiply(A01, B11, out=temp) 

1214 C[..., 0, 1] += temp 

1215 # --- Compute C10 --- 

1216 np.multiply(A10, B00, out=C[..., 1, 0]) 

1217 np.multiply(A11, B10, out=temp) 

1218 C[..., 1, 0] += temp 

1219 # --- Compute C11 --- 

1220 np.multiply(A10, B01, out=C[..., 1, 1]) 

1221 np.multiply(A11, B11, out=temp) 

1222 C[..., 1, 1] += temp 

1223 return C 

1224 

1225@nb.njit(cache=True, fastmath = True, inline='always') 

1226def gemm22_kernel_njit(A:FloatArray, B: FloatArray, out: FloatArray): 

1227 """Inlined Numba scalar kernel computing the product of two 2x2 matrices. 

1228 

1229 Parameters 

1230 ---------- 

1231 A : FloatArray 

1232 2x2 left-hand matrix slice. 

1233 B : FloatArray 

1234 2x2 right-hand matrix slice. 

1235 out : FloatArray 

1236 2x2 destination buffer. 

1237 """ 

1238 A00, A01, A10, A11 = A[0,0], A[0,1], A[1,0], A[1,1] 

1239 B00, B01, B10, B11 = B[0,0], B[0,1], B[1,0], B[1,1] 

1240 out[0,0] = A00*B00 + A01*B10 

1241 out[0,1] = A00*B01 + A01*B11 

1242 out[1,0] = A10*B00 + A11*B10 

1243 out[1,1] = A10*B01 + A11*B11 

1244 

1245def AAT22(A, out = None): 

1246 """Compute the symmetric product B = A @ A^T for a batch of 2x2 matrices. 

1247 

1248 Parameters 

1249 ---------- 

1250 A : ndarray 

1251 Batch of 2x2 matrices of shape `(..., 2, 2)`. 

1252 out : ndarray, optional 

1253 Preallocated array receiving the result. Defaults to None. 

1254 

1255 Returns 

1256 ------- 

1257 ndarray 

1258 Symmetric matrix product array of shape `(..., 2, 2)`. 

1259 """ 

1260 if out is None: 

1261 B = np.empty_like(A) 

1262 else: 

1263 B = out 

1264 #B = np.empty_like(A) 

1265 A00 = A[...,0,0] 

1266 A01 = A[...,0,1] 

1267 A10 = A[...,1,0] 

1268 A11 = A[...,1,1] 

1269 B[...,0,0] = A00*A00 + A01*A01 

1270 B[...,0,1] = A00*A10 + A01*A11 

1271 B[...,1,0] = A10*A00 + A11*A01 

1272 B[...,1,1] = A10*A10 + A11*A11 

1273 return B 

1274 

1275@nb.njit(cache=True, fastmath = False) 

1276def AAT22_kernel(A22, out): 

1277 """Numba kernel computing A @ A^T for an individual 2x2 matrix. 

1278 

1279 Parameters 

1280 ---------- 

1281 A22 : ndarray 

1282 Single 2x2 matrix slice. 

1283 out : ndarray 

1284 Output 2x2 buffer. 

1285 """ 

1286 A00 = A22[0, 0] 

1287 A01 = A22[0, 1] 

1288 A10 = A22[1, 0] 

1289 A11 = A22[1, 1] 

1290 out[0, 0] = A00*A00 + A01*A01 

1291 out[0, 1] = A00*A10 + A01*A11 

1292 out[1, 0] = A10*A00 + A11*A01 

1293 out[1, 1] = A10*A10 + A11*A11 

1294 

1295@guvectorize([ 

1296 (float64[:,:], float64[:,:]), 

1297 (float32[:,:], float32[:,:]) 

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

1299def _AAT22_guvec(A, out): 

1300 """Numba Generalized Ufunc computing A @ A^T across matrix slices. 

1301 

1302 Parameters 

1303 ---------- 

1304 A : ndarray 

1305 Input matrix slices of shape `(m, m)`. 

1306 out : ndarray 

1307 Output matrix slices of shape `(m, m)`. 

1308 """ 

1309 AAT22_kernel(A, out) 

1310 

1311@nb.njit(parallel = True, cache =True, fastmath = FASTMATH) 

1312def _AAT22_pnjit(A, out): 

1313 """Parallel Numba kernel computing A @ A^T over an array of matrices. 

1314 

1315 Parameters 

1316 ---------- 

1317 A : ndarray 

1318 Batch of matrices of shape `(N, 2, 2)`. 

1319 out : ndarray 

1320 Output buffer of shape `(N, 2, 2)`. 

1321 """ 

1322 for j in nb.prange(A.shape[0]): # pylint: disable=not-an-iterable 

1323 AAT22_kernel(A[j], out[j]) 

1324 

1325 

1326# Wrapper Python propre 

1327def AAT22_guvec(A: np.ndarray, out=None) -> np.ndarray: 

1328 """Compute A @ A^T for batches of 2x2 matrices via Numba guvectorize. 

1329 

1330 Parameters 

1331 ---------- 

1332 A : np.ndarray 

1333 Array of matrices of shape `(..., 2, 2)`. 

1334 out : np.ndarray, optional 

1335 Destination buffer. Defaults to None. 

1336 

1337 Returns 

1338 ------- 

1339 np.ndarray 

1340 Symmetric matrix products of shape `(..., 2, 2)`. 

1341 """ 

1342 if out is None: 

1343 out = np.empty_like(A) 

1344 _AAT22_guvec(A, out=out) # type: ignore 

1345 return out 

1346 

1347AAT22 = AAT22_guvec 

1348 

1349def _cross_ijkl(A:FloatArray, B: FloatArray) -> FloatArray: 

1350 """Compute the fourth-order tensor outer product C_{ijkl} = A_{ij} * B_{kl}. 

1351 

1352 Parameters 

1353 ---------- 

1354 A : FloatArray 

1355 First second-order tensor batch of shape `(..., N, N)`. 

1356 B : FloatArray 

1357 Second second-order tensor batch of shape `(..., N, N)`. 

1358 

1359 Returns 

1360 ------- 

1361 FloatArray 

1362 Fourth-order tensor batch of shape `(..., N, N, N, N)`. 

1363 """ 

1364 return np.einsum("...ij, ...kl-> ...ijkl", A, B) 

1365 

1366cross_ijkl = _cross_ijkl 

1367 

1368def cross_ikjl(A:FloatArray, B: FloatArray) -> FloatArray: 

1369 """Compute the fourth-order tensor product with index permutation C_{ijkl} = A_{ik} * B_{jl}. 

1370 

1371 Parameters 

1372 ---------- 

1373 A : FloatArray 

1374 First second-order tensor batch of shape `(..., N, N)`. 

1375 B : FloatArray 

1376 Second second-order tensor batch of shape `(..., N, N)`. 

1377 

1378 Returns 

1379 ------- 

1380 FloatArray 

1381 Fourth-order tensor batch of shape `(..., N, N, N, N)`. 

1382 """ 

1383 return np.einsum("...ik, ...jl-> ...ijkl", A, B) 

1384 

1385def cross_iklj(A:FloatArray, B: FloatArray) -> FloatArray: 

1386 """Compute the fourth-order tensor product with index permutation C_{ijkl} = A_{ik} * B_{lj}. 

1387 

1388 Parameters 

1389 ---------- 

1390 A : FloatArray 

1391 First second-order tensor batch of shape `(..., N, N)`. 

1392 B : FloatArray 

1393 Second second-order tensor batch of shape `(..., N, N)`. 

1394 

1395 Returns 

1396 ------- 

1397 FloatArray 

1398 Fourth-order tensor batch of shape `(..., N, N, N, N)`. 

1399 """ 

1400 return np.einsum("...ik, ...lj-> ...ijkl", A, B) 

1401 

1402def cross_ilkj(A:FloatArray, B: FloatArray) -> FloatArray: 

1403 """Compute the fourth-order tensor product with index permutation C_{ijkl} = A_{il} * B_{kj}. 

1404 

1405 Parameters 

1406 ---------- 

1407 A : FloatArray 

1408 First second-order tensor batch of shape `(..., N, N)`. 

1409 B : FloatArray 

1410 Second second-order tensor batch of shape `(..., N, N)`. 

1411 

1412 Returns 

1413 ------- 

1414 FloatArray 

1415 Fourth-order tensor batch of shape `(..., N, N, N, N)`. 

1416 """ 

1417 return np.einsum("...il, ...kj-> ...ijkl", A, B) 

1418 

1419def cross_likj(A:FloatArray, B: FloatArray) -> FloatArray: 

1420 """Compute the fourth-order tensor product with index permutation C_{ijkl} = A_{li} * B_{kj}. 

1421 

1422 Parameters 

1423 ---------- 

1424 A : FloatArray 

1425 First second-order tensor batch of shape `(..., N, N)`. 

1426 B : FloatArray 

1427 Second second-order tensor batch of shape `(..., N, N)`. 

1428 

1429 Returns 

1430 ------- 

1431 FloatArray 

1432 Fourth-order tensor batch of shape `(..., N, N, N, N)`. 

1433 """ 

1434 AijBkl= cross_ijkl(A,B) 

1435 axis = list(range(AijBkl.ndim-4)) + [-3,-1,-2,-4] 

1436 AliBkj = AijBkl.transpose(axis) 

1437 return AliBkj 

1438 

1439def cross_iljk(A:FloatArray, B: FloatArray) -> FloatArray: 

1440 """Compute the fourth-order tensor product with index permutation C_{ijkl} = A_{il} * B_{jk}. 

1441 

1442 Parameters 

1443 ---------- 

1444 A : FloatArray 

1445 First second-order tensor batch of shape `(..., N, N)`. 

1446 B : FloatArray 

1447 Second second-order tensor batch of shape `(..., N, N)`. 

1448 

1449 Returns 

1450 ------- 

1451 FloatArray 

1452 Fourth-order tensor batch of shape `(..., N, N, N, N)`. 

1453 """ 

1454 AijBkl = cross_ijkl(A,B) 

1455 axis = list(range(AijBkl.ndim-4)) + [-4,-2,-1,-3] 

1456 AilBjk = AijBkl.transpose(axis) 

1457 return AilBjk 

1458 

1459def df_num( f:Callable[[FloatArray], FloatArray|float], A_in:FloatArray|float, 

1460 shape_iA:tuple[int,...], shape_ifA:tuple[int,...])-> FloatArray: 

1461 """Compute numerical Jacobian derivatives of tensor functions via finite differences. 

1462 

1463 Uses `scipy.optimize.approx_fprime` iteratively over batch dimensions. 

1464 

1465 Parameters 

1466 ---------- 

1467 f : Callable[[FloatArray], FloatArray or float] 

1468 Tensor function to differentiate. 

1469 A_in : FloatArray or float 

1470 Evaluation point or batch of evaluation points. 

1471 shape_iA : tuple of int 

1472 Shape of a single input argument to `f`. 

1473 shape_ifA : tuple of int 

1474 Shape of a single output evaluation from `f`. 

1475 

1476 Returns 

1477 ------- 

1478 FloatArray 

1479 Numerical derivative tensor of shape `shape_coll + shape_ifA + shape_iA`. 

1480 """ 

1481 A = np.atleast_1d(A_in) 

1482 dim_coll = A.ndim - len(shape_iA) 

1483 shape_coll = A.shape[:dim_coll] 

1484 shape_idfA = shape_ifA + shape_iA 

1485 df_num_res = np.zeros(shape_coll+shape_idfA) 

1486 def fflat(iAflat): 

1487 fiA = f(iAflat.reshape(shape_iA)) 

1488 if isinstance(fiA, np.ndarray) : 

1489 fiA = fiA.flatten() 

1490 return fiA 

1491 for k in np.ndindex(A.shape[:dim_coll]): 

1492 Ak = A[k] 

1493 df_num_res[k] = sp.optimize.approx_fprime(Ak.flatten(), fflat).reshape(shape_idfA) 

1494 if not isinstance(A_in, np.ndarray) : 

1495 df_num_res = df_num_res.squeeze() 

1496 return df_num_res 

1497 

1498def check_relerr(ref:FloatArray|npfloat, val:FloatArray|npfloat, innershape, 

1499 reltol:float = 1.e-4, abstol:float = 1.e-5, 

1500 checkname:str="", verbose:bool = True )-> bool: 

1501 """Compare two tensor arrays elementwise using absolute and relative error tolerances. 

1502 

1503 Parameters 

1504 ---------- 

1505 ref : FloatArray or npfloat 

1506 Reference ground-truth values. 

1507 val : FloatArray or npfloat 

1508 Computed values to validate. 

1509 innershape : tuple of int 

1510 Shape of each tensor element inside the collection. 

1511 reltol : float, optional 

1512 Maximum allowed relative error. Defaults to 1e-4. 

1513 abstol : float, optional 

1514 Maximum allowed absolute error. Defaults to 1e-5. 

1515 checkname : str, optional 

1516 Name or label describing the test. Defaults to "". 

1517 verbose : bool, optional 

1518 If True, prints detailed diagnostics upon failure. Defaults to True. 

1519 

1520 Returns 

1521 ------- 

1522 bool 

1523 True if all slices pass within error tolerances, False otherwise. 

1524 """ 

1525 ref = np.asarray(ref).reshape((-1,)+ innershape) 

1526 val = np.asarray(val).reshape((-1,)+ innershape) 

1527 n = ref.shape[0] 

1528 for i in range(n): 

1529 err = np.linalg.norm((ref[i]-val[i])) 

1530 refnorm = np.linalg.norm(ref) 

1531 if err > abstol and err > refnorm*reltol : 

1532 if verbose: 

1533 print(f"check {checkname} Failed, for abstol = {abstol:.4e}, reltol {reltol:.4e}" ) 

1534 print(f" index {i}, error norm : {err :.4e}" + \ 

1535 f" relative error norm : {err/refnorm:.4e}") 

1536 return False 

1537 return True 

1538 

1539def check_df(f, df, x, ifshape, ixshape, verbose = False): 

1540 """Verify an analytical derivative against finite-difference approximation. 

1541 

1542 Parameters 

1543 ---------- 

1544 f : Callable 

1545 Original function. 

1546 df : Callable 

1547 Analytical derivative function. 

1548 x : FloatArray 

1549 Evaluation inputs. 

1550 ifshape : tuple of int 

1551 Output shape of f for a single input item. 

1552 ixshape : tuple of int 

1553 Input shape for a single item. 

1554 verbose : bool, optional 

1555 If True, prints verification mismatch details. Defaults to False. 

1556 

1557 Returns 

1558 ------- 

1559 bool 

1560 True if the analytical and numerical gradients agree, False otherwise. 

1561 """ 

1562 dfx_exa = df(x) 

1563 dfx_num = df_num(f, x, ixshape, ifshape) 

1564 return check_relerr(dfx_exa, dfx_num, ifshape+ixshape, verbose = verbose) 

1565 

1566# The following block needs mygrad library ... 

1567# def df_vector_auto(Fun, X): 

1568# """ compute the auto derivative of fun(A) at A where A is a vector and fun(A) a vector""" 

1569# if not HAS_MYGRAD: 

1570# frame = inspect.currentframe() 

1571# fun_name = frame.f_code.co_name if frame else "df_vector_auto" 

1572# raise ImportError(f"mygrad library is needed to run {fun_name}") 

1573# else: 

1574# 

1575# dim = X.shape[-1] 

1576# dFundX_shape = X.shape + (dim,) 

1577# dFundX = np.zeros(dFundX_shape) 

1578 

1579# for i in range(dim): 

1580# mgX = mgTensor(X) 

1581# fxi = Fun(mgX)[..., i] 

1582# fxi.backward() 

1583# dFundX[...,i,:] = np.array(mgX.grad) 

1584# return dFundX 

1585 

1586# def df_tensor_auto(Fun, X, shape_fun=None): 

1587# 

1588# """ compute the auto derivative of fun(A) at A where A is a tensor and fun(A) a tensor""" 

1589# if not HAS_MYGRAD: 

1590# frame = inspect.currentframe() 

1591# fun_name = frame.f_code.co_name if frame else "df_tensor_auto" 

1592# raise ImportError(f"mygrad library is needed to run {fun_name}") 

1593# xdim0 = X.shape[-2] 

1594# xdim1 = X.shape[-1] 

1595# if shape_fun is None: 

1596# shape_fun=(xdim0, xdim1) 

1597# fdim0 = shape_fun[0] 

1598# fdim1 = shape_fun[1] 

1599# shapedfdx = X.shape[:-2] + ( fdim0, fdim1, xdim0, xdim1) 

1600# dFdX = np.empty(shapedfdx) 

1601# mgX = mgTensor(X) 

1602# for i in range(fdim0) : 

1603# for j in range(fdim1): 

1604# FX = Fun(mgX)[..., i, j] 

1605# FX.backward() 

1606# dFdX[..., i,j, :,: ] = mgX.grad 

1607# return dFdX 

1608 

1609def eigval22sym(A:FloatArray): 

1610 """Compute analytical eigenvalues for a batch of symmetric 2x2 matrices. 

1611 

1612 Parameters 

1613 ---------- 

1614 A : FloatArray 

1615 Collection of symmetric 2x2 matrices of shape `(..., 2, 2)`. 

1616 

1617 Returns 

1618 ------- 

1619 FloatArray 

1620 Eigenvalues array of shape `(..., 2)` with entries `[..., 0]` (max) and `[..., 1]` (min). 

1621 

1622 Raises 

1623 ------ 

1624 ValueError 

1625 If trailing dimensions are not (2, 2) or matrices have negative discriminant. 

1626 """ 

1627 if A.shape[-2:] != (2,2) : 

1628 raise ValueError('A must be of shape[-2:] == (2,2)') 

1629 a = A[..., 0, 0] 

1630 b = A[..., 0, 1] 

1631 c = A[..., 1, 1] 

1632 disc = (a-c)**2 + 4*b**2 

1633 if np.any(disc < 0): 

1634 raise ValueError('all 2*2 matrix in a must be positive definite') 

1635 eigsvals = np.empty(A.shape[:-2]+(2,)) 

1636 disc = np.sqrt(disc) 

1637 eigsvals[..., 0] = (a+c + disc )/2. 

1638 eigsvals[..., 1] = (a+c - disc )/2. 

1639 return eigsvals 

1640 

1641def max_eigval22sym(A:FloatArray, outlmax : Optional[FloatArray] = None): 

1642 """Compute the maximum eigenvalue for a batch of symmetric 2x2 matrices. 

1643 

1644 Parameters 

1645 ---------- 

1646 A : FloatArray 

1647 Batch of symmetric 2x2 matrices of shape `(..., 2, 2)`. 

1648 outlmax : FloatArray, optional 

1649 Output buffer receiving eigenvalues. Defaults to None. 

1650 

1651 Returns 

1652 ------- 

1653 FloatArray 

1654 Maximum eigenvalues array of shape `(...)`. 

1655 

1656 Raises 

1657 ------ 

1658 ValueError 

1659 If trailing dimensions are not (2, 2) or matrices have negative discriminant. 

1660 """ 

1661 if A.shape[-2:] != (2,2) : 

1662 raise ValueError('A must be of shape[-2:] == (2,2)') 

1663 if outlmax is None: 

1664 outlmax = np.empty(A.shape[:-2]) 

1665 a = A[..., 0, 0] 

1666 b = A[..., 0, 1] 

1667 c = A[..., 1, 1] 

1668 disc = (a-c)**2 + 4*b**2 

1669 if np.any(disc < 0): 

1670 raise ValueError('all 2*2 matrix in a must be positive definite') 

1671 

1672 disc = np.sqrt(disc) 

1673 outlmax[...] = (a+c + disc )/2. 

1674 return outlmax 

1675 

1676@nb.njit(cache=True, fastmath = FASTMATH) 

1677def max_eigval22sym_kernel_njit(A:FloatArray): 

1678 """Numba scalar kernel computing the maximum eigenvalue of a single symmetric 2x2 matrix. 

1679 

1680 Parameters 

1681 ---------- 

1682 A : FloatArray 

1683 2x2 symmetric matrix slice. 

1684 

1685 Returns 

1686 ------- 

1687 float 

1688 Maximum eigenvalue. 

1689 

1690 Raises 

1691 ------ 

1692 ValueError 

1693 If the discriminant is negative. 

1694 """ 

1695 a = A[0, 0] 

1696 b = A[0, 1] 

1697 c = A[1, 1] 

1698 disc = (a-c)**2 + 4*b**2 

1699 if disc < 0: 

1700 raise ValueError('2*2 matrix in a must be positive definite') 

1701 return (a+c + np.sqrt(disc ))/2.