mechprops-extractor 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.
- mechprops_extractor-0.1.0/LICENSE +21 -0
- mechprops_extractor-0.1.0/PKG-INFO +93 -0
- mechprops_extractor-0.1.0/README.md +49 -0
- mechprops_extractor-0.1.0/pyproject.toml +34 -0
- mechprops_extractor-0.1.0/setup.cfg +4 -0
- mechprops_extractor-0.1.0/src/mechprops/__init__.py +3 -0
- mechprops_extractor-0.1.0/src/mechprops/curve.py +193 -0
- mechprops_extractor-0.1.0/src/mechprops/plotter.py +379 -0
- mechprops_extractor-0.1.0/src/mechprops_extractor.egg-info/PKG-INFO +93 -0
- mechprops_extractor-0.1.0/src/mechprops_extractor.egg-info/SOURCES.txt +11 -0
- mechprops_extractor-0.1.0/src/mechprops_extractor.egg-info/dependency_links.txt +1 -0
- mechprops_extractor-0.1.0/src/mechprops_extractor.egg-info/requires.txt +3 -0
- mechprops_extractor-0.1.0/src/mechprops_extractor.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Wyatt Ho
|
|
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,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mechprops-extractor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Extract Young's modulus, ultimate point, and break-point from stress-strain curves
|
|
5
|
+
Author-email: wyatthoho <wyatthoho@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Wyatt Ho
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/wyatthoho/mechprops-extractor
|
|
29
|
+
Project-URL: Repository, https://github.com/wyatthoho/mechprops-extractor
|
|
30
|
+
Keywords: stress-strain,mechanical-properties,materials-testing,youngs-modulus
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Science/Research
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
+
Classifier: Topic :: Scientific/Engineering
|
|
37
|
+
Requires-Python: >=3.12
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Requires-Dist: pandas>=2.3
|
|
41
|
+
Requires-Dist: matplotlib>=3.10
|
|
42
|
+
Requires-Dist: scipy>=1.16
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# Mechanical Properties Extractor
|
|
46
|
+
|
|
47
|
+
Extracts Young's modulus, ultimate point, and break-point properties from
|
|
48
|
+
stress-strain curve data, via ISO 527 linear regression, a custom
|
|
49
|
+
RMSProp-based curve fit, or break-point detection.
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- Young's modulus via ISO 527 secant-line linear regression
|
|
54
|
+
- Young's modulus via a custom RMSProp-based enclosed-area fit
|
|
55
|
+
- Ultimate (peak stress) point detection on the stress-strain curve
|
|
56
|
+
- Break-point (failure) detection on the stress-strain curve
|
|
57
|
+
- Built-in plots and an animation of the RMSProp fitting process
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install mechprops-extractor
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For development (editable install):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install -e .
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
Load a stress-strain CSV (`strain`, `stress` columns) into a `StressStrainCurve`,
|
|
74
|
+
then call any combination of its analysis methods. The example below uses one
|
|
75
|
+
of the sample datasets bundled in [`data/`](data):
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import pandas as pd
|
|
79
|
+
|
|
80
|
+
from mechprops import StressStrainCurve
|
|
81
|
+
|
|
82
|
+
df = pd.read_csv("data/polyimide-stress-strain.csv")
|
|
83
|
+
curve = StressStrainCurve(df["strain"], df["stress"])
|
|
84
|
+
|
|
85
|
+
curve.fit_modulus_iso527() # Young's modulus via ISO 527 linear regression
|
|
86
|
+
curve.fit_modulus_rmsprop() # Young's modulus via RMSProp area minimization
|
|
87
|
+
curve.find_ultimate_point() # (strain, stress) at the ultimate (peak stress) point
|
|
88
|
+
curve.find_break_point() # (strain, stress) at the detected break point
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Each method returns its computed value and, by default, opens a plot window
|
|
92
|
+
illustrating the result — close it to continue. Pass `show=False` to skip the
|
|
93
|
+
plot and get the value only.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Mechanical Properties Extractor
|
|
2
|
+
|
|
3
|
+
Extracts Young's modulus, ultimate point, and break-point properties from
|
|
4
|
+
stress-strain curve data, via ISO 527 linear regression, a custom
|
|
5
|
+
RMSProp-based curve fit, or break-point detection.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Young's modulus via ISO 527 secant-line linear regression
|
|
10
|
+
- Young's modulus via a custom RMSProp-based enclosed-area fit
|
|
11
|
+
- Ultimate (peak stress) point detection on the stress-strain curve
|
|
12
|
+
- Break-point (failure) detection on the stress-strain curve
|
|
13
|
+
- Built-in plots and an animation of the RMSProp fitting process
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
pip install mechprops-extractor
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
For development (editable install):
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
pip install -e .
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
Load a stress-strain CSV (`strain`, `stress` columns) into a `StressStrainCurve`,
|
|
30
|
+
then call any combination of its analysis methods. The example below uses one
|
|
31
|
+
of the sample datasets bundled in [`data/`](data):
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
import pandas as pd
|
|
35
|
+
|
|
36
|
+
from mechprops import StressStrainCurve
|
|
37
|
+
|
|
38
|
+
df = pd.read_csv("data/polyimide-stress-strain.csv")
|
|
39
|
+
curve = StressStrainCurve(df["strain"], df["stress"])
|
|
40
|
+
|
|
41
|
+
curve.fit_modulus_iso527() # Young's modulus via ISO 527 linear regression
|
|
42
|
+
curve.fit_modulus_rmsprop() # Young's modulus via RMSProp area minimization
|
|
43
|
+
curve.find_ultimate_point() # (strain, stress) at the ultimate (peak stress) point
|
|
44
|
+
curve.find_break_point() # (strain, stress) at the detected break point
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Each method returns its computed value and, by default, opens a plot window
|
|
48
|
+
illustrating the result — close it to continue. Pass `show=False` to skip the
|
|
49
|
+
plot and get the value only.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mechprops-extractor"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Extract Young's modulus, ultimate point, and break-point from stress-strain curves"
|
|
9
|
+
authors = [{ name = "wyatthoho", email = "wyatthoho@gmail.com" }]
|
|
10
|
+
dependencies = ["pandas >= 2.3", "matplotlib >= 3.10", "scipy >= 1.16"]
|
|
11
|
+
requires-python = ">=3.12"
|
|
12
|
+
readme = "README.md"
|
|
13
|
+
license = { file = "LICENSE" }
|
|
14
|
+
keywords = [
|
|
15
|
+
"stress-strain",
|
|
16
|
+
"mechanical-properties",
|
|
17
|
+
"materials-testing",
|
|
18
|
+
"youngs-modulus",
|
|
19
|
+
]
|
|
20
|
+
classifiers = [
|
|
21
|
+
"Development Status :: 3 - Alpha",
|
|
22
|
+
"Intended Audience :: Science/Research",
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Topic :: Scientific/Engineering",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/wyatthoho/mechprops-extractor"
|
|
31
|
+
Repository = "https://github.com/wyatthoho/mechprops-extractor"
|
|
32
|
+
|
|
33
|
+
[tool.setuptools]
|
|
34
|
+
package-dir = { "" = "src" }
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
from scipy import integrate, stats
|
|
4
|
+
|
|
5
|
+
from mechprops.plotter import (
|
|
6
|
+
BreakDetectGraph,
|
|
7
|
+
Iso527Graph,
|
|
8
|
+
RmsPropAnimation,
|
|
9
|
+
UltimatePointGraph,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
# Analysis Thresholds
|
|
13
|
+
TINY_MODULUS = 1.0e-5
|
|
14
|
+
CONVERGENCE_EPS = 1e-9
|
|
15
|
+
BREAK_THRESHOLD = 0.1
|
|
16
|
+
|
|
17
|
+
# RMSProp Hyperparameters
|
|
18
|
+
RMS_PROP_BASE_LR = 0.01
|
|
19
|
+
RMS_PROP_ALPHA = 0.9
|
|
20
|
+
RMS_PROP_EPS = 1e-8
|
|
21
|
+
|
|
22
|
+
# ISO-527
|
|
23
|
+
STRAIN_LOWER = 0.0005
|
|
24
|
+
STRAIN_UPPER = 0.0025
|
|
25
|
+
|
|
26
|
+
# Modulus Fitting Defaults
|
|
27
|
+
DEFAULT_TOL = 2.0e-5
|
|
28
|
+
DEFAULT_MAX_ITER = 20_000
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class StressStrainCurve:
|
|
32
|
+
"""Fits Young's modulus from a stress-strain curve, via ISO 527 linear
|
|
33
|
+
regression or a custom RMSProp-based enclosed-area minimization."""
|
|
34
|
+
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
strain: pd.Series,
|
|
38
|
+
stress: pd.Series,
|
|
39
|
+
) -> None:
|
|
40
|
+
self.strain_raw = strain
|
|
41
|
+
self.stress_raw = stress
|
|
42
|
+
|
|
43
|
+
idx = int(self.stress_raw.idxmax())
|
|
44
|
+
self.ultimate_x = float(self.strain_raw[idx])
|
|
45
|
+
self.ultimate_y = float(self.stress_raw[idx])
|
|
46
|
+
|
|
47
|
+
self.strain_norm = self.strain_raw / self.ultimate_x
|
|
48
|
+
self.stress_norm = self.stress_raw / self.ultimate_y
|
|
49
|
+
|
|
50
|
+
def find_ultimate_point(self, show: bool = True) -> tuple[float, float]:
|
|
51
|
+
"""Returns the (strain, stress) at maximum stress, the ultimate
|
|
52
|
+
point of the curve.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
show: Plots the ultimate point over the raw input curve.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The (strain, stress) of the ultimate point in raw units.
|
|
59
|
+
"""
|
|
60
|
+
ultimate_point = (self.ultimate_x, self.ultimate_y)
|
|
61
|
+
if show:
|
|
62
|
+
graph = UltimatePointGraph(self.strain_raw, self.stress_raw, ultimate_point)
|
|
63
|
+
graph.show()
|
|
64
|
+
|
|
65
|
+
return ultimate_point
|
|
66
|
+
|
|
67
|
+
def fit_modulus_iso527(self, show: bool = True) -> float:
|
|
68
|
+
"""Fits modulus via ISO 527 linear regression over the
|
|
69
|
+
[STRAIN_LOWER, STRAIN_UPPER] strain range.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
show: Plots the fitted secant line over the raw input curve.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
ValueError: Fewer than 2 raw strain samples fall in the range.
|
|
76
|
+
"""
|
|
77
|
+
xs = self.strain_raw
|
|
78
|
+
ys = self.stress_raw
|
|
79
|
+
|
|
80
|
+
target = xs.between(STRAIN_LOWER, STRAIN_UPPER)
|
|
81
|
+
if target.sum() < 2:
|
|
82
|
+
raise ValueError("Not enough raw strain data in the range for regression.")
|
|
83
|
+
|
|
84
|
+
res = stats.linregress(xs[target], ys[target], "greater")
|
|
85
|
+
m = res.slope
|
|
86
|
+
shift = res.intercept
|
|
87
|
+
|
|
88
|
+
if show:
|
|
89
|
+
secant = m * xs + shift
|
|
90
|
+
graph = Iso527Graph(xs, ys, secant, m)
|
|
91
|
+
graph.show()
|
|
92
|
+
|
|
93
|
+
return m
|
|
94
|
+
|
|
95
|
+
def fit_modulus_rmsprop(
|
|
96
|
+
self,
|
|
97
|
+
tol: float = DEFAULT_TOL,
|
|
98
|
+
max_iter: int = DEFAULT_MAX_ITER,
|
|
99
|
+
show: bool = True,
|
|
100
|
+
) -> float:
|
|
101
|
+
"""Fits modulus by using RMSProp to minimize the area enclosed between
|
|
102
|
+
the normalized curve and its secant line.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
tol: Stops iterating once the relative change in modulus is below this.
|
|
106
|
+
max_iter: Maximum number of RMSProp iterations to run.
|
|
107
|
+
show: Plays an animation of the fitting process.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
The fitted modulus, rescaled back to raw strain/stress units.
|
|
111
|
+
"""
|
|
112
|
+
m = 1.0
|
|
113
|
+
v = 0.0
|
|
114
|
+
|
|
115
|
+
xs = self.strain_norm
|
|
116
|
+
ys = self.stress_norm
|
|
117
|
+
|
|
118
|
+
x0 = float(self.strain_norm.iloc[0])
|
|
119
|
+
y0 = float(self.stress_norm.iloc[0])
|
|
120
|
+
|
|
121
|
+
m_records = []
|
|
122
|
+
loss_records = []
|
|
123
|
+
lr_records = []
|
|
124
|
+
|
|
125
|
+
for _ in range(max_iter):
|
|
126
|
+
area = self._compute_enclosed_area(m, x0, y0, xs, ys)
|
|
127
|
+
area_eps = self._compute_enclosed_area(m + TINY_MODULUS, x0, y0, xs, ys)
|
|
128
|
+
gradient = (area_eps - area) / TINY_MODULUS
|
|
129
|
+
v, lr = self._update_rmsprop(v, gradient)
|
|
130
|
+
|
|
131
|
+
m_records.append(m)
|
|
132
|
+
loss_records.append(area)
|
|
133
|
+
lr_records.append(lr)
|
|
134
|
+
|
|
135
|
+
m_new = m - lr * gradient
|
|
136
|
+
if abs(m_new - m) / (abs(m) + CONVERGENCE_EPS) < tol:
|
|
137
|
+
break
|
|
138
|
+
m = m_new
|
|
139
|
+
|
|
140
|
+
m_scale = m * self.ultimate_y / self.ultimate_x
|
|
141
|
+
|
|
142
|
+
if show:
|
|
143
|
+
ani = RmsPropAnimation(xs, ys, m_records, lr_records, loss_records)
|
|
144
|
+
ani.play()
|
|
145
|
+
|
|
146
|
+
return m_scale
|
|
147
|
+
|
|
148
|
+
def find_break_point(
|
|
149
|
+
self,
|
|
150
|
+
threshold: float = BREAK_THRESHOLD,
|
|
151
|
+
show: bool = True,
|
|
152
|
+
) -> tuple[float, float]:
|
|
153
|
+
"""Locates the first point where the normalized stress curve's slope
|
|
154
|
+
drops below -threshold, signaling the specimen break.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
threshold: Slope drop (in normalized stress per sample) that
|
|
158
|
+
signals a break.
|
|
159
|
+
show: Plots the detected break point over the normalized curve.
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
The (strain, stress) of the break point in raw units, or the last
|
|
163
|
+
sample's coordinates if no break is detected.
|
|
164
|
+
"""
|
|
165
|
+
xs = self.strain_norm
|
|
166
|
+
ys = self.stress_norm
|
|
167
|
+
|
|
168
|
+
gradient = -np.gradient(ys)
|
|
169
|
+
positions = np.where(gradient > threshold)[0]
|
|
170
|
+
position = int(positions[0]) if positions.size > 0 else -1
|
|
171
|
+
label = int(xs.index[position])
|
|
172
|
+
|
|
173
|
+
if show:
|
|
174
|
+
break_pt_norm = float(xs[label]), float(ys[label])
|
|
175
|
+
graph = BreakDetectGraph(xs, ys, gradient, threshold, break_pt_norm)
|
|
176
|
+
graph.show()
|
|
177
|
+
|
|
178
|
+
return float(self.strain_raw[label]), float(self.stress_raw[label])
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def _compute_enclosed_area(
|
|
182
|
+
m: float, x0: float, y0: float, xs: pd.Series, ys: pd.Series
|
|
183
|
+
) -> float:
|
|
184
|
+
shift = y0 - m * x0
|
|
185
|
+
secant = m * xs + shift
|
|
186
|
+
clipped = np.clip(ys - secant, 0, None)
|
|
187
|
+
return float(integrate.trapezoid(clipped, xs))
|
|
188
|
+
|
|
189
|
+
@staticmethod
|
|
190
|
+
def _update_rmsprop(v: float, gradient: float) -> tuple[float, float]:
|
|
191
|
+
v_new = RMS_PROP_ALPHA * v + (1 - RMS_PROP_ALPHA) * (gradient**2)
|
|
192
|
+
lr = RMS_PROP_BASE_LR / (v_new**0.5 + RMS_PROP_EPS)
|
|
193
|
+
return v_new, lr
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import matplotlib.pyplot as plt
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pandas as pd
|
|
4
|
+
from matplotlib import animation
|
|
5
|
+
from matplotlib.collections import PathCollection, PolyCollection
|
|
6
|
+
from matplotlib.lines import Line2D
|
|
7
|
+
from matplotlib.text import Annotation
|
|
8
|
+
|
|
9
|
+
# Shared constants
|
|
10
|
+
MARKER_SIZE = 49
|
|
11
|
+
MARKER_ZORDER = 5
|
|
12
|
+
LINE_WIDTH = 1.5
|
|
13
|
+
SECANT_POSITION_DIVISOR = 50
|
|
14
|
+
ANNOTATION_OFFSET_LEFT = (5, 0)
|
|
15
|
+
ANNOTATION_OFFSET_RIGHT = (-10, 0)
|
|
16
|
+
COLOR_BLUE = "tab:blue"
|
|
17
|
+
COLOR_ORANGE = "tab:orange"
|
|
18
|
+
|
|
19
|
+
# RmsPropAnimation constants
|
|
20
|
+
FIG_SIZE_RMSPROP = (4.8, 6)
|
|
21
|
+
HEIGHT_RATIOS_RMSPROP = [2.4, 0.8, 0.8]
|
|
22
|
+
ANI_INTERVAL = 50
|
|
23
|
+
FRAME_SIZE_REQUEST = 50
|
|
24
|
+
ALPHA_FILL = 0.3
|
|
25
|
+
COLOR_FILL = "lightsteelblue"
|
|
26
|
+
PROGRESS_MARKER_EDGECOLOR = "white"
|
|
27
|
+
|
|
28
|
+
# Iso527Graph constants
|
|
29
|
+
FIG_SIZE_ISO527 = (4.8, 3.6)
|
|
30
|
+
|
|
31
|
+
# UltimatePointGraph constants
|
|
32
|
+
FIG_SIZE_ULTIMATE = (4.8, 3.6)
|
|
33
|
+
|
|
34
|
+
# BreakDetectGraph constants
|
|
35
|
+
FIG_SIZE_BREAK = (4.8, 4.8)
|
|
36
|
+
HEIGHT_RATIOS_BREAK = [2.4, 0.8]
|
|
37
|
+
|
|
38
|
+
Point = tuple[float, float]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _get_secant_line(modulus: float, strain: pd.Series, stress: pd.Series) -> pd.Series:
|
|
42
|
+
"""Calculates a secant line pinned directly to the initial data point."""
|
|
43
|
+
intercept = stress.values[0] - modulus * strain.values[0]
|
|
44
|
+
return modulus * strain + intercept
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _get_annotation_pos(strain: pd.Series, secant: pd.Series) -> Point:
|
|
48
|
+
"""Determines a stable, indexed coordinate for placing text annotations."""
|
|
49
|
+
idx = max(1, len(strain) // SECANT_POSITION_DIVISOR)
|
|
50
|
+
return float(strain.iloc[idx]), float(secant.iloc[idx])
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _show_figure(fig: plt.Figure) -> None:
|
|
54
|
+
"""Displays only the given figure, leaving any other open figures untouched."""
|
|
55
|
+
fig.canvas.manager.show()
|
|
56
|
+
fig.canvas.mpl_connect("close_event", lambda _: fig.canvas.stop_event_loop())
|
|
57
|
+
fig.canvas.start_event_loop()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class RmsPropAnimation:
|
|
61
|
+
"""Animates the RMSProp fit: a moving secant line over the stress-strain
|
|
62
|
+
curve alongside the learning-rate and loss traces. Call play() to start."""
|
|
63
|
+
|
|
64
|
+
_fig: plt.Figure
|
|
65
|
+
_ax_norm: plt.Axes
|
|
66
|
+
_ax_lr: plt.Axes
|
|
67
|
+
_ax_loss: plt.Axes
|
|
68
|
+
_line_secant: Line2D
|
|
69
|
+
_ann_secant: Annotation
|
|
70
|
+
_lr_marker: PathCollection
|
|
71
|
+
_loss_marker: PathCollection
|
|
72
|
+
_fill: PolyCollection | None
|
|
73
|
+
_ani: animation.FuncAnimation | None
|
|
74
|
+
_rids: list[int]
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
strain: pd.Series,
|
|
79
|
+
stress: pd.Series,
|
|
80
|
+
m_records: list[float],
|
|
81
|
+
lr_records: list[float],
|
|
82
|
+
loss_records: list[float],
|
|
83
|
+
):
|
|
84
|
+
self.strain = strain
|
|
85
|
+
self.stress = stress
|
|
86
|
+
self.m_records = m_records
|
|
87
|
+
self.lr_records = lr_records
|
|
88
|
+
self.loss_records = loss_records
|
|
89
|
+
|
|
90
|
+
self._fig, axs = plt.subplots(
|
|
91
|
+
nrows=3,
|
|
92
|
+
ncols=1,
|
|
93
|
+
figsize=FIG_SIZE_RMSPROP,
|
|
94
|
+
tight_layout=True,
|
|
95
|
+
height_ratios=HEIGHT_RATIOS_RMSPROP,
|
|
96
|
+
)
|
|
97
|
+
self._ax_norm, self._ax_lr, self._ax_loss = axs
|
|
98
|
+
|
|
99
|
+
self._setup_norm_axis()
|
|
100
|
+
self._setup_lr_axis()
|
|
101
|
+
self._setup_loss_axis()
|
|
102
|
+
self._fill = None
|
|
103
|
+
self._ani = None
|
|
104
|
+
self._rids = []
|
|
105
|
+
|
|
106
|
+
def _setup_norm_axis(self) -> None:
|
|
107
|
+
self._ax_norm.plot(
|
|
108
|
+
self.strain,
|
|
109
|
+
self.stress,
|
|
110
|
+
label="Stress-Strain Curve",
|
|
111
|
+
linewidth=LINE_WIDTH,
|
|
112
|
+
color=COLOR_BLUE,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Placeholder y=x line, overwritten with the real secant on the first animation frame
|
|
116
|
+
(self._line_secant,) = self._ax_norm.plot(
|
|
117
|
+
self.strain,
|
|
118
|
+
self.strain,
|
|
119
|
+
label="RMSProp Fit",
|
|
120
|
+
linewidth=LINE_WIDTH,
|
|
121
|
+
linestyle="--",
|
|
122
|
+
color=COLOR_ORANGE,
|
|
123
|
+
)
|
|
124
|
+
self._ann_secant = self._ax_norm.annotate(
|
|
125
|
+
text="",
|
|
126
|
+
xy=_get_annotation_pos(self.strain, self.strain),
|
|
127
|
+
xytext=ANNOTATION_OFFSET_LEFT,
|
|
128
|
+
textcoords="offset points",
|
|
129
|
+
color=COLOR_ORANGE,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
self._ax_norm.legend()
|
|
133
|
+
self._ax_norm.set(
|
|
134
|
+
xlabel="Normalized Strain",
|
|
135
|
+
ylabel="Normalized Stress",
|
|
136
|
+
title="RMSProp Optimization",
|
|
137
|
+
)
|
|
138
|
+
self._ax_norm.grid(True)
|
|
139
|
+
self.initial_ybound = self._ax_norm.get_ybound()
|
|
140
|
+
|
|
141
|
+
def _setup_lr_axis(self) -> None:
|
|
142
|
+
self._ax_lr.semilogy(self.lr_records, linewidth=LINE_WIDTH, color=COLOR_BLUE)
|
|
143
|
+
self._lr_marker = self._ax_lr.scatter(
|
|
144
|
+
0,
|
|
145
|
+
self.lr_records[0],
|
|
146
|
+
facecolors=COLOR_ORANGE,
|
|
147
|
+
edgecolors=PROGRESS_MARKER_EDGECOLOR,
|
|
148
|
+
linewidth=LINE_WIDTH,
|
|
149
|
+
zorder=MARKER_ZORDER,
|
|
150
|
+
s=MARKER_SIZE,
|
|
151
|
+
)
|
|
152
|
+
self._ax_lr.set(xlabel="Iteration", ylabel="LR")
|
|
153
|
+
self._ax_lr.grid(True)
|
|
154
|
+
|
|
155
|
+
def _setup_loss_axis(self) -> None:
|
|
156
|
+
self._ax_loss.plot(self.loss_records, linewidth=LINE_WIDTH, color=COLOR_BLUE)
|
|
157
|
+
self._loss_marker = self._ax_loss.scatter(
|
|
158
|
+
0,
|
|
159
|
+
self.loss_records[0],
|
|
160
|
+
facecolors=COLOR_ORANGE,
|
|
161
|
+
edgecolors=PROGRESS_MARKER_EDGECOLOR,
|
|
162
|
+
linewidth=LINE_WIDTH,
|
|
163
|
+
zorder=MARKER_ZORDER,
|
|
164
|
+
s=MARKER_SIZE,
|
|
165
|
+
)
|
|
166
|
+
self._ax_loss.set(xlabel="Iteration", ylabel="Loss")
|
|
167
|
+
self._ax_loss.grid(True)
|
|
168
|
+
|
|
169
|
+
def _get_record_indices(self) -> list[int]:
|
|
170
|
+
total = len(self.m_records)
|
|
171
|
+
step = max(1, total // FRAME_SIZE_REQUEST)
|
|
172
|
+
return list(range((total - 1) % step, total, step))
|
|
173
|
+
|
|
174
|
+
def _update_frame(self, fid: int) -> None:
|
|
175
|
+
rid = self._rids[fid]
|
|
176
|
+
m = self.m_records[rid]
|
|
177
|
+
secant = _get_secant_line(m, self.strain, self.stress)
|
|
178
|
+
|
|
179
|
+
# Redraw secant profile line adjustments
|
|
180
|
+
self._line_secant.set_ydata(secant)
|
|
181
|
+
self._ann_secant.set_text(f"E = {m:.2f}")
|
|
182
|
+
self._ann_secant.xy = _get_annotation_pos(self.strain, secant)
|
|
183
|
+
|
|
184
|
+
# Update shaded regional error tracking
|
|
185
|
+
if self._fill:
|
|
186
|
+
self._fill.remove()
|
|
187
|
+
|
|
188
|
+
self._fill = self._ax_norm.fill_between(
|
|
189
|
+
x=self.strain,
|
|
190
|
+
y1=self.stress,
|
|
191
|
+
y2=secant,
|
|
192
|
+
where=(self.stress > secant),
|
|
193
|
+
alpha=ALPHA_FILL,
|
|
194
|
+
color=COLOR_FILL,
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# Update dynamic progress indicator markers
|
|
198
|
+
self._lr_marker.set_offsets(np.array([[rid, self.lr_records[rid]]]))
|
|
199
|
+
self._loss_marker.set_offsets(np.array([[rid, self.loss_records[rid]]]))
|
|
200
|
+
self._ax_norm.set_ybound(*self.initial_ybound)
|
|
201
|
+
|
|
202
|
+
def play(self) -> None:
|
|
203
|
+
self._rids = self._get_record_indices()
|
|
204
|
+
self._ani = animation.FuncAnimation(
|
|
205
|
+
fig=self._fig,
|
|
206
|
+
func=self._update_frame,
|
|
207
|
+
frames=len(self._rids),
|
|
208
|
+
interval=ANI_INTERVAL,
|
|
209
|
+
blit=False,
|
|
210
|
+
repeat=False,
|
|
211
|
+
)
|
|
212
|
+
_show_figure(self._fig)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class Iso527Graph:
|
|
216
|
+
"""Renders the static ISO 527 stress-strain curve with the fitted secant
|
|
217
|
+
line. Call show() to display it."""
|
|
218
|
+
|
|
219
|
+
_fig: plt.Figure
|
|
220
|
+
|
|
221
|
+
def __init__(
|
|
222
|
+
self,
|
|
223
|
+
strain: pd.Series,
|
|
224
|
+
stress: pd.Series,
|
|
225
|
+
secant: pd.Series,
|
|
226
|
+
m: float,
|
|
227
|
+
):
|
|
228
|
+
self._fig, ax = plt.subplots(figsize=FIG_SIZE_ISO527, tight_layout=True)
|
|
229
|
+
|
|
230
|
+
ax.plot(
|
|
231
|
+
strain,
|
|
232
|
+
stress,
|
|
233
|
+
label="Stress-Strain Curve",
|
|
234
|
+
linewidth=LINE_WIDTH,
|
|
235
|
+
color=COLOR_BLUE,
|
|
236
|
+
)
|
|
237
|
+
ybound = ax.get_ybound()
|
|
238
|
+
|
|
239
|
+
ax.plot(
|
|
240
|
+
strain,
|
|
241
|
+
secant,
|
|
242
|
+
label="ISO 527 Fit",
|
|
243
|
+
linewidth=LINE_WIDTH,
|
|
244
|
+
linestyle="--",
|
|
245
|
+
color=COLOR_ORANGE,
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
ax.annotate(
|
|
249
|
+
text=f"E = {m:.1f}",
|
|
250
|
+
xy=_get_annotation_pos(strain, secant),
|
|
251
|
+
xytext=ANNOTATION_OFFSET_LEFT,
|
|
252
|
+
textcoords="offset points",
|
|
253
|
+
color=COLOR_ORANGE,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
ax.set(xlabel="Strain", ylabel="Stress", title="ISO 527 Regression")
|
|
257
|
+
ax.grid(True)
|
|
258
|
+
ax.set_ybound(*ybound)
|
|
259
|
+
ax.legend()
|
|
260
|
+
|
|
261
|
+
def show(self) -> None:
|
|
262
|
+
_show_figure(self._fig)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class UltimatePointGraph:
|
|
266
|
+
"""Renders the stress-strain curve with the ultimate (peak stress) point
|
|
267
|
+
marked. Call show() to display it."""
|
|
268
|
+
|
|
269
|
+
_fig: plt.Figure
|
|
270
|
+
|
|
271
|
+
def __init__(
|
|
272
|
+
self,
|
|
273
|
+
strain: pd.Series,
|
|
274
|
+
stress: pd.Series,
|
|
275
|
+
ultimate_point: Point,
|
|
276
|
+
):
|
|
277
|
+
self._fig, ax = plt.subplots(figsize=FIG_SIZE_ULTIMATE, tight_layout=True)
|
|
278
|
+
|
|
279
|
+
ax.plot(
|
|
280
|
+
strain,
|
|
281
|
+
stress,
|
|
282
|
+
label="Stress-Strain Curve",
|
|
283
|
+
linewidth=LINE_WIDTH,
|
|
284
|
+
color=COLOR_BLUE,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
ux, uy = ultimate_point
|
|
288
|
+
ax.scatter(
|
|
289
|
+
ux,
|
|
290
|
+
uy,
|
|
291
|
+
marker="^",
|
|
292
|
+
s=MARKER_SIZE,
|
|
293
|
+
c=COLOR_ORANGE,
|
|
294
|
+
label="Ultimate Point",
|
|
295
|
+
zorder=MARKER_ZORDER,
|
|
296
|
+
)
|
|
297
|
+
ax.annotate(
|
|
298
|
+
f"({ux:.2f}, {uy:.2f})",
|
|
299
|
+
xy=(ux, uy),
|
|
300
|
+
xytext=ANNOTATION_OFFSET_RIGHT,
|
|
301
|
+
textcoords="offset points",
|
|
302
|
+
ha="right",
|
|
303
|
+
va="center",
|
|
304
|
+
color=COLOR_ORANGE,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
ax.set(xlabel="Strain", ylabel="Stress", title="Ultimate Point Detection")
|
|
308
|
+
ax.grid(True)
|
|
309
|
+
ax.legend()
|
|
310
|
+
|
|
311
|
+
def show(self) -> None:
|
|
312
|
+
_show_figure(self._fig)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
class BreakDetectGraph:
|
|
316
|
+
"""Renders the stress-strain curve with the detected break point and its
|
|
317
|
+
underlying gradient trace. Call show() to display it."""
|
|
318
|
+
|
|
319
|
+
_fig: plt.Figure
|
|
320
|
+
_ax_norm: plt.Axes
|
|
321
|
+
_ax_break: plt.Axes
|
|
322
|
+
|
|
323
|
+
def __init__(
|
|
324
|
+
self,
|
|
325
|
+
strain: pd.Series,
|
|
326
|
+
stress: pd.Series,
|
|
327
|
+
bindex: np.ndarray,
|
|
328
|
+
threshold: float,
|
|
329
|
+
break_point: tuple[float, float],
|
|
330
|
+
):
|
|
331
|
+
self._fig, axs = plt.subplots(
|
|
332
|
+
nrows=2,
|
|
333
|
+
ncols=1,
|
|
334
|
+
figsize=FIG_SIZE_BREAK,
|
|
335
|
+
tight_layout=True,
|
|
336
|
+
height_ratios=HEIGHT_RATIOS_BREAK,
|
|
337
|
+
)
|
|
338
|
+
|
|
339
|
+
self._ax_norm, self._ax_break = axs
|
|
340
|
+
|
|
341
|
+
self._ax_norm.plot(
|
|
342
|
+
strain,
|
|
343
|
+
stress,
|
|
344
|
+
label="Stress-Strain Curve",
|
|
345
|
+
linewidth=LINE_WIDTH,
|
|
346
|
+
color=COLOR_BLUE,
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
self._ax_norm.set(ylabel="Normalized Stress", title="Break Point Detection")
|
|
350
|
+
self._ax_norm.grid(True)
|
|
351
|
+
|
|
352
|
+
bx, by = break_point
|
|
353
|
+
self._ax_norm.scatter(
|
|
354
|
+
bx,
|
|
355
|
+
by,
|
|
356
|
+
marker="x",
|
|
357
|
+
s=MARKER_SIZE,
|
|
358
|
+
c=COLOR_ORANGE,
|
|
359
|
+
label="Break Point",
|
|
360
|
+
zorder=MARKER_ZORDER,
|
|
361
|
+
)
|
|
362
|
+
self._ax_norm.annotate(
|
|
363
|
+
f"({bx:.2f}, {by:.2f})",
|
|
364
|
+
xy=(bx, by),
|
|
365
|
+
xytext=ANNOTATION_OFFSET_RIGHT,
|
|
366
|
+
textcoords="offset points",
|
|
367
|
+
ha="right",
|
|
368
|
+
va="center",
|
|
369
|
+
color=COLOR_ORANGE,
|
|
370
|
+
)
|
|
371
|
+
self._ax_norm.legend()
|
|
372
|
+
|
|
373
|
+
self._ax_break.plot(strain, bindex)
|
|
374
|
+
self._ax_break.axhline(threshold, color=COLOR_ORANGE, linestyle="--")
|
|
375
|
+
self._ax_break.grid(True)
|
|
376
|
+
self._ax_break.set(xlabel="Normalized Strain", ylabel="Gradient")
|
|
377
|
+
|
|
378
|
+
def show(self) -> None:
|
|
379
|
+
_show_figure(self._fig)
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mechprops-extractor
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Extract Young's modulus, ultimate point, and break-point from stress-strain curves
|
|
5
|
+
Author-email: wyatthoho <wyatthoho@gmail.com>
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Wyatt Ho
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/wyatthoho/mechprops-extractor
|
|
29
|
+
Project-URL: Repository, https://github.com/wyatthoho/mechprops-extractor
|
|
30
|
+
Keywords: stress-strain,mechanical-properties,materials-testing,youngs-modulus
|
|
31
|
+
Classifier: Development Status :: 3 - Alpha
|
|
32
|
+
Classifier: Intended Audience :: Science/Research
|
|
33
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
34
|
+
Classifier: Programming Language :: Python :: 3
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
36
|
+
Classifier: Topic :: Scientific/Engineering
|
|
37
|
+
Requires-Python: >=3.12
|
|
38
|
+
Description-Content-Type: text/markdown
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Requires-Dist: pandas>=2.3
|
|
41
|
+
Requires-Dist: matplotlib>=3.10
|
|
42
|
+
Requires-Dist: scipy>=1.16
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# Mechanical Properties Extractor
|
|
46
|
+
|
|
47
|
+
Extracts Young's modulus, ultimate point, and break-point properties from
|
|
48
|
+
stress-strain curve data, via ISO 527 linear regression, a custom
|
|
49
|
+
RMSProp-based curve fit, or break-point detection.
|
|
50
|
+
|
|
51
|
+
## Features
|
|
52
|
+
|
|
53
|
+
- Young's modulus via ISO 527 secant-line linear regression
|
|
54
|
+
- Young's modulus via a custom RMSProp-based enclosed-area fit
|
|
55
|
+
- Ultimate (peak stress) point detection on the stress-strain curve
|
|
56
|
+
- Break-point (failure) detection on the stress-strain curve
|
|
57
|
+
- Built-in plots and an animation of the RMSProp fitting process
|
|
58
|
+
|
|
59
|
+
## Installation
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install mechprops-extractor
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
For development (editable install):
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pip install -e .
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
|
|
73
|
+
Load a stress-strain CSV (`strain`, `stress` columns) into a `StressStrainCurve`,
|
|
74
|
+
then call any combination of its analysis methods. The example below uses one
|
|
75
|
+
of the sample datasets bundled in [`data/`](data):
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
import pandas as pd
|
|
79
|
+
|
|
80
|
+
from mechprops import StressStrainCurve
|
|
81
|
+
|
|
82
|
+
df = pd.read_csv("data/polyimide-stress-strain.csv")
|
|
83
|
+
curve = StressStrainCurve(df["strain"], df["stress"])
|
|
84
|
+
|
|
85
|
+
curve.fit_modulus_iso527() # Young's modulus via ISO 527 linear regression
|
|
86
|
+
curve.fit_modulus_rmsprop() # Young's modulus via RMSProp area minimization
|
|
87
|
+
curve.find_ultimate_point() # (strain, stress) at the ultimate (peak stress) point
|
|
88
|
+
curve.find_break_point() # (strain, stress) at the detected break point
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Each method returns its computed value and, by default, opens a plot window
|
|
92
|
+
illustrating the result — close it to continue. Pass `show=False` to skip the
|
|
93
|
+
plot and get the value only.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/mechprops/__init__.py
|
|
5
|
+
src/mechprops/curve.py
|
|
6
|
+
src/mechprops/plotter.py
|
|
7
|
+
src/mechprops_extractor.egg-info/PKG-INFO
|
|
8
|
+
src/mechprops_extractor.egg-info/SOURCES.txt
|
|
9
|
+
src/mechprops_extractor.egg-info/dependency_links.txt
|
|
10
|
+
src/mechprops_extractor.egg-info/requires.txt
|
|
11
|
+
src/mechprops_extractor.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
mechprops
|