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.
- lamkit/__init__.py +19 -0
- lamkit/analysis/__init__.py +0 -0
- lamkit/analysis/buckling.py +406 -0
- lamkit/analysis/laminate.py +757 -0
- lamkit/analysis/larc05.py +977 -0
- lamkit/analysis/material.py +319 -0
- lamkit/components/_S.py +2563 -0
- lamkit/components/__init__.py +0 -0
- lamkit/components/_ii_F.py +5429 -0
- lamkit/components/build_k.py +192 -0
- lamkit/components/functions.py +68 -0
- lamkit/components/write_pre_integrated_terms.py +118 -0
- lamkit/components/write_shape_function.py +95 -0
- lamkit/lekhnitskii/__init__.py +22 -0
- lamkit/lekhnitskii/hole.py +400 -0
- lamkit/lekhnitskii/homogenisation.py +215 -0
- lamkit/lekhnitskii/loaded_hole.py +405 -0
- lamkit/lekhnitskii/unloaded_hole.py +258 -0
- lamkit/lekhnitskii/utils.py +162 -0
- lamkit/requirements.py +438 -0
- lamkit/utils.py +190 -0
- lamkit-0.1.0.dist-info/METADATA +80 -0
- lamkit-0.1.0.dist-info/RECORD +25 -0
- lamkit-0.1.0.dist-info/WHEEL +4 -0
- lamkit-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -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
|
+
|