lamkit 0.1.0__tar.gz

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.
Files changed (67) hide show
  1. lamkit-0.1.0/.gitignore +29 -0
  2. lamkit-0.1.0/LICENSE +21 -0
  3. lamkit-0.1.0/MANIFEST.in +3 -0
  4. lamkit-0.1.0/PKG-INFO +80 -0
  5. lamkit-0.1.0/README.md +23 -0
  6. lamkit-0.1.0/docs/Makefile +16 -0
  7. lamkit-0.1.0/docs/_static/.gitkeep +1 -0
  8. lamkit-0.1.0/docs/_templates/.gitkeep +1 -0
  9. lamkit-0.1.0/docs/api.rst +7 -0
  10. lamkit-0.1.0/docs/conf.py +31 -0
  11. lamkit-0.1.0/docs/index.rst +12 -0
  12. lamkit-0.1.0/docs/installation.rst +16 -0
  13. lamkit-0.1.0/docs/make.bat +20 -0
  14. lamkit-0.1.0/docs/quickstart.rst +8 -0
  15. lamkit-0.1.0/docs/requirements.txt +3 -0
  16. lamkit-0.1.0/example/1-laminate/example_laminate.py +316 -0
  17. lamkit-0.1.0/example/1-laminate/images/laminate_bending_0-90-90-0.png +0 -0
  18. lamkit-0.1.0/example/1-laminate/images/laminate_membrane_0-90-90-0.png +0 -0
  19. lamkit-0.1.0/example/1-laminate/images/laminate_membrane_45-pm45-symmetric.png +0 -0
  20. lamkit-0.1.0/example/2-lekhnitskii-solution/example_unloaded_hole.py +284 -0
  21. lamkit-0.1.0/example/2-lekhnitskii-solution/images/open_hole_1.png +0 -0
  22. lamkit-0.1.0/example/2-lekhnitskii-solution/images/open_hole_2.png +0 -0
  23. lamkit-0.1.0/example/2-lekhnitskii-solution/images/open_hole_3.png +0 -0
  24. lamkit-0.1.0/example/2-lekhnitskii-solution/images/open_hole_4.png +0 -0
  25. lamkit-0.1.0/example/2-lekhnitskii-solution/images/open_hole_5.png +0 -0
  26. lamkit-0.1.0/example/2-lekhnitskii-solution/images/open_hole_6.png +0 -0
  27. lamkit-0.1.0/example/3-open-hole/example_open_hole.py +395 -0
  28. lamkit-0.1.0/example/3-open-hole/images/open_hole_face.png +0 -0
  29. lamkit-0.1.0/example/3-open-hole/images/open_hole_field.png +0 -0
  30. lamkit-0.1.0/example/4-effective-stiffness/example_effective_stiffness.py +233 -0
  31. lamkit-0.1.0/example/4-effective-stiffness/images/open_hole_homogenisation-1.png +0 -0
  32. lamkit-0.1.0/example/4-effective-stiffness/images/open_hole_homogenisation-2.png +0 -0
  33. lamkit-0.1.0/example/4-effective-stiffness/images/open_hole_homogenisation-3.png +0 -0
  34. lamkit-0.1.0/example/4-effective-stiffness/images/open_hole_homogenisation-4.png +0 -0
  35. lamkit-0.1.0/example/5-laminate-buckling/example_buckling.py +65 -0
  36. lamkit-0.1.0/example/5-laminate-buckling/images/buckling_modes.png +0 -0
  37. lamkit-0.1.0/example/6-laminate-optimization-task/example_laminate_opt_function.py +194 -0
  38. lamkit-0.1.0/example/6-laminate-optimization-task/output.txt +24 -0
  39. lamkit-0.1.0/pyproject.toml +58 -0
  40. lamkit-0.1.0/src/lamkit/__init__.py +19 -0
  41. lamkit-0.1.0/src/lamkit/analysis/__init__.py +0 -0
  42. lamkit-0.1.0/src/lamkit/analysis/buckling.py +406 -0
  43. lamkit-0.1.0/src/lamkit/analysis/laminate.py +757 -0
  44. lamkit-0.1.0/src/lamkit/analysis/larc05.py +977 -0
  45. lamkit-0.1.0/src/lamkit/analysis/material.py +319 -0
  46. lamkit-0.1.0/src/lamkit/components/_S.py +2563 -0
  47. lamkit-0.1.0/src/lamkit/components/__init__.py +0 -0
  48. lamkit-0.1.0/src/lamkit/components/_ii_F.py +5429 -0
  49. lamkit-0.1.0/src/lamkit/components/build_k.py +192 -0
  50. lamkit-0.1.0/src/lamkit/components/functions.py +68 -0
  51. lamkit-0.1.0/src/lamkit/components/write_pre_integrated_terms.py +118 -0
  52. lamkit-0.1.0/src/lamkit/components/write_shape_function.py +95 -0
  53. lamkit-0.1.0/src/lamkit/lekhnitskii/__init__.py +22 -0
  54. lamkit-0.1.0/src/lamkit/lekhnitskii/hole.py +400 -0
  55. lamkit-0.1.0/src/lamkit/lekhnitskii/homogenisation.py +215 -0
  56. lamkit-0.1.0/src/lamkit/lekhnitskii/loaded_hole.py +405 -0
  57. lamkit-0.1.0/src/lamkit/lekhnitskii/unloaded_hole.py +258 -0
  58. lamkit-0.1.0/src/lamkit/lekhnitskii/utils.py +162 -0
  59. lamkit-0.1.0/src/lamkit/requirements.py +438 -0
  60. lamkit-0.1.0/src/lamkit/utils.py +190 -0
  61. lamkit-0.1.0/tests/conftest.py +5 -0
  62. lamkit-0.1.0/tests/test_additional_coverage.py +197 -0
  63. lamkit-0.1.0/tests/test_buckling.py +64 -0
  64. lamkit-0.1.0/tests/test_laminate.py +146 -0
  65. lamkit-0.1.0/tests/test_larc05.py +19 -0
  66. lamkit-0.1.0/tests/test_lekhnitskii.py +73 -0
  67. lamkit-0.1.0/tests/test_material.py +39 -0
@@ -0,0 +1,29 @@
1
+ # Python cache and bytecode
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.pyo
5
+
6
+ # Packaging artifacts
7
+ build/
8
+ dist/
9
+ *.egg-info/
10
+ .eggs/
11
+
12
+ # Virtual environments
13
+ .venv/
14
+ venv/
15
+ env/
16
+
17
+ # Test and tooling cache
18
+ .pytest_cache/
19
+ .mypy_cache/
20
+ .ruff_cache/
21
+ .coverage
22
+ htmlcov/
23
+
24
+ # Sphinx docs build output
25
+ docs/_build/
26
+
27
+ # IDE
28
+ .idea/
29
+ .vscode/
lamkit-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Runze LI
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,3 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include docs *.rst *.md *.py *.txt
lamkit-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.4
2
+ Name: lamkit
3
+ Version: 0.1.0
4
+ Summary: Toolkit for stress analysis and failure prediction of composite laminates with holes and joints.
5
+ Project-URL: Homepage, https://github.com/your-org/lamkit
6
+ Project-URL: Documentation, https://your-org.github.io/lamkit/
7
+ Project-URL: Repository, https://github.com/your-org/lamkit
8
+ Project-URL: Issues, https://github.com/your-org/lamkit/issues
9
+ Author: lamkit contributors
10
+ License: MIT License
11
+
12
+ Copyright (c) 2026 Runze LI
13
+
14
+ Permission is hereby granted, free of charge, to any person obtaining a copy
15
+ of this software and associated documentation files (the "Software"), to deal
16
+ in the Software without restriction, including without limitation the rights
17
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
18
+ copies of the Software, and to permit persons to whom the Software is
19
+ furnished to do so, subject to the following conditions:
20
+
21
+ The above copyright notice and this permission notice shall be included in all
22
+ copies or substantial portions of the Software.
23
+
24
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
25
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
26
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
27
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
28
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
29
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
30
+ SOFTWARE.
31
+ License-File: LICENSE
32
+ Keywords: composite,engineering,laminate,stress-analysis
33
+ Classifier: Development Status :: 3 - Alpha
34
+ Classifier: Intended Audience :: Developers
35
+ Classifier: Intended Audience :: Science/Research
36
+ Classifier: License :: OSI Approved :: MIT License
37
+ Classifier: Programming Language :: Python :: 3
38
+ Classifier: Programming Language :: Python :: 3 :: Only
39
+ Classifier: Programming Language :: Python :: 3.9
40
+ Classifier: Programming Language :: Python :: 3.10
41
+ Classifier: Programming Language :: Python :: 3.11
42
+ Classifier: Programming Language :: Python :: 3.12
43
+ Classifier: Topic :: Scientific/Engineering
44
+ Requires-Python: >=3.9
45
+ Requires-Dist: matplotlib>=3.7
46
+ Requires-Dist: numpy>=1.24
47
+ Requires-Dist: pandas>=2.0
48
+ Provides-Extra: dev
49
+ Requires-Dist: build>=1.2; extra == 'dev'
50
+ Requires-Dist: pytest>=8.0; extra == 'dev'
51
+ Requires-Dist: twine>=5.1; extra == 'dev'
52
+ Provides-Extra: docs
53
+ Requires-Dist: furo>=2024.8.6; extra == 'docs'
54
+ Requires-Dist: myst-parser>=4.0; extra == 'docs'
55
+ Requires-Dist: sphinx>=8.0; extra == 'docs'
56
+ Description-Content-Type: text/markdown
57
+
58
+ # lamkit
59
+
60
+ A toolkit for stress analysis and failure prediction of composite laminates with holes and joints.
61
+
62
+ ## Development setup
63
+
64
+ ```bash
65
+ pip install -e .[dev,docs]
66
+ pytest
67
+ ```
68
+
69
+ ## Build package
70
+
71
+ ```bash
72
+ python -m build
73
+ ```
74
+
75
+ ## Build docs
76
+
77
+ ```bash
78
+ cd docs
79
+ sphinx-build -b html . _build/html
80
+ ```
lamkit-0.1.0/README.md ADDED
@@ -0,0 +1,23 @@
1
+ # lamkit
2
+
3
+ A toolkit for stress analysis and failure prediction of composite laminates with holes and joints.
4
+
5
+ ## Development setup
6
+
7
+ ```bash
8
+ pip install -e .[dev,docs]
9
+ pytest
10
+ ```
11
+
12
+ ## Build package
13
+
14
+ ```bash
15
+ python -m build
16
+ ```
17
+
18
+ ## Build docs
19
+
20
+ ```bash
21
+ cd docs
22
+ sphinx-build -b html . _build/html
23
+ ```
@@ -0,0 +1,16 @@
1
+ # Minimal makefile for Sphinx docs.
2
+
3
+ SPHINXBUILD ?= sphinx-build
4
+ SOURCEDIR = .
5
+ BUILDDIR = _build
6
+
7
+ .PHONY: help clean html
8
+
9
+ help:
10
+ $(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)"
11
+
12
+ clean:
13
+ rm -rf "$(BUILDDIR)"
14
+
15
+ html:
16
+ $(SPHINXBUILD) -M html "$(SOURCEDIR)" "$(BUILDDIR)"
@@ -0,0 +1 @@
1
+
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,7 @@
1
+ API reference
2
+ =============
3
+
4
+ .. automodule:: lamkit.core
5
+ :members:
6
+ :undoc-members:
7
+ :show-inheritance:
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ from datetime import datetime
6
+
7
+ PROJECT_ROOT = os.path.abspath("..")
8
+ SRC_ROOT = os.path.join(PROJECT_ROOT, "src")
9
+ sys.path.insert(0, SRC_ROOT)
10
+
11
+ project = "lamkit"
12
+ author = "lamkit contributors"
13
+ copyright = f"{datetime.now().year}, {author}"
14
+ release = "0.1.0"
15
+
16
+ extensions = [
17
+ "sphinx.ext.autodoc",
18
+ "sphinx.ext.napoleon",
19
+ "sphinx.ext.viewcode",
20
+ "myst_parser",
21
+ ]
22
+
23
+ templates_path = ["_templates"]
24
+ exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
25
+
26
+ language = "en"
27
+ html_theme = "furo"
28
+ html_static_path = ["_static"]
29
+
30
+ autodoc_member_order = "bysource"
31
+ autodoc_typehints = "description"
@@ -0,0 +1,12 @@
1
+ lamkit documentation
2
+ ====================
3
+
4
+ Toolkit for stress analysis and failure prediction of composite laminates.
5
+
6
+ .. toctree::
7
+ :maxdepth: 2
8
+ :caption: Contents
9
+
10
+ installation
11
+ quickstart
12
+ api
@@ -0,0 +1,16 @@
1
+ Installation
2
+ ============
3
+
4
+ From source
5
+ -----------
6
+
7
+ .. code-block:: bash
8
+
9
+ pip install -e .[dev,docs]
10
+
11
+ From PyPI (after release)
12
+ -------------------------
13
+
14
+ .. code-block:: bash
15
+
16
+ pip install lamkit
@@ -0,0 +1,20 @@
1
+ @ECHO OFF
2
+
3
+ pushd %~dp0
4
+
5
+ if "%SPHINXBUILD%" == "" (
6
+ set SPHINXBUILD=sphinx-build
7
+ )
8
+ set SOURCEDIR=.
9
+ set BUILDDIR=_build
10
+
11
+ if "%1" == "" goto help
12
+
13
+ %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
14
+ goto end
15
+
16
+ :help
17
+ %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
18
+
19
+ :end
20
+ popd
@@ -0,0 +1,8 @@
1
+ Quickstart
2
+ ==========
3
+
4
+ .. code-block:: python
5
+
6
+ from lamkit import hello
7
+
8
+ print(hello())
@@ -0,0 +1,3 @@
1
+ sphinx>=8.0
2
+ furo>=2024.8.6
3
+ myst-parser>=4.0
@@ -0,0 +1,316 @@
1
+ '''
2
+ Example of using the Laminate class (Classical Lamination Theory).
3
+
4
+ - Plot thickness distribution of stress and strain components.
5
+ - LaRC05 failure indices in the same figure as stress/strain (bottom two rows).
6
+ - Test different stacking sequences.
7
+ - Test different loading conditions.
8
+ '''
9
+ from __future__ import annotations
10
+
11
+ from collections.abc import Callable
12
+
13
+ import os
14
+ import sys
15
+
16
+ path = os.path.dirname(os.path.abspath(__file__))
17
+ src_root = os.path.abspath(os.path.join(path, "..", "..", "src"))
18
+ if src_root not in sys.path:
19
+ sys.path.insert(0, src_root)
20
+
21
+ import matplotlib.pyplot as plt
22
+ import numpy as np
23
+ import pandas as pd
24
+
25
+ from lamkit.analysis.laminate import Laminate
26
+ from lamkit.analysis.material import IM7_8551_7, Ply
27
+
28
+ DPI = 100
29
+ PLY_T_MM = 0.125
30
+ # When max(x) - min(x) is below this, set a symmetric x-window around the data mean.
31
+ X_SPAN_SMALL = 1e-3
32
+ # |mean| below this is treated as "essentially zero" for the x-axis window.
33
+ X_MEAN_NEAR_ZERO = 1e-9
34
+ X_LIM_NEAR_ZERO_MEAN = (-0.01, 0.01)
35
+ # Non-zero mean: half-width is at least this, and scales with |mean|.
36
+ X_HALF_MIN = 0.01
37
+ X_HALF_REL = 1e-4 # half >= max(X_HALF_MIN, X_HALF_REL * |mean|)
38
+
39
+
40
+ def _maybe_widen_small_x_range(ax: plt.Axes, x_values: list[float] | np.ndarray) -> None:
41
+ """
42
+ If data span on x is < 1e-3, fix xlim so the (almost) vertical profile is centred.
43
+
44
+ - If |mean| is essentially zero: xlim = [-0.01, 0.01].
45
+ - Else: symmetric window [mean - half, mean + half] with
46
+ half = max(X_HALF_MIN, X_HALF_REL * |mean|).
47
+ """
48
+ xv = np.asarray(x_values, dtype=float).ravel()
49
+ if xv.size == 0:
50
+ return
51
+ span = float(np.nanmax(xv) - np.nanmin(xv))
52
+ if span >= X_SPAN_SMALL:
53
+ return
54
+ mean = float(np.nanmean(xv))
55
+ if not np.isfinite(mean):
56
+ return
57
+ if abs(mean) < X_MEAN_NEAR_ZERO:
58
+ ax.set_xlim(X_LIM_NEAR_ZERO_MEAN)
59
+ else:
60
+ half = max(X_HALF_MIN, X_HALF_REL * abs(mean))
61
+ ax.set_xlim(mean - half, mean + half)
62
+
63
+
64
+ def build_connected_profile(
65
+ z_bot: np.ndarray,
66
+ z_top: np.ndarray,
67
+ n_ply: int,
68
+ value_at: Callable[[int, float], float],
69
+ ) -> tuple[np.ndarray, np.ndarray]:
70
+ """
71
+ Piecewise-linear (value, z) path through all plies, monotonic in z.
72
+ Always visits each ply bottom and top so the polyline matches ply_endpoint_markers.
73
+
74
+ At an interface where the bottom value of ply i+1 differs from the top value of ply i,
75
+ the path includes both points at the same z (horizontal segment in this axes layout).
76
+ """
77
+ xs: list[float] = []
78
+ zs: list[float] = []
79
+ for i in range(n_ply):
80
+ z_lo = float(z_bot[i])
81
+ z_hi = float(z_top[i])
82
+ v_lo = float(value_at(i, z_lo))
83
+ v_hi = float(value_at(i, z_hi))
84
+ if i == 0:
85
+ xs.extend([v_lo, v_hi])
86
+ zs.extend([z_lo, z_hi])
87
+ else:
88
+ xs.append(v_lo)
89
+ zs.append(z_lo)
90
+ xs.append(v_hi)
91
+ zs.append(z_hi)
92
+ return np.asarray(xs), np.asarray(zs)
93
+
94
+
95
+ def ply_endpoint_markers(
96
+ z_bot: np.ndarray,
97
+ z_top: np.ndarray,
98
+ n_ply: int,
99
+ value_at: Callable[[int, float], float],
100
+ ) -> tuple[np.ndarray, np.ndarray]:
101
+ """Solid markers at each ply bottom and top (interfaces may appear twice if values jump)."""
102
+ xs: list[float] = []
103
+ zs: list[float] = []
104
+ for i in range(n_ply):
105
+ z_lo = float(z_bot[i])
106
+ z_hi = float(z_top[i])
107
+ xs.append(float(value_at(i, z_lo)))
108
+ zs.append(z_lo)
109
+ xs.append(float(value_at(i, z_hi)))
110
+ zs.append(z_hi)
111
+ return np.asarray(xs), np.asarray(zs)
112
+
113
+
114
+ def _value_at_face(
115
+ field: pd.DataFrame,
116
+ col: str,
117
+ z_bot: np.ndarray,
118
+ z_top: np.ndarray,
119
+ ) -> Callable[[int, float], float]:
120
+ """Look up ``col`` at ply ``i`` bottom (``z == z_bot[i]``) or top (``z == z_top[i]``)."""
121
+ idx = field.set_index(['index_ply', 'index_surface'])
122
+
123
+ def value_at(i: int, z: float) -> float:
124
+ z = float(z)
125
+ zb, zt = float(z_bot[i]), float(z_top[i])
126
+ if np.isclose(z, zb, rtol=0.0, atol=1e-9):
127
+ return float(idx.loc[(i, 0), col])
128
+ if np.isclose(z, zt, rtol=0.0, atol=1e-9):
129
+ return float(idx.loc[(i, 1), col])
130
+ raise ValueError(f'z={z} is not a face of ply {i} (expect {zb} or {zt})')
131
+
132
+ return value_at
133
+
134
+
135
+ def plot_laminate_response(
136
+ lam: Laminate,
137
+ N: np.ndarray,
138
+ title: str,
139
+ out_path: str,
140
+ field: pd.DataFrame | None = None,
141
+ ) -> None:
142
+ """
143
+ One figure per laminate load case: strains, stresses, and LaRC05 failure indices vs z
144
+ (6 rows × 3 cols; rows 0–3 stress/strain, rows 4–5 LaRC05).
145
+
146
+ If ``field`` is None, it is filled with ``evaluate_laminate(lam, N)``.
147
+ """
148
+ z_if = np.asarray(lam.z_position, dtype=float)
149
+ z_bot = z_if[:-1]
150
+ z_top = z_if[1:]
151
+ if field is None:
152
+ field = lam.evaluate_laminate(N)
153
+ eps6 = np.asarray(field.attrs['epsilon0'], dtype=float)
154
+
155
+ row_labels = (
156
+ (r"$\varepsilon_x$", r"$\varepsilon_y$", r"$\gamma_{xy}$"),
157
+ (r"$\varepsilon_1$", r"$\varepsilon_2$", r"$\gamma_{12}$"),
158
+ (r"$\sigma_x$", r"$\sigma_y$", r"$\tau_{xy}$"),
159
+ (r"$\sigma_1$", r"$\sigma_2$", r"$\tau_{12}$"),
160
+ )
161
+ row_title = (
162
+ "strain (plate x-y)",
163
+ "strain (material 1-2)",
164
+ "stress (plate x-y), MPa",
165
+ "stress (material 1-2), MPa",
166
+ "LaRC05 FI (cracking, splitting, tension)",
167
+ "LaRC05 FI (kinking, interface, FI_max)",
168
+ )
169
+ fi_labels = (
170
+ "FI matrix cracking",
171
+ "FI matrix splitting",
172
+ "FI fibre tension",
173
+ "FI fibre kinking",
174
+ "FI matrix interface",
175
+ r"FI$_{\mathrm{max}}$ (UVARM6)",
176
+ )
177
+
178
+ fig, axes = plt.subplots(6, 3, figsize=(10, 16), sharey=True)
179
+ for ax in axes.flat:
180
+ ax.axhline(0.0, color="k", linewidth=0.5, linestyle=":")
181
+ ax.grid(True, alpha=0.3)
182
+
183
+ n_ply = lam.n_ply
184
+
185
+ for j in range(3):
186
+ ax = axes[0, j]
187
+ xs0: list[float] = []
188
+ zs0: list[float] = []
189
+ for k in range(len(z_if)):
190
+ exy_k = Laminate.strain_xy_at_z(eps6, z_if[k])[0]
191
+ xs0.append(float(exy_k[j]))
192
+ zs0.append(float(z_if[k]))
193
+ ax.plot(xs0, zs0, color="C0", linewidth=1.8)
194
+ ax.scatter(xs0, zs0, s=24, c="C0", zorder=5, edgecolors="none")
195
+ _maybe_widen_small_x_range(ax, xs0)
196
+ ax.set_xlabel(row_labels[0][j])
197
+
198
+ stress_strain_cols = {
199
+ (1, 0): 'epsilon_1',
200
+ (1, 1): 'epsilon_2',
201
+ (1, 2): 'gamma_12',
202
+ (2, 0): 'sigma_x',
203
+ (2, 1): 'sigma_y',
204
+ (2, 2): 'tau_xy',
205
+ (3, 0): 'sigma_1',
206
+ (3, 1): 'sigma_2',
207
+ (3, 2): 'tau_12',
208
+ }
209
+
210
+ for row in (1, 2, 3):
211
+ for j in range(3):
212
+ col = stress_strain_cols[(row, j)]
213
+ value_at = _value_at_face(field, col, z_bot, z_top)
214
+
215
+ xs, zs = build_connected_profile(z_bot, z_top, n_ply, value_at)
216
+ mx, mz = ply_endpoint_markers(z_bot, z_top, n_ply, value_at)
217
+ ax = axes[row, j]
218
+ ax.plot(xs, zs, color="C0", linewidth=1.8)
219
+ ax.scatter(mx, mz, s=24, c="C0", zorder=5, edgecolors="none")
220
+ _maybe_widen_small_x_range(ax, xs)
221
+ ax.set_xlabel(row_labels[row][j])
222
+
223
+ fi_cols = (
224
+ 'FI_matrix_cracking',
225
+ 'FI_matrix_splitting',
226
+ 'FI_fibre_tension',
227
+ 'FI_fibre_kinking',
228
+ 'FI_matrix_interface',
229
+ 'FI_max',
230
+ )
231
+
232
+ for fi_row in (0, 1):
233
+ for j in range(3):
234
+ fi_idx = fi_row * 3 + j
235
+ ax = axes[4 + fi_row, j]
236
+ value_at = _value_at_face(field, fi_cols[fi_idx], z_bot, z_top)
237
+
238
+ xs, zs = build_connected_profile(z_bot, z_top, n_ply, value_at)
239
+ mx, mz = ply_endpoint_markers(z_bot, z_top, n_ply, value_at)
240
+ ax.plot(xs, zs, color="C3", linewidth=1.8)
241
+ ax.scatter(mx, mz, s=24, c="C3", zorder=5, edgecolors="none")
242
+ _maybe_widen_small_x_range(ax, xs)
243
+ ax.set_xlabel(fi_labels[fi_idx], fontsize=9)
244
+
245
+ for r in range(6):
246
+ axes[r, 0].set_ylabel("z (mm)")
247
+ axes[r, 1].set_title(row_title[r], fontsize=9, pad=6)
248
+
249
+ fig.suptitle(title, fontsize=12)
250
+ fig.tight_layout()
251
+ os.makedirs(os.path.dirname(out_path), exist_ok=True)
252
+ fig.savefig(out_path, dpi=DPI, bbox_inches="tight")
253
+ plt.close(fig)
254
+
255
+
256
+ def main() -> None:
257
+ ply = Ply(IM7_8551_7, thickness=PLY_T_MM)
258
+
259
+ stacks = {
260
+ "[0/90/90/0]": ([0.0, 90.0, 90.0, 0.0], "0-90-90-0"),
261
+ "[45/-45/-45/45] (symmetric)": ([45.0, -45.0, -45.0, 45.0], "45-pm45-symmetric"),
262
+ }
263
+
264
+ # Uniaxial membrane resultant Nxx (N/mm); plies defined in mm, stiffness in MPa => consistent.
265
+ N_pull = np.array([80.0, 0.0, 0.0, 0.0, 0.0, 0.0])
266
+ # Pure bending about x (N)
267
+ Mxx = 50.0
268
+ N_bend = np.array([0.0, 0.0, 0.0, Mxx, 0.0, 0.0])
269
+
270
+ out_dir = os.path.join(path, "images")
271
+
272
+ for name, (stacking, slug) in stacks.items():
273
+ lam = Laminate(stacking, [ply] * len(stacking))
274
+ h = sum(p.thickness for p in lam.plies)
275
+ print(f"\n=== {name} ===")
276
+ print(f"Total thickness: {h:.3f} mm")
277
+ print(f"A11 = {lam.A[0, 0]:.1f} N/mm, D11 = {lam.D[0, 0]:.2f} N.mm")
278
+ print("Mid-plane strains under N_pull [ex0, ey0, gxy0, kx, ky, kxy]:")
279
+ print(np.round(lam.get_mid_plane_strains(N_pull), 6))
280
+ field = lam.evaluate_laminate(N_pull)
281
+ print("Ply face field (bottom to top; index_surface 0=bot, 1=top):")
282
+ show_cols = [
283
+ 'index_ply',
284
+ 'index_surface',
285
+ 'z',
286
+ 'angle',
287
+ 'sigma_1',
288
+ 'sigma_2',
289
+ 'tau_12',
290
+ 'FI_max',
291
+ ]
292
+ with pd.option_context("display.max_rows", 12):
293
+ print(field[show_cols].to_string(index=False))
294
+
295
+ plot_laminate_response(
296
+ lam,
297
+ N_pull,
298
+ title=f"{name}, membrane Nxx={N_pull[0]:.0f} N/mm",
299
+ out_path=os.path.join(out_dir, f"laminate_membrane_{slug}.png"),
300
+ field=field,
301
+ )
302
+
303
+ # Same stack, bending-only loading
304
+ lam_qs = Laminate(stacks["[0/90/90/0]"][0], [ply] * 4)
305
+ plot_laminate_response(
306
+ lam_qs,
307
+ N_bend,
308
+ title=f"[0/90/90/0], bending Mxx={Mxx} N",
309
+ out_path=os.path.join(out_dir, "laminate_bending_0-90-90-0.png"),
310
+ )
311
+ print("\n=== [0/90/90/0] under pure Mxx ===")
312
+ print(np.round(lam_qs.get_mid_plane_strains(N_bend), 6))
313
+
314
+
315
+ if __name__ == "__main__":
316
+ main()