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 ADDED
@@ -0,0 +1,19 @@
1
+ __version__ = '0.1.0'
2
+
3
+
4
+ from .analysis.material import Material, Ply
5
+ from .analysis.laminate import Laminate
6
+ from .analysis.larc05 import LaRC05
7
+ from lamkit.lekhnitskii.hole import Hole
8
+ from lamkit.lekhnitskii.unloaded_hole import UnloadedHole
9
+ from lamkit.requirements import EngineeringRequirements
10
+
11
+ __all__ = [
12
+ 'Material',
13
+ 'Ply',
14
+ 'Laminate',
15
+ 'LaRC05',
16
+ 'Hole',
17
+ 'UnloadedHole',
18
+ 'Requirements',
19
+ ]
File without changes
@@ -0,0 +1,406 @@
1
+ """
2
+ This is a modified version of the composipy package.
3
+ It is used to calculate the buckling load of a laminate plate.
4
+
5
+ Reference:
6
+ https://github.com/rafaelpsilva07/composipy
7
+
8
+ Author: Runze Li @ Department of Aeronautics, Imperial College London
9
+ Date: 2026-03-25
10
+ """
11
+
12
+ from itertools import product
13
+ from typing import Dict, Iterable, Tuple
14
+
15
+ import numpy as np
16
+ from scipy.sparse import csr_matrix
17
+ from scipy.sparse.linalg import eigsh
18
+ import matplotlib.pyplot as plt
19
+ import os
20
+
21
+ from lamkit.analysis.laminate import Laminate
22
+ from lamkit.components.functions import sxieta
23
+ from lamkit.components.build_k import (
24
+ calc_K11_ijkl,
25
+ calc_k12_ijkl,
26
+ calc_k13_ijkl,
27
+ calc_k21_ijkl,
28
+ calc_k22_ijkl,
29
+ calc_k23_ijkl,
30
+ calc_k31_ijkl,
31
+ calc_k32_ijkl,
32
+ calc_k33_ijkl,
33
+ calc_kG33_ijkl,
34
+ )
35
+
36
+
37
+ ConstraintDict = Dict[str, Iterable[str]]
38
+
39
+
40
+ class BucklingAnalysis:
41
+ """
42
+ Buckling analysis of a laminate plate based on Ritz approximation.
43
+
44
+ Parameters
45
+ ----------
46
+ laminate : Laminate
47
+ Laminate object that provides `A`, `D`, and `ABD` stiffness matrices.
48
+ a : float
49
+ Plate length along x direction (mm).
50
+ b : float
51
+ Plate length along y direction (mm).
52
+ constraints : str | dict, default "PINNED"
53
+ Boundary-condition definition. Built-in options: "PINNED", "CLAMPED".
54
+ A custom dict can define edge constraints for keys `x0`, `xa`, `y0`, `yb`.
55
+ Nxx, Nyy, Nxy : float, default 0.0
56
+ In-plane pre-buckling loads, force per unit length (N/mm).
57
+ `Fx = Nxx * b`, `Fy = Nyy * a`, `Fxy = Nxy * a * b`.
58
+ m, n : int, default 10
59
+ Number of Ritz terms in x and y directions.
60
+ """
61
+
62
+ def __init__(self, laminate: Laminate, a: float, b: float,
63
+ constraints: str | ConstraintDict = "PINNED",
64
+ Nxx: float = 0.0, Nyy: float = 0.0, Nxy: float = 0.0,
65
+ m: int = 10, n: int = 10) -> None:
66
+
67
+ if not isinstance(laminate, Laminate):
68
+ raise TypeError("laminate must be a lamkit.analysis.laminate.Laminate instance")
69
+ if float(a) <= 0 or float(b) <= 0:
70
+ raise ValueError("a and b must be positive")
71
+ if int(m) < 1 or int(n) < 1:
72
+ raise ValueError("m and n must be >= 1")
73
+
74
+ self._laminate = laminate
75
+ self._a = float(a)
76
+ self._b = float(b)
77
+ self._constraints = constraints
78
+ self._Nxx = float(Nxx)
79
+ self._Nyy = float(Nyy)
80
+ self._Nxy = float(Nxy)
81
+ self._m = int(m)
82
+ self._n = int(n)
83
+
84
+ self.su_idx = None
85
+ self.sv_idx = None
86
+ self.sw_idx = None
87
+ self.eigenvalue = None
88
+ self.eigenvector = None
89
+
90
+ @property
91
+ def laminate(self) -> Laminate:
92
+ """Return the laminate object used by this analysis."""
93
+ return self._laminate
94
+
95
+ @property
96
+ def a(self) -> float:
97
+ """Return plate length in x direction."""
98
+ return self._a
99
+
100
+ @property
101
+ def b(self) -> float:
102
+ """Return plate length in y direction."""
103
+ return self._b
104
+
105
+ @property
106
+ def constraints(self) -> str | ConstraintDict:
107
+ """Return boundary condition definition."""
108
+ return self._constraints
109
+
110
+ @property
111
+ def Nxx(self) -> float:
112
+ """Return in-plane normal load in x direction."""
113
+ return self._Nxx
114
+
115
+ @property
116
+ def Nyy(self) -> float:
117
+ """Return in-plane normal load in y direction."""
118
+ return self._Nyy
119
+
120
+ @property
121
+ def Nxy(self) -> float:
122
+ """Return in-plane shear load."""
123
+ return self._Nxy
124
+
125
+ @property
126
+ def m(self) -> int:
127
+ """Return number of Ritz terms along x."""
128
+ return self._m
129
+
130
+ @property
131
+ def n(self) -> int:
132
+ """Return number of Ritz terms along y."""
133
+ return self._n
134
+
135
+ def _compute_constraints(self) -> Tuple[list, list, list]:
136
+ """
137
+ Build Ritz index sets that satisfy the selected boundary conditions.
138
+
139
+ The method filters basis-function families for in-plane (`u`, `v`) and
140
+ out-of-plane (`w`) fields according to constrained translations/rotations
141
+ on each edge, then builds Cartesian products used for matrix assembly.
142
+
143
+ Returns
144
+ -------
145
+ tuple[list, list, list]
146
+ `(uidx, vidx, widx)` index lists for assembling stiffness terms.
147
+ """
148
+ if self.constraints == "PINNED":
149
+ x0 = xa = y0 = yb = ["TX", "TY", "TZ"]
150
+ elif self.constraints == "CLAMPED":
151
+ x0 = xa = y0 = yb = ["TX", "TY", "TZ", "RX", "RY", "RZ"]
152
+ else:
153
+ if not isinstance(self.constraints, dict):
154
+ raise TypeError("constraints must be 'PINNED', 'CLAMPED' or a constraint dict")
155
+ x0 = list(self.constraints.get("x0", []))
156
+ xa = list(self.constraints.get("xa", []))
157
+ y0 = list(self.constraints.get("y0", []))
158
+ yb = list(self.constraints.get("yb", []))
159
+
160
+ sm = [i for i in range(self.m + 4)]
161
+ sn = [i for i in range(self.n + 4)]
162
+
163
+ um, un = sm.copy(), sn.copy()
164
+ vm, vn = sm.copy(), sn.copy()
165
+ wm, wn = sm.copy(), sn.copy()
166
+
167
+ if "TX" in x0:
168
+ um.remove(0)
169
+ if "TY" in x0:
170
+ vm.remove(0)
171
+ if "TZ" in x0:
172
+ wm.remove(0)
173
+ if "RX" in x0:
174
+ um.remove(1)
175
+ if "RY" in x0:
176
+ vm.remove(1)
177
+ if "RZ" in x0:
178
+ wm.remove(1)
179
+
180
+ if "TX" in xa:
181
+ um.remove(2)
182
+ if "TY" in xa:
183
+ vm.remove(2)
184
+ if "TZ" in xa:
185
+ wm.remove(2)
186
+ if "RX" in xa:
187
+ um.remove(3)
188
+ if "RY" in xa:
189
+ vm.remove(3)
190
+ if "RZ" in xa:
191
+ wm.remove(3)
192
+
193
+ if "TX" in y0:
194
+ un.remove(0)
195
+ if "TY" in y0:
196
+ vn.remove(0)
197
+ if "TZ" in y0:
198
+ wn.remove(0)
199
+ if "RX" in y0:
200
+ un.remove(1)
201
+ if "RY" in y0:
202
+ vn.remove(1)
203
+ if "RZ" in y0:
204
+ wn.remove(1)
205
+
206
+ if "TX" in yb:
207
+ un.remove(2)
208
+ if "TY" in yb:
209
+ vn.remove(2)
210
+ if "TZ" in yb:
211
+ wn.remove(2)
212
+ if "RX" in yb:
213
+ un.remove(3)
214
+ if "RY" in yb:
215
+ vn.remove(3)
216
+ if "RZ" in yb:
217
+ wn.remove(3)
218
+
219
+ um, un = um[0 : self.m], un[0 : self.n]
220
+ vm, vn = vm[0 : self.m], vn[0 : self.n]
221
+ wm, wn = wm[0 : self.m], wn[0 : self.n]
222
+
223
+ uidx = list(product(um, un, um, un))
224
+ vidx = list(product(vm, vn, vm, vn))
225
+ widx = list(product(wm, wn, wm, wn))
226
+
227
+ self.su_idx = list(product(um, un))
228
+ self.sv_idx = list(product(vm, vn))
229
+ self.sw_idx = list(product(wm, wn))
230
+
231
+ return (uidx, vidx, widx)
232
+
233
+ def calc_K_KG_ABD(self) -> Tuple[np.ndarray, np.ndarray]:
234
+ """
235
+ Assemble structural and geometric stiffness matrices using full ABD coupling.
236
+
237
+ Returns
238
+ -------
239
+ tuple[np.ndarray, np.ndarray]
240
+ `(K, KG)` where `K` is the structural stiffness matrix and `KG` is
241
+ the geometric stiffness matrix.
242
+ """
243
+ # Read laminate extensional/coupling/bending stiffness entries.
244
+ A11, A12, A16, B11, B12, B16 = self.laminate.ABD[0, :]
245
+ A12, A22, A26, B12, B22, B26 = self.laminate.ABD[1, :]
246
+ A16, A26, A66, B16, B26, B66 = self.laminate.ABD[2, :]
247
+ B11, B12, B16, D11, D12, D16 = self.laminate.ABD[3, :]
248
+ B12, B22, B26, D12, D22, D26 = self.laminate.ABD[4, :]
249
+ B16, B26, B66, D16, D26, D66 = self.laminate.ABD[5, :]
250
+
251
+ k11, k12, k13, k21, k22, k23, k31, k32, k33 = [], [], [], [], [], [], [], [], []
252
+ k33g = []
253
+
254
+ uidx, vidx, widx = self._compute_constraints()
255
+ size = self.m**2 * self.n**2
256
+
257
+ # Loop over Ritz index combinations and evaluate pre-integrated terms.
258
+ for i in range(size):
259
+ ui, uj, uk, ul = uidx[i]
260
+ vi, vj, vk, vl = vidx[i]
261
+ wi, wj, wk, wl = widx[i]
262
+
263
+ k11.append(calc_K11_ijkl(self.a, self.b, ui, uj, uk, ul, A11, A16, A66))
264
+ k12.append(calc_k12_ijkl(self.a, self.b, ui, uj, vk, vl, A12, A16, A26, A66))
265
+ k13.append(calc_k13_ijkl(self.a, self.b, ui, uj, wk, wl, B11, B12, B16, B26, B66))
266
+ k21.append(calc_k21_ijkl(self.a, self.b, vi, vj, uk, ul, A12, A16, A26, A66))
267
+ k22.append(calc_k22_ijkl(self.a, self.b, vi, vj, vk, vl, A22, A26, A66))
268
+ k23.append(calc_k23_ijkl(self.a, self.b, vi, vj, wk, wl, B12, B16, B22, B26, B66))
269
+ k31.append(calc_k31_ijkl(self.a, self.b, wi, wj, uk, ul, B11, B12, B16, B26, B66))
270
+ k32.append(calc_k32_ijkl(self.a, self.b, wi, wj, vk, vl, B11, B12, B16, B22, B26, B66))
271
+ k33.append(calc_k33_ijkl(self.a, self.b, wi, wj, wk, wl, D11, D12, D22, D16, D26, D66))
272
+ k33g.append(calc_kG33_ijkl(self.a, self.b, wi, wj, wk, wl, self.Nxx, self.Nyy, self.Nxy))
273
+
274
+ dim = self.m * self.n
275
+ k11 = np.array(k11).reshape(dim, dim)
276
+ k12 = np.array(k12).reshape(dim, dim)
277
+ k13 = np.array(k13).reshape(dim, dim)
278
+ k21 = np.array(k21).reshape(dim, dim)
279
+ k22 = np.array(k22).reshape(dim, dim)
280
+ k23 = np.array(k23).reshape(dim, dim)
281
+ k31 = np.array(k31).reshape(dim, dim)
282
+ k32 = np.array(k32).reshape(dim, dim)
283
+ k33 = np.array(k33).reshape(dim, dim)
284
+ k00 = np.zeros((dim, dim))
285
+ k33g = np.array(k33g).reshape(dim, dim)
286
+
287
+ # Build global block matrices:
288
+ # K = [[Kuu, Kuv, Kuw], [Kvu, Kvv, Kvw], [Kwu, Kwv, Kww]]
289
+ K = np.vstack([np.hstack([k11, k12, k13]), np.hstack([k21, k22, k23]), np.hstack([k31, k32, k33])])
290
+ KG = np.vstack([np.hstack([k00, k00, k00]), np.hstack([k00, k00, k00]), np.hstack([k00, k00, k33g])])
291
+
292
+ return K, KG
293
+
294
+ def calc_K_KG_D(self) -> Tuple[np.ndarray, np.ndarray]:
295
+ """
296
+ Assemble stiffness matrices using bending-only (`D`) approximation.
297
+
298
+ This reduced formulation keeps only the transverse displacement field
299
+ in the buckling eigen problem.
300
+
301
+ Returns
302
+ -------
303
+ tuple[np.ndarray, np.ndarray]
304
+ `(K, KG)` bending stiffness and geometric stiffness matrices.
305
+ """
306
+ # Read bending stiffness matrix entries.
307
+ D11, D12, D16 = self.laminate.D[0, :]
308
+ D12, D22, D26 = self.laminate.D[1, :]
309
+ D16, D26, D66 = self.laminate.D[2, :]
310
+
311
+ k33 = []
312
+ k33g = []
313
+ _, _, widx = self._compute_constraints()
314
+ size = self.m**2 * self.n**2
315
+
316
+ # Evaluate bending and geometric terms for each Ritz pair.
317
+ for i in range(size):
318
+ wi, wj, wk, wl = widx[i]
319
+ k33.append(calc_k33_ijkl(self.a, self.b, wi, wj, wk, wl, D11, D12, D22, D16, D26, D66))
320
+ k33g.append(calc_kG33_ijkl(self.a, self.b, wi, wj, wk, wl, self.Nxx, self.Nyy, self.Nxy))
321
+
322
+ dim = self.m * self.n
323
+ K = np.array(k33).reshape(dim, dim)
324
+ KG = np.array(k33g).reshape(dim, dim)
325
+ return K, KG
326
+
327
+ def buckling_analysis(self, num_eigvalues: int = 5) -> Tuple[np.ndarray, np.ndarray]:
328
+ """
329
+ Solve the generalized eigenvalue buckling problem.
330
+
331
+ The current implementation uses the bending-only matrices from
332
+ `calc_K_KG_D` and solves:
333
+ KG * phi = lambda * K * phi
334
+ then converts to load multipliers as `-1/lambda`.
335
+
336
+ Parameters
337
+ ----------
338
+ num_eigvalues : int, default 5
339
+ Requested number of lowest-magnitude eigenvalues.
340
+
341
+ Returns
342
+ -------
343
+ tuple[np.ndarray, np.ndarray]
344
+ Eigenvalues (load multipliers) and eigenvectors.
345
+ """
346
+ K, KG = self.calc_K_KG_D()
347
+ K, KG = csr_matrix(K), csr_matrix(KG)
348
+
349
+ k = min(int(num_eigvalues), KG.shape[0] - 2)
350
+ eigvals, eigvecs = eigsh(A=KG, k=k, which="SM", M=K, tol=0.0, sigma=1.0, mode="cayley")
351
+ eigvals = -1.0 / eigvals
352
+
353
+ self.eigenvalue, self.eigenvector = eigvals, eigvecs
354
+ return eigvals, eigvecs
355
+
356
+
357
+ def plot_buckling_modes(analysis: BucklingAnalysis,
358
+ eigvals: np.ndarray, n_modes: int,
359
+ ngridx: int, ngridy: int, case_text: str, save_path: str) -> None:
360
+ """
361
+ Plot several buckling modes in one figure with multiple subplots.
362
+ """
363
+ n_modes = min(int(n_modes), int(eigvals.shape[0]))
364
+ n_cols = 2
365
+ n_rows = (n_modes + n_cols - 1) // n_cols
366
+
367
+ xi_arr = np.linspace(-1.0, 1.0, ngridx)
368
+ eta_arr = np.linspace(-1.0, 1.0, ngridy)
369
+ xi_mesh, eta_mesh = np.meshgrid(xi_arr, eta_arr)
370
+ x_mesh = (analysis.a / 2.0) * (xi_mesh + 1.0)
371
+ y_mesh = (analysis.b / 2.0) * (eta_mesh + 1.0)
372
+
373
+ fig = plt.figure(figsize=(7 * n_cols, 5.5 * n_rows))
374
+
375
+ for mode_idx in range(n_modes):
376
+ c_values = analysis.eigenvector[:, mode_idx]
377
+ len_w = len(analysis.sw_idx)
378
+ cw_values = c_values[-len_w:]
379
+
380
+ z = np.zeros((ngridx, ngridy))
381
+ for i in range(ngridx):
382
+ for j in range(ngridy):
383
+ sw = sxieta(analysis.sw_idx, xi_mesh[i, j], eta_mesh[i, j])
384
+ z[i, j] = float(sw @ cw_values)
385
+
386
+ is_buckled = float(eigvals[mode_idx]) <= 1.0
387
+ buckle_text = "buckled" if is_buckled else "not-buckled"
388
+
389
+ ax = fig.add_subplot(n_rows, n_cols, mode_idx + 1, projection="3d")
390
+ ax.plot_surface(x_mesh, y_mesh, z, cmap="coolwarm")
391
+ ax.set_title(f"Mode {mode_idx + 1} | {buckle_text}\n(lambda={eigvals[mode_idx]:.3f})")
392
+ ax.set_xticks(np.linspace(0.0, max(analysis.a, analysis.b), 5))
393
+ ax.set_yticks(np.linspace(0.0, max(analysis.a, analysis.b), 5))
394
+ ax.set_xlabel("x (mm)")
395
+ ax.set_ylabel("y (mm)")
396
+ ax.set_zlabel("w (mm)")
397
+
398
+ fig.suptitle(f"Case: {case_text}", fontsize=14)
399
+ fig.tight_layout(rect=(0.0, 0.0, 1.0, 0.96))
400
+
401
+ out_dir = os.path.dirname(save_path)
402
+ if out_dir:
403
+ os.makedirs(out_dir, exist_ok=True)
404
+ fig.savefig(save_path, dpi=180, bbox_inches="tight")
405
+ plt.close(fig)
406
+