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.
- lamkit-0.1.0/.gitignore +29 -0
- lamkit-0.1.0/LICENSE +21 -0
- lamkit-0.1.0/MANIFEST.in +3 -0
- lamkit-0.1.0/PKG-INFO +80 -0
- lamkit-0.1.0/README.md +23 -0
- lamkit-0.1.0/docs/Makefile +16 -0
- lamkit-0.1.0/docs/_static/.gitkeep +1 -0
- lamkit-0.1.0/docs/_templates/.gitkeep +1 -0
- lamkit-0.1.0/docs/api.rst +7 -0
- lamkit-0.1.0/docs/conf.py +31 -0
- lamkit-0.1.0/docs/index.rst +12 -0
- lamkit-0.1.0/docs/installation.rst +16 -0
- lamkit-0.1.0/docs/make.bat +20 -0
- lamkit-0.1.0/docs/quickstart.rst +8 -0
- lamkit-0.1.0/docs/requirements.txt +3 -0
- lamkit-0.1.0/example/1-laminate/example_laminate.py +316 -0
- lamkit-0.1.0/example/1-laminate/images/laminate_bending_0-90-90-0.png +0 -0
- lamkit-0.1.0/example/1-laminate/images/laminate_membrane_0-90-90-0.png +0 -0
- lamkit-0.1.0/example/1-laminate/images/laminate_membrane_45-pm45-symmetric.png +0 -0
- lamkit-0.1.0/example/2-lekhnitskii-solution/example_unloaded_hole.py +284 -0
- lamkit-0.1.0/example/2-lekhnitskii-solution/images/open_hole_1.png +0 -0
- lamkit-0.1.0/example/2-lekhnitskii-solution/images/open_hole_2.png +0 -0
- lamkit-0.1.0/example/2-lekhnitskii-solution/images/open_hole_3.png +0 -0
- lamkit-0.1.0/example/2-lekhnitskii-solution/images/open_hole_4.png +0 -0
- lamkit-0.1.0/example/2-lekhnitskii-solution/images/open_hole_5.png +0 -0
- lamkit-0.1.0/example/2-lekhnitskii-solution/images/open_hole_6.png +0 -0
- lamkit-0.1.0/example/3-open-hole/example_open_hole.py +395 -0
- lamkit-0.1.0/example/3-open-hole/images/open_hole_face.png +0 -0
- lamkit-0.1.0/example/3-open-hole/images/open_hole_field.png +0 -0
- lamkit-0.1.0/example/4-effective-stiffness/example_effective_stiffness.py +233 -0
- lamkit-0.1.0/example/4-effective-stiffness/images/open_hole_homogenisation-1.png +0 -0
- lamkit-0.1.0/example/4-effective-stiffness/images/open_hole_homogenisation-2.png +0 -0
- lamkit-0.1.0/example/4-effective-stiffness/images/open_hole_homogenisation-3.png +0 -0
- lamkit-0.1.0/example/4-effective-stiffness/images/open_hole_homogenisation-4.png +0 -0
- lamkit-0.1.0/example/5-laminate-buckling/example_buckling.py +65 -0
- lamkit-0.1.0/example/5-laminate-buckling/images/buckling_modes.png +0 -0
- lamkit-0.1.0/example/6-laminate-optimization-task/example_laminate_opt_function.py +194 -0
- lamkit-0.1.0/example/6-laminate-optimization-task/output.txt +24 -0
- lamkit-0.1.0/pyproject.toml +58 -0
- lamkit-0.1.0/src/lamkit/__init__.py +19 -0
- lamkit-0.1.0/src/lamkit/analysis/__init__.py +0 -0
- lamkit-0.1.0/src/lamkit/analysis/buckling.py +406 -0
- lamkit-0.1.0/src/lamkit/analysis/laminate.py +757 -0
- lamkit-0.1.0/src/lamkit/analysis/larc05.py +977 -0
- lamkit-0.1.0/src/lamkit/analysis/material.py +319 -0
- lamkit-0.1.0/src/lamkit/components/_S.py +2563 -0
- lamkit-0.1.0/src/lamkit/components/__init__.py +0 -0
- lamkit-0.1.0/src/lamkit/components/_ii_F.py +5429 -0
- lamkit-0.1.0/src/lamkit/components/build_k.py +192 -0
- lamkit-0.1.0/src/lamkit/components/functions.py +68 -0
- lamkit-0.1.0/src/lamkit/components/write_pre_integrated_terms.py +118 -0
- lamkit-0.1.0/src/lamkit/components/write_shape_function.py +95 -0
- lamkit-0.1.0/src/lamkit/lekhnitskii/__init__.py +22 -0
- lamkit-0.1.0/src/lamkit/lekhnitskii/hole.py +400 -0
- lamkit-0.1.0/src/lamkit/lekhnitskii/homogenisation.py +215 -0
- lamkit-0.1.0/src/lamkit/lekhnitskii/loaded_hole.py +405 -0
- lamkit-0.1.0/src/lamkit/lekhnitskii/unloaded_hole.py +258 -0
- lamkit-0.1.0/src/lamkit/lekhnitskii/utils.py +162 -0
- lamkit-0.1.0/src/lamkit/requirements.py +438 -0
- lamkit-0.1.0/src/lamkit/utils.py +190 -0
- lamkit-0.1.0/tests/conftest.py +5 -0
- lamkit-0.1.0/tests/test_additional_coverage.py +197 -0
- lamkit-0.1.0/tests/test_buckling.py +64 -0
- lamkit-0.1.0/tests/test_laminate.py +146 -0
- lamkit-0.1.0/tests/test_larc05.py +19 -0
- lamkit-0.1.0/tests/test_lekhnitskii.py +73 -0
- lamkit-0.1.0/tests/test_material.py +39 -0
lamkit-0.1.0/.gitignore
ADDED
|
@@ -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.
|
lamkit-0.1.0/MANIFEST.in
ADDED
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,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,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,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()
|
|
Binary file
|
|
Binary file
|