lamkit 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,757 @@
1
+ '''
2
+ This is a modified version of the composipy package.
3
+ It is used to calculate the mechanical properties of a laminate.
4
+
5
+ Reference:
6
+ https://github.com/rafaelpsilva07/composipy
7
+
8
+ Author: Runze Li @ Department of Aeronautics, Imperial College London
9
+ Date: 2025-10-29
10
+ '''
11
+
12
+ import numpy as np
13
+ import pandas as pd
14
+ from typing import List, Tuple, Dict, Union, Any
15
+
16
+ from lamkit.analysis.material import Ply
17
+ from lamkit.analysis.larc05 import FAILURE_MODE_NAMES, LaRC05
18
+
19
+
20
+ class Laminate():
21
+ '''
22
+ Laminate class for Classical Lamination Theory (CLT).
23
+
24
+ Parameters
25
+ ----------
26
+ stacking : list or dict
27
+ To define a angle stacking sequence
28
+ An iterable containing the angles (in degrees) of layup.
29
+ To define a stack based on lamination parameters.
30
+ {xiA: [xiA1, xiA2, xiA3, xiA4],
31
+ xiB: [xiB1, xiB2, xiB3, xiB4],
32
+ xiD: [xiD1, xiD2, xiD3, xiD4],
33
+ T: thickness}
34
+ plies : Ply or list
35
+ A single Ply or a list of Ply object
36
+
37
+ Units
38
+ ------
39
+ Angles: degrees
40
+ Thickness: mm
41
+ Load: N
42
+ Moment: N*mm
43
+ Strain: unitless
44
+ Stress: MPa
45
+ Material properties: MPa and mm
46
+
47
+ Note
48
+ -----
49
+ The first element of the stacking list corresponds to the BOTTOM OF THE LAYUP,
50
+ and the last element corresponds to the TOP OF THE LAYUP.
51
+ This is important for non-symmetric laminates.
52
+
53
+ Refer to https://github.com/rafaelpsilva07/composipy/issues/28.
54
+ '''
55
+
56
+ def __init__(self, stacking: List[float]|Dict[str, List[float]],
57
+ plies: List[Ply]) -> None:
58
+
59
+ self.stacking = stacking
60
+
61
+ # Checking layup
62
+ if not isinstance(stacking, dict): # implements angle stacking sequence
63
+
64
+ if isinstance(plies, Ply):
65
+ n_plies = len(stacking)
66
+ plies = [plies for _ in range(n_plies)]
67
+ elif len(plies) != len(stacking):
68
+ raise ValueError('Number of plies and number of stacking must match')
69
+
70
+ xiA = None
71
+ xiB = None
72
+ xiD = None
73
+ total_thickness = sum([ply.thickness for ply in plies])
74
+ layup = list(zip(stacking, plies)) # [(angle, ply), ...]
75
+
76
+ else:
77
+ try:
78
+ xiA = stacking['xiA']
79
+ except KeyError:
80
+ xiA = None
81
+ try:
82
+ xiB = stacking['xiB']
83
+ except KeyError:
84
+ xiB = None
85
+ try:
86
+ xiD = stacking['xiD']
87
+ except KeyError:
88
+ KeyError('xiD must be a key')
89
+ try:
90
+ total_thickness = stacking['T']
91
+ except KeyError:
92
+ KeyError('T must be a key')
93
+ layup = []
94
+
95
+ self.plies = plies
96
+ self.ply_material = plies[0]._material
97
+ self.layup = layup
98
+ self._z_position = None
99
+ self._Q_layup = None
100
+ self._T_layup = None
101
+ self._A = None
102
+ self._B = None
103
+ self._D = None
104
+ self._ABD = None
105
+ self._ABD_inverse_matrix = None
106
+ self._xiA = xiA
107
+ self._xiB = xiB
108
+ self._xiD = xiD
109
+ self._total_thickness = total_thickness
110
+ self._S = None
111
+
112
+ def __repr__(self) -> str:
113
+ representation = f'Laminate\n'
114
+ representation += f'stacking = {self.stacking}'
115
+ return representation
116
+
117
+ def __eq__(self, other) -> bool:
118
+ if isinstance(other, Laminate):
119
+ return (self.layup == other.layup)
120
+ return NotImplemented
121
+
122
+ @property
123
+ def n_ply(self) -> int:
124
+ '''
125
+ Number of plies in the laminate.
126
+ '''
127
+ return len(self.plies)
128
+
129
+ @property
130
+ def stacking_sequence(self) -> List[float]:
131
+ '''
132
+ Stacking sequence (ply angle, degrees) of the laminate.
133
+ '''
134
+ return [angle for angle, _ in self.layup]
135
+
136
+ @property
137
+ def z_position(self) -> List[float]:
138
+ '''
139
+ Z coordinates of the ply surfaces in the laminate.
140
+
141
+ Returns
142
+ -------
143
+ z_position: List[float] (n_ply + 1,)
144
+ Z-position of the ply surfaces in the laminate.
145
+ '''
146
+ return np.cumsum([0] + [ply.thickness for ply in self.plies]) - self._total_thickness/2
147
+
148
+ @property
149
+ def Q_layup(self) -> List[np.ndarray]:
150
+ '''
151
+ Transformed reduced stiffness matrix of each ply in the laminate.
152
+
153
+ Returns
154
+ -------
155
+ Q_layup: List[np.ndarray [3, 3]]
156
+ Transformed reduced stiffness matrix of each ply in the laminate.
157
+ '''
158
+ if self._Q_layup is None:
159
+ self._Q_layup = [ply.get_Q_bar(theta) for theta, ply in self.layup]
160
+ return self._Q_layup
161
+
162
+ @property
163
+ def T_layup(self) -> List[np.ndarray]:
164
+ '''
165
+ Transformation matrix of each ply in the laminate.
166
+
167
+ Returns
168
+ -------
169
+ T_layup: List[Tuple[np.ndarray [3, 3], np.ndarray [3, 3]]]
170
+ Transformation matrix of each ply in the laminate.
171
+ The first ndarray is the transformation matrix for this ply.
172
+ The second ndarray is the engineering transformation matrix for this ply.
173
+ '''
174
+ if self._T_layup is None:
175
+ self._T_layup = []
176
+ for theta in self.layup:
177
+ T_real = self.ply_material.get_rotation_matrix(theta)
178
+ T_engineering = self.ply_material.get_engineering_rotation_matrix(theta)
179
+ self._T_layup.append([T_real,T_engineering])
180
+ return self._T_layup
181
+
182
+ @property
183
+ def xiA(self) -> np.ndarray:
184
+ '''
185
+ Lamination parameter xiA for extension
186
+
187
+ Returns
188
+ -------
189
+ xiA : np.ndarray (4,)
190
+ Lamination parameter xiA
191
+ '''
192
+ xiA = np.zeros(4)
193
+ T = sum([ply.thickness for ply in self.plies])
194
+ for i, angle in enumerate(self.stacking):
195
+ angle *= np.pi / 180
196
+ zk1 = self.z_position[i+1]
197
+ zk0 = self.z_position[i]
198
+
199
+ xiA[0] += (zk1-zk0) * np.cos(2*angle)
200
+ xiA[1] += (zk1-zk0) * np.sin(2*angle)
201
+ xiA[2] += (zk1-zk0) * np.cos(4*angle)
202
+ xiA[3] += (zk1-zk0) * np.sin(4*angle)
203
+
204
+ self._xiA = xiA / T
205
+ return self._xiA
206
+
207
+ @property
208
+ def xiB(self) -> np.ndarray:
209
+ '''
210
+ Lamination parameter xiB for extension-bending coupling.
211
+
212
+ Returns
213
+ -------
214
+ xiB : np.ndarray (4,)
215
+ (4/T²) Σ_k (z_{k+1}² - z_k²) [cos2θ, sin2θ, cos4θ, sin4θ] per ply k.
216
+ '''
217
+ if isinstance(self.stacking, dict):
218
+ if self._xiB is None:
219
+ raise ValueError(
220
+ 'xiB is required in stacking dict for coupling parameters'
221
+ )
222
+ return np.asarray(self._xiB, dtype=float)
223
+
224
+ xiB = np.zeros(4)
225
+ T = sum([ply.thickness for ply in self.plies])
226
+ for i, angle in enumerate(self.stacking):
227
+ angle *= np.pi / 180
228
+ zk1 = self.z_position[i+1]
229
+ zk0 = self.z_position[i]
230
+
231
+ dz2 = zk1**2 - zk0**2
232
+ xiB[0] += dz2 * np.cos(2*angle)
233
+ xiB[1] += dz2 * np.sin(2*angle)
234
+ xiB[2] += dz2 * np.cos(4*angle)
235
+ xiB[3] += dz2 * np.sin(4*angle)
236
+
237
+ self._xiB = 4 * xiB / T**2
238
+ return self._xiB
239
+
240
+ @property
241
+ def xiD(self) -> np.ndarray:
242
+ '''
243
+ Lamination parameter xiD for bending
244
+
245
+ Returns
246
+ -------
247
+ xiD : np.ndarray (4,)
248
+ Lamination parameter xiD
249
+ '''
250
+ xiD = np.zeros(4)
251
+ T = sum([ply.thickness for ply in self.plies])
252
+ for i, angle in enumerate(self.stacking):
253
+ angle *= np.pi / 180
254
+ zk1 = self.z_position[i+1]
255
+ zk0 = self.z_position[i]
256
+
257
+ xiD[0] += (zk1**3-zk0**3) * np.cos(2*angle)
258
+ xiD[1] += (zk1**3-zk0**3) * np.sin(2*angle)
259
+ xiD[2] += (zk1**3-zk0**3) * np.cos(4*angle)
260
+ xiD[3] += (zk1**3-zk0**3) * np.sin(4*angle)
261
+ self._xiD = 4 * xiD / T**3
262
+ return self._xiD
263
+
264
+ @property
265
+ def A(self) -> np.ndarray:
266
+ '''
267
+ [A] matrix of the laminate for extension.
268
+
269
+ Returns
270
+ -------
271
+ A : np.ndarray (3x3)
272
+ [A] Matrix of the laminate
273
+ '''
274
+ if self._A is None:
275
+ self._A = np.zeros(9).reshape(3,3)
276
+
277
+ for i in enumerate(self.Q_layup):
278
+ zk1 = self.z_position[i[0]+1]
279
+ zk0 = self.z_position[i[0]]
280
+ self._A += (zk1-zk0) * i[1]
281
+ return self._A
282
+
283
+ @property
284
+ def B(self) -> np.ndarray:
285
+ '''
286
+ [B] matrix of the laminate for coupling between extension and bending.
287
+
288
+ Returns
289
+ -------
290
+ B : np.ndarray (3x3)
291
+ [B] matrix of the laminate
292
+ '''
293
+ if self._B is None:
294
+ self._B = np.zeros((3,3))
295
+
296
+ for i in enumerate(self.Q_layup):
297
+ zk1 = self.z_position[i[0]+1]
298
+ zk0 = self.z_position[i[0]]
299
+ self._B += (1/2) * (zk1**2-zk0**2) * i[1]
300
+ return self._B
301
+
302
+ @property
303
+ def D(self) -> np.ndarray:
304
+ '''
305
+ [D] matrix of the laminate for bending.
306
+
307
+ Returns
308
+ -------
309
+ D : np.ndarray (3x3)
310
+ [D] matrix of the laminate
311
+ '''
312
+ if self._D is None:
313
+ self._D = np.zeros((3,3))
314
+
315
+ for i in enumerate(self.Q_layup):
316
+ zk1 = self.z_position[i[0]+1]
317
+ zk0 = self.z_position[i[0]]
318
+ self._D += (1/3) * (zk1**3-zk0**3) * i[1]
319
+ return self._D
320
+
321
+ @property
322
+ def ABD(self) -> np.ndarray:
323
+ '''
324
+ [ABD] matrix of the laminate
325
+
326
+ Returns
327
+ -------
328
+ ABD : np.ndarray (6x6)
329
+ ABD matrix of the laminate
330
+ '''
331
+ if self._ABD is None:
332
+ self._ABD = np.vstack([
333
+ np.hstack([self.A, self.B]),
334
+ np.hstack([self.B, self.D])
335
+ ])
336
+ return self._ABD
337
+
338
+ @property
339
+ def ABD_inverse_matrix(self) -> np.ndarray:
340
+ '''
341
+ Get the inverse of the ABD matrix of the laminate.
342
+ '''
343
+ if self._ABD_inverse_matrix is None:
344
+ self._ABD_inverse_matrix = np.linalg.inv(self.ABD)
345
+ return self._ABD_inverse_matrix
346
+
347
+ @property
348
+ def ABD_determinant(self) -> float:
349
+ '''
350
+ Get the determinant of the ABD matrix of the laminate.
351
+ '''
352
+ return np.linalg.det(self.ABD)
353
+
354
+ @property
355
+ def ABD_eigenvalues(self) -> np.ndarray:
356
+ '''
357
+ Calculate eigenvalues of the ABD matrix.
358
+
359
+ Returns raw eigenvalues (no normalization) that can be directly compared
360
+ across different layups with the same material.
361
+
362
+ Returns
363
+ -------
364
+ eigenvalues : np.ndarray (6,)
365
+ Eigenvalues of the ABD matrix, sorted in descending order.
366
+ Returns raw eigenvalues that can be compared directly.
367
+
368
+ Notes
369
+ -----
370
+ The ABD matrix is symmetric, so all eigenvalues are real.
371
+ The eigenvalues are sorted in descending order (largest first).
372
+ '''
373
+ # Get ABD matrix
374
+ abd = self.ABD
375
+
376
+ # Calculate eigenvalues (ABD matrix is symmetric, so eigenvalues are real)
377
+ eigenvalues = np.linalg.eigvals(abd)
378
+
379
+ # Extract real part (should be real for symmetric matrix, but handle numerical precision)
380
+ eigenvalues = np.real(eigenvalues)
381
+
382
+ # Sort eigenvalues in descending order
383
+ eigenvalues = np.sort(eigenvalues)[::-1]
384
+
385
+ return eigenvalues
386
+
387
+ @property
388
+ def in_plane_stiffness_matrix(self) -> np.ndarray:
389
+ '''
390
+ Get the in-plane stiffness matrix of the laminate.
391
+ '''
392
+ return self.A
393
+
394
+ @property
395
+ def in_plane_compliance_matrix(self) -> np.ndarray:
396
+ '''
397
+ Equivalent plane-stress compliance for a homogeneous plate (3x3).
398
+
399
+ Returns h * inv(A), i.e. S_eq = (A/h)^(-1), where h is total thickness
400
+ and A is the CLT extensional stiffness. This relates thickness-averaged
401
+ in-plane stress sigma_bar = N/h to mid-plane strain:
402
+ epsilon0 = S_eq @ sigma_bar (with N = A @ epsilon0 under pure membrane
403
+ response, B = 0 and kappa = 0).
404
+
405
+ This is NOT inv(A): the latter maps stress resultants N to epsilon0
406
+ (epsilon0 = inv(A) @ N) and has different physical dimensions.
407
+
408
+ Notes
409
+ -----
410
+ For 2D hole problems (e.g. Lekhnitskii), the solver expects the material
411
+ compliance S in epsilon = S @ sigma (Pa-level stresses). Using S_eq
412
+ matches that convention for an equivalent homogeneous laminate.
413
+ If B is non-zero, an equivalent S_eq is only an approximation.
414
+ '''
415
+ if self._S is None:
416
+ # S_eq = (A/h)^(-1) for epsilon0 = S_eq @ (N/h); see docstring.
417
+ self._S = self._total_thickness * np.linalg.inv(self.A)
418
+ return self._S
419
+
420
+
421
+ def get_mid_plane_strains(self, N: np.ndarray) -> np.ndarray:
422
+ '''
423
+ Get the mid plane strains of the laminate.
424
+
425
+ Parameters
426
+ ------------------
427
+ N: np.ndarray
428
+ Forces and moments, i.e., [Nxx, Nyy, Nxy, Mxx, Myy, Mxy].
429
+
430
+ Returns
431
+ ------------------
432
+ epsilon0: np.ndarray (6,)
433
+ Mid plane strains, i.e., [epsilon_x0, epsilon_y0, gamma_xy0, kappa_x0, kappa_y0, kappa_xy0].
434
+ '''
435
+ return self.ABD_inverse_matrix @ N
436
+
437
+ def _ply_invariants(self) -> np.ndarray:
438
+ '''[U1..U5] from the first ply (uniform-material laminates).'''
439
+ return self.ply_material.get_property('invariants')
440
+
441
+
442
+ def get_A_from_lamination_parameters(self) -> np.ndarray:
443
+ '''
444
+ Calculate the [A] matrix from the lamination parameters.
445
+
446
+ Returns
447
+ -------
448
+ A: np.ndarray (3x3)
449
+ [A] matrix of the laminate
450
+ '''
451
+ U1, U2, U3, U4, U5 = self._ply_invariants()
452
+ xi1, xi2, xi3, xi4 = self.xiA
453
+ T = self._total_thickness
454
+ A11 = T*(U1 + U2*xi1 + U3*xi3)
455
+ A12 = T*(-U3*xi3 + U4)
456
+ A13 = T*(U2*xi2/2 + U3*xi4)
457
+ A21 = T*(-U3*xi3 + U4)
458
+ A22 = T*(U1 - U2*xi1 + U3*xi3)
459
+ A23 = T*(U2*xi2/2 - U3*xi4)
460
+ A31 = T*(U2*xi2/2 + U3*xi4)
461
+ A32 = T*(U2*xi2/2 - U3*xi4)
462
+ A33 = T*(-U3*xi3 + U5)
463
+
464
+ return np.array([[A11, A12, A13],
465
+ [A21, A22, A23],
466
+ [A31, A32, A33]])
467
+
468
+ def get_B_from_lamination_parameters(self) -> np.ndarray:
469
+ '''
470
+ Calculate the [B] matrix from the lamination parameters.
471
+
472
+ Returns
473
+ -------
474
+ B: np.ndarray (3x3)
475
+ [B] matrix of the laminate
476
+ '''
477
+ _, U2, U3, _, _ = self._ply_invariants()
478
+ xi1, xi2, xi3, xi4 = self.xiB
479
+ T = self._total_thickness
480
+ fac = T**2 / 8.0
481
+ # Invariant terms proportional to U1, U4, U5 drop out: Σ_k (z_{k+1}² - z_k²) = 0.
482
+ B11 = fac * (U2*xi1 + U3*xi3)
483
+ B12 = fac * (-U3*xi3)
484
+ B13 = fac * (U2*xi2/2 + U3*xi4)
485
+ B21 = B12
486
+ B22 = fac * (-U2*xi1 + U3*xi3)
487
+ B23 = fac * (U2*xi2/2 - U3*xi4)
488
+ B31 = B13
489
+ B32 = B23
490
+ B33 = fac * (-U3*xi3)
491
+
492
+ return np.array([[B11, B12, B13],
493
+ [B21, B22, B23],
494
+ [B31, B32, B33]])
495
+
496
+ def get_D_from_lamination_parameters(self) -> np.ndarray:
497
+ '''
498
+ Calculate the [D] matrix from the lamination parameters.
499
+
500
+ Returns
501
+ -------
502
+ D: np.ndarray (3x3)
503
+ [D] matrix of the laminate
504
+ '''
505
+ U1, U2, U3, U4, U5 = self._ply_invariants()
506
+ xi1, xi2, xi3, xi4 = self.xiD
507
+ T = self._total_thickness
508
+
509
+ D11 = T**3*(U1 + U2*xi1 + U3*xi3)/12
510
+ D12 = T**3*(-U3*xi3 + U4)/12
511
+ D13 = T**3*(U2*xi2/2 + U3*xi4)/12
512
+ D21 = T**3*(-U3*xi3 + U4)/12
513
+ D22 = T**3*(U1 - U2*xi1 + U3*xi3)/12
514
+ D23 = T**3*(U2*xi2/2 - U3*xi4)/12
515
+ D31 = T**3*(U2*xi2/2 + U3*xi4)/12
516
+ D32 = T**3*(U2*xi2/2 - U3*xi4)/12
517
+ D33 = T**3*(-U3*xi3 + U5)/12
518
+
519
+ return np.array([[D11, D12, D13],
520
+ [D21, D22, D23],
521
+ [D31, D32, D33]])
522
+
523
+
524
+ def get_effective_properties(self) -> Dict[str, float]:
525
+ '''
526
+ Get the effective properties of the laminate.
527
+ '''
528
+ S_eff = self.in_plane_compliance_matrix
529
+ E11_eff = 1/S_eff[0, 0]
530
+ E22_eff = 1/S_eff[1, 1]
531
+ G12_eff = 1/S_eff[2, 2]
532
+ nu12_eff = -S_eff[0, 1] / S_eff[0, 0]
533
+ nu21_eff = -S_eff[1, 0] / S_eff[1, 1]
534
+
535
+ return {
536
+ 'E11_eff': E11_eff,
537
+ 'E22_eff': E22_eff,
538
+ 'G12_eff': G12_eff,
539
+ 'nu12_eff': nu12_eff,
540
+ 'nu21_eff': nu21_eff,
541
+ }
542
+
543
+
544
+ #* Static methods
545
+
546
+ @staticmethod
547
+ def get_epsilon0(ABD: np.ndarray, N: np.ndarray) -> np.ndarray:
548
+ '''
549
+ Get the mid plane strains of the laminate.
550
+
551
+ Parameters
552
+ ----------
553
+ ABD: np.ndarray (6x6)
554
+ ABD matrix of the laminate
555
+ (Material properties described in MPa and mm)
556
+
557
+ N: np.ndarray (6,)
558
+ Load vector, [Nxx, Nyy, Nxy, Mxx, Myy, Mxy].
559
+ Nxx, Nyy, Nxy: in-plane forces (N/mm)
560
+ Mxx, Myy, Mxy: bending moments (N)
561
+
562
+ Returns
563
+ -------
564
+ epsilon0: np.ndarray (6,)
565
+ Mid plane strains, i.e.,
566
+ [epsilon_x0, epsilon_y0, gamma_xy0, kappa_x0, kappa_y0, kappa_xy0].
567
+ '''
568
+ return np.linalg.inv(ABD) @ N
569
+
570
+ @staticmethod
571
+ def strain_xy_at_z(epsilon6: np.ndarray, z: Union[np.ndarray, float]) -> np.ndarray:
572
+ '''
573
+ Global engineering strains [ex, ey, gxy] through the thickness (CLT).
574
+
575
+ Parameters
576
+ ----------
577
+ epsilon6 : np.ndarray (6,)
578
+ Mid-plane generalised strains
579
+ [ex0, ey0, gxy0, kx, ky, kxy].
580
+ z : np.ndarray or float
581
+ Through-thickness coordinate(s) (mm), same convention as z_position.
582
+
583
+ Returns
584
+ -------
585
+ epsilon_xy : np.ndarray (n_z, 3)
586
+ Rows are [ex, ey, gxy] at each z.
587
+ '''
588
+ zv = np.atleast_1d(np.asarray(z, dtype=float))
589
+ e0 = np.asarray(epsilon6[:3], dtype=float)
590
+ k = np.asarray(epsilon6[3:6], dtype=float)
591
+ return e0 + zv[:, np.newaxis] * k
592
+
593
+ @staticmethod
594
+ def strain_xy_global_to_material(epsilon_xy: np.ndarray, theta_deg: float) -> np.ndarray:
595
+ '''
596
+ Transform engineering strains from plate x-y to ply material 1-2.
597
+
598
+ Same convention as get_epsilon_plies_123 / NASA handbook.
599
+ '''
600
+ v = np.asarray(epsilon_xy, dtype=float).reshape(3).copy()
601
+ th = np.radians(theta_deg)
602
+ c, s = np.cos(th), np.sin(th)
603
+ v[2] /= 2.0
604
+ T = np.array(
605
+ [
606
+ [c**2, s**2, 2 * c * s],
607
+ [s**2, c**2, -2 * c * s],
608
+ [-c * s, c * s, c**2 - s**2],
609
+ ]
610
+ )
611
+ e123 = T @ v
612
+ e123 = np.asarray(e123, dtype=float).copy()
613
+ e123[2] *= 2.0
614
+ return e123
615
+
616
+ @staticmethod
617
+ def stress_xy_global_from_strain(epsilon_xy: np.ndarray, Q_bar: np.ndarray) -> np.ndarray:
618
+ '''Global stresses [sx, sy, txy] from global strains and transformed stiffness [Q_bar].'''
619
+ exy = np.asarray(epsilon_xy, dtype=float).reshape(3)
620
+ return Q_bar @ exy
621
+
622
+ @staticmethod
623
+ def stress_material_from_strain(
624
+ epsilon_xy: np.ndarray, Q_material: np.ndarray, theta_deg: float
625
+ ) -> np.ndarray:
626
+ '''Material stresses [s1, s2, t12] from global strains and ply [Q] in material axes.'''
627
+ e123 = Laminate.strain_xy_global_to_material(epsilon_xy, theta_deg)
628
+ return Q_material @ e123
629
+
630
+
631
+ def get_ply_level_results(self, epsilon0: np.ndarray, larc05: LaRC05) -> List[Dict[str, Any]]:
632
+ '''
633
+ Get the ply-level results of the laminate.
634
+
635
+ Parameters
636
+ ----------
637
+ epsilon0: np.ndarray (6,)
638
+ Mid-plane strains, i.e.,
639
+ `[epsilon_x0, epsilon_y0, gamma_xy0, kappa_x0, kappa_y0, kappa_xy0]`.
640
+ larc05: LaRC05
641
+ LaRC05 object.
642
+
643
+ Returns
644
+ -------
645
+ results: List[Dict[str, Any]]
646
+ List of dictionaries, each containing the results for a ply.
647
+ Length is `2*n_ply`.
648
+ '''
649
+ z_pos = self.z_position
650
+ results = []
651
+ for index_ply in range(self.n_ply):
652
+ theta, ply_obj = self.layup[index_ply]
653
+ theta = float(theta)
654
+ z_bottom = float(z_pos[index_ply])
655
+ z_top = float(z_pos[index_ply + 1])
656
+ Q_bar = ply_obj.get_Q_bar(theta)
657
+ Q_mat = np.asarray(ply_obj('Q'), dtype=float)
658
+
659
+ for index_surface, z_eval in ((0, z_bottom), (1, z_top)):
660
+ exy = Laminate.strain_xy_at_z(epsilon0, z_eval)[0]
661
+ sig_xy = Laminate.stress_xy_global_from_strain(exy, Q_bar)
662
+ s123 = Laminate.stress_material_from_strain(exy, Q_mat, theta)
663
+ e123 = Laminate.strain_xy_global_to_material(exy, theta)
664
+
665
+ uvarm = larc05.get_uvarm(np.asarray(s123, dtype=float))
666
+ fi_block = uvarm[:5]
667
+ fi_max = float(np.max(fi_block))
668
+ mode_idx = int(np.argmax(fi_block)) + 1
669
+ failure_mode = FAILURE_MODE_NAMES[mode_idx]
670
+
671
+ results.append(
672
+ {
673
+ 'index_ply': index_ply,
674
+ 'index_surface': index_surface,
675
+ 'z': z_eval,
676
+ 'angle': theta,
677
+ 'sigma_x': float(sig_xy[0]),
678
+ 'sigma_y': float(sig_xy[1]),
679
+ 'tau_xy': float(sig_xy[2]),
680
+ 'sigma_1': float(s123[0]),
681
+ 'sigma_2': float(s123[1]),
682
+ 'tau_12': float(s123[2]),
683
+ 'epsilon_x': float(exy[0]),
684
+ 'epsilon_y': float(exy[1]),
685
+ 'gamma_xy': float(exy[2]),
686
+ 'epsilon_1': float(e123[0]),
687
+ 'epsilon_2': float(e123[1]),
688
+ 'gamma_12': float(e123[2]),
689
+ 'FI_matrix_cracking': float(fi_block[0]),
690
+ 'FI_matrix_splitting': float(fi_block[1]),
691
+ 'FI_fibre_tension': float(fi_block[2]),
692
+ 'FI_fibre_kinking': float(fi_block[3]),
693
+ 'FI_matrix_interface': float(fi_block[4]),
694
+ 'FI_max': fi_max,
695
+ 'failure_mode': failure_mode,
696
+ }
697
+ )
698
+ return results
699
+
700
+ def evaluate_laminate(self, N: np.ndarray) -> pd.DataFrame:
701
+ '''
702
+ Evaluate the failure field of the laminate,
703
+ the laminate consists of plies with the same material.
704
+
705
+ Parameters
706
+ ----------
707
+ laminate: Laminate
708
+ Laminate object (units: MPa, mm)
709
+ N: np.ndarray (6,)
710
+ Load vector, [Nxx, Nyy, Nxy, Mxx, Myy, Mxy].
711
+ Nxx, Nyy, Nxy: in-plane forces (N/mm)
712
+ Mxx, Myy, Mxy: bending moments (N)
713
+
714
+ Returns
715
+ -------
716
+ field_results: pd.DataFrame
717
+ One row per ply face, ordered from the bottom of the layup upward (increasing z).
718
+
719
+ Columns:
720
+ - index_ply: 0-based ply index, same order as `laminate.layup` (0 = bottom ply)
721
+ - index_surface: (0, 1) = bottom/top face of the ply
722
+ - z: z coordinate of the ply (bottom/top) face (mm)
723
+ - angle: ply angle (degree)
724
+ - sigma_x, sigma_y, tau_xy: global stresses (MPa)
725
+ - sigma_1, sigma_2, tau_12: material stresses (MPa)
726
+ - epsilon_x, epsilon_y, gamma_xy: global strains (unitless)
727
+ - epsilon_1, epsilon_2, gamma_12: material strains (unitless)
728
+ - FI_*: LaRC05 failure indices (unitless)
729
+ - FI_max: maximum LaRC05 failure index (unitless)
730
+ - failure_mode: failure mode (string)
731
+
732
+ Attributes:
733
+ - epsilon0: mid-plane generalized strains (ndarray (6,))
734
+ accessed as `field_results.attrs['epsilon0']`,
735
+ which is `[epsilon_x0, epsilon_y0, gamma_xy0, kappa_x0, kappa_y0, kappa_xy0]`.
736
+ - global_FI_*: maximum failure indices of all plies
737
+ '''
738
+ N = np.asarray(N, dtype=float).reshape(6)
739
+ epsilon0 = self.get_mid_plane_strains(N)
740
+
741
+ larc05 = LaRC05(nSCply=3, material=self.ply_material.name)
742
+
743
+ results = self.get_ply_level_results(epsilon0, larc05)
744
+
745
+ out = pd.DataFrame.from_records(results)
746
+
747
+ out.attrs['epsilon0'] = np.asarray(epsilon0, dtype=float)
748
+ out.attrs['global_FI_matrix_cracking'] = np.max(out['FI_matrix_cracking'])
749
+ out.attrs['global_FI_matrix_splitting'] = np.max(out['FI_matrix_splitting'])
750
+ out.attrs['global_FI_fibre_tension'] = np.max(out['FI_fibre_tension'])
751
+ out.attrs['global_FI_fibre_kinking'] = np.max(out['FI_fibre_kinking'])
752
+ out.attrs['global_FI_matrix_interface'] = np.max(out['FI_matrix_interface'])
753
+ out.attrs['global_FI_max'] = np.max(out['FI_max'])
754
+
755
+ return out
756
+
757
+