delta-theory 6.9.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.
- apps/__init__.py +6 -0
- apps/delta_fatigue_app.py +337 -0
- core/Universal_Lindemann.py +422 -0
- core/__init__.py +35 -0
- core/dbt_unified.py +690 -0
- core/materials.py +688 -0
- core/unified_yield_fatigue_v6_9.py +762 -0
- delta_theory-6.9.0.dist-info/METADATA +640 -0
- delta_theory-6.9.0.dist-info/RECORD +15 -0
- delta_theory-6.9.0.dist-info/WHEEL +5 -0
- delta_theory-6.9.0.dist-info/entry_points.txt +3 -0
- delta_theory-6.9.0.dist-info/licenses/LICENSE +26 -0
- delta_theory-6.9.0.dist-info/top_level.txt +3 -0
- validation/__init__.py +10 -0
- validation/fatigue_redis_api.py +334 -0
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Unified Yield + δ-fatigue (v6.9b)
|
|
3
|
+
|
|
4
|
+
- Yield model: v5.0 FINAL
|
|
5
|
+
σ_y = σ_base(δ) + Δσ_ss(c) + Δσ_ρ(ε) + Δσ_ppt(r,f)
|
|
6
|
+
|
|
7
|
+
- Fatigue model: v6.8 δ-fatigue damage
|
|
8
|
+
dD/dN = 0 (r<=r_th) else A_eff * (r-r_th)^n
|
|
9
|
+
with r = (amplitude)/(yield in the same loading mode)
|
|
10
|
+
and failure when Λ(D)=D/(1-D) reaches 1 (i.e., D>=0.5) by default.
|
|
11
|
+
|
|
12
|
+
- Added from v4.1 (was missing in original v6.9 integration)
|
|
13
|
+
τ/σ = (α_s/α_t) * C_class * T_twin * A_texture
|
|
14
|
+
σ_c/σ_t = R_comp (twinning/asymmetry)
|
|
15
|
+
|
|
16
|
+
This lets v6.9 operate not only with tensile amplitude σ_a, but also
|
|
17
|
+
with shear amplitude τ_a or compression amplitude σ_a^c.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
from dataclasses import dataclass, replace
|
|
24
|
+
from typing import Dict, Literal, Optional, Tuple
|
|
25
|
+
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
# ==============================================================================
|
|
29
|
+
# Physical constants
|
|
30
|
+
# ==============================================================================
|
|
31
|
+
PI = np.pi
|
|
32
|
+
ELECTRON_CHARGE_J = 1.602176634e-19 # 1 eV in Joule
|
|
33
|
+
M_TAYLOR = 3.0
|
|
34
|
+
ALPHA_TAYLOR = 0.3
|
|
35
|
+
|
|
36
|
+
# ==============================================================================
|
|
37
|
+
# v5.0 δ-yield constants
|
|
38
|
+
# ==============================================================================
|
|
39
|
+
ALPHA_DELTA = {'BCC': 0.289, 'FCC': 0.250, 'HCP': 0.350}
|
|
40
|
+
N_EXPONENT = {'interstitial': 0.90, 'substitutional': 0.95}
|
|
41
|
+
|
|
42
|
+
# Work hardening presets
|
|
43
|
+
K_RHO = {
|
|
44
|
+
'Fe': 1.24e15, 'Cu': 1.07e15, 'Al': 5.98e14, 'Ni': 1.27e15,
|
|
45
|
+
'BCC': 1.2e15, 'FCC': 1.0e15, 'HCP': 0.8e15,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
# ==============================================================================
|
|
49
|
+
# v4.1 τ/σ factors
|
|
50
|
+
# ==============================================================================
|
|
51
|
+
CU_TAU_SIGMA_TORSION = 0.565 # Cu torsion calibration point
|
|
52
|
+
DEFAULT_BCC_W110 = 0.0 # {112} dominant
|
|
53
|
+
|
|
54
|
+
# Twinning factor (tension) and compression/tension asymmetry
|
|
55
|
+
T_TWIN = {
|
|
56
|
+
'Cu': 1.0, 'Al': 1.0, 'Ni': 1.0, 'Au': 1.0, 'Ag': 1.0,
|
|
57
|
+
'Fe': 1.0, 'W': 1.0,
|
|
58
|
+
'Ti': 1.0,
|
|
59
|
+
'Mg': 0.6,
|
|
60
|
+
'Zn': 0.9,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
R_COMP = { # σ_c/σ_t
|
|
64
|
+
'Cu': 1.0, 'Al': 1.0, 'Ni': 1.0, 'Au': 1.0, 'Ag': 1.0,
|
|
65
|
+
'Fe': 1.0, 'W': 1.0,
|
|
66
|
+
'Ti': 1.0,
|
|
67
|
+
'Mg': 0.6,
|
|
68
|
+
'Zn': 1.2,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
T_REF = {
|
|
72
|
+
'BCC': np.array([1, 0, 0], dtype=float),
|
|
73
|
+
'FCC': np.array([1, 1, 0], dtype=float),
|
|
74
|
+
'HCP': np.array([1, 0, 0], dtype=float),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# ==============================================================================
|
|
78
|
+
# Material database
|
|
79
|
+
# ==============================================================================
|
|
80
|
+
@dataclass(frozen=True)
|
|
81
|
+
class Material:
|
|
82
|
+
name: str
|
|
83
|
+
structure: Literal['BCC', 'FCC', 'HCP']
|
|
84
|
+
|
|
85
|
+
# v5.0 base-yield inputs
|
|
86
|
+
a: float # lattice parameter [m]
|
|
87
|
+
T_m: float # melting point [K]
|
|
88
|
+
dL: float # Lindemann parameter
|
|
89
|
+
Eb: float # bond energy [eV]
|
|
90
|
+
f_d: float # d-electron directionality factor
|
|
91
|
+
G: float # shear modulus [Pa]
|
|
92
|
+
|
|
93
|
+
# v4.1 τ/σ inputs
|
|
94
|
+
c_a: float = 1.633
|
|
95
|
+
A_texture: float = 1.0
|
|
96
|
+
T_twin: float = 1.0
|
|
97
|
+
R_comp: float = 1.0
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
MATERIALS: Dict[str, Material] = {
|
|
101
|
+
'Fe': Material('Fe', 'BCC', 2.92e-10, 1811, 0.18, 4.28, 1.5, 82e9, 1.633, 1.0, T_TWIN['Fe'], R_COMP['Fe']),
|
|
102
|
+
'W': Material('W', 'BCC', 3.16e-10, 3695, 0.16, 8.90, 4.7, 161e9, 1.633, 1.0, T_TWIN['W'], R_COMP['W']),
|
|
103
|
+
'Cu': Material('Cu', 'FCC', 3.61e-10, 1357, 0.10, 3.49, 2.0, 48e9, 1.633, 1.0, T_TWIN['Cu'], R_COMP['Cu']),
|
|
104
|
+
'Al': Material('Al', 'FCC', 4.05e-10, 933, 0.10, 3.39, 1.6, 26e9, 1.633, 1.0, T_TWIN['Al'], R_COMP['Al']),
|
|
105
|
+
'Ni': Material('Ni', 'FCC', 3.52e-10, 1728, 0.11, 4.44, 2.6, 76e9, 1.633, 1.0, T_TWIN['Ni'], R_COMP['Ni']),
|
|
106
|
+
'Au': Material('Au', 'FCC', 4.08e-10, 1337, 0.10, 3.81, 1.1, 27e9, 1.633, 1.0, T_TWIN['Au'], R_COMP['Au']),
|
|
107
|
+
'Ag': Material('Ag', 'FCC', 4.09e-10, 1235, 0.10, 2.95, 2.0, 30e9, 1.633, 1.0, T_TWIN['Ag'], R_COMP['Ag']),
|
|
108
|
+
'Ti': Material('Ti', 'HCP', 2.95e-10, 1941, 0.10, 4.85, 5.7, 44e9, 1.587, 1.0, T_TWIN['Ti'], R_COMP['Ti']),
|
|
109
|
+
'Mg': Material('Mg', 'HCP', 3.21e-10, 923, 0.117,1.51, 8.2, 17e9, 1.624, 1.0, T_TWIN['Mg'], R_COMP['Mg']),
|
|
110
|
+
'Zn': Material('Zn', 'HCP', 2.66e-10, 693, 0.12, 1.35, 2.0, 43e9, 1.856, 1.0, T_TWIN['Zn'], R_COMP['Zn']),
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
# ==============================================================================
|
|
114
|
+
# Helpers
|
|
115
|
+
# ==============================================================================
|
|
116
|
+
|
|
117
|
+
def calc_burgers(a: float, structure: str) -> float:
|
|
118
|
+
if structure == 'BCC':
|
|
119
|
+
return a * np.sqrt(3) / 2
|
|
120
|
+
if structure == 'FCC':
|
|
121
|
+
return a / np.sqrt(2)
|
|
122
|
+
return a
|
|
123
|
+
|
|
124
|
+
# ==============================================================================
|
|
125
|
+
# v5.0 yield components
|
|
126
|
+
# ==============================================================================
|
|
127
|
+
|
|
128
|
+
def sigma_base_delta(mat: Material, T_K: float = 300.0) -> float:
|
|
129
|
+
"""Base yield stress from δ-theory [MPa]."""
|
|
130
|
+
alpha = ALPHA_DELTA[mat.structure]
|
|
131
|
+
b = calc_burgers(mat.a, mat.structure)
|
|
132
|
+
V_act = b**3
|
|
133
|
+
HP = max(0.0, 1.0 - T_K / mat.T_m)
|
|
134
|
+
E_eff = mat.Eb * ELECTRON_CHARGE_J * alpha * mat.f_d
|
|
135
|
+
sigma = (E_eff / V_act) * mat.dL * HP / (2 * PI * M_TAYLOR)
|
|
136
|
+
return sigma / 1e6
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def delta_sigma_ss(c_wt_percent: float, k: float,
|
|
140
|
+
solute_type: Optional[Literal['interstitial', 'substitutional']]) -> float:
|
|
141
|
+
if solute_type is None or c_wt_percent <= 0:
|
|
142
|
+
return 0.0
|
|
143
|
+
n = N_EXPONENT[solute_type]
|
|
144
|
+
return k * (c_wt_percent ** n)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def calibrate_k_ss(c_wt_percent: float, sigma_exp_MPa: float,
|
|
148
|
+
sigma_base_MPa: float,
|
|
149
|
+
solute_type: Literal['interstitial', 'substitutional']) -> float:
|
|
150
|
+
n = N_EXPONENT[solute_type]
|
|
151
|
+
return (sigma_exp_MPa - sigma_base_MPa) / (c_wt_percent ** n)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def delta_sigma_taylor(eps: float, mat: Material, rho_0: float = 1e12) -> float:
|
|
155
|
+
"""Work hardening via Taylor relation [MPa]."""
|
|
156
|
+
if eps <= 0 and rho_0 <= 0:
|
|
157
|
+
return 0.0
|
|
158
|
+
K = K_RHO.get(mat.name, K_RHO.get(mat.structure, 1e15))
|
|
159
|
+
rho = rho_0 + K * max(eps, 0.0)
|
|
160
|
+
b = calc_burgers(mat.a, mat.structure)
|
|
161
|
+
return M_TAYLOR * ALPHA_TAYLOR * mat.G * b * np.sqrt(rho) / 1e6
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def delta_sigma_cutting(r_nm: float, f: float, gamma: float, G: float, b: float) -> float:
|
|
165
|
+
r = r_nm * 1e-9
|
|
166
|
+
if r <= 0 or f <= 0 or gamma <= 0:
|
|
167
|
+
return 0.0
|
|
168
|
+
T_line = 0.5 * G * b**2
|
|
169
|
+
return M_TAYLOR * (gamma / (2 * b)) * np.sqrt(3 * PI * f * r / T_line) / 1e6
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def delta_sigma_orowan(r_nm: float, f: float, G: float, b: float) -> float:
|
|
173
|
+
r = r_nm * 1e-9
|
|
174
|
+
if r <= 0 or f <= 0:
|
|
175
|
+
return 0.0
|
|
176
|
+
factor = max(0.1, (2 * PI / (3 * f))**0.5 - 2)
|
|
177
|
+
lambda_eff = 2 * r * factor
|
|
178
|
+
log_term = np.log(max(2 * r / b, 2))
|
|
179
|
+
return 0.4 * M_TAYLOR * G * b / lambda_eff * log_term / 1e6
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def delta_sigma_ppt(r_nm: float, f: float, gamma: float, mat: Material, A: float = 1.0) -> Tuple[float, str]:
|
|
183
|
+
if r_nm <= 0 or f <= 0:
|
|
184
|
+
return 0.0, 'None'
|
|
185
|
+
b = calc_burgers(mat.a, mat.structure)
|
|
186
|
+
d_cut = A * delta_sigma_cutting(r_nm, f, gamma, mat.G, b)
|
|
187
|
+
d_oro = A * delta_sigma_orowan(r_nm, f, mat.G, b)
|
|
188
|
+
return (d_cut, 'Cutting') if d_cut <= d_oro else (d_oro, 'Orowan')
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def calc_sigma_y(
|
|
192
|
+
mat: Material,
|
|
193
|
+
T_K: float = 300.0,
|
|
194
|
+
c_wt_percent: float = 0.0,
|
|
195
|
+
k_ss: float = 0.0,
|
|
196
|
+
solute_type: Optional[Literal['interstitial', 'substitutional']] = None,
|
|
197
|
+
eps: float = 0.0,
|
|
198
|
+
rho_0: float = 0.0,
|
|
199
|
+
r_ppt_nm: float = 0.0,
|
|
200
|
+
f_ppt: float = 0.0,
|
|
201
|
+
gamma_apb: float = 0.0,
|
|
202
|
+
A_ppt: float = 1.0,
|
|
203
|
+
) -> Dict[str, float | str]:
|
|
204
|
+
base = sigma_base_delta(mat, T_K)
|
|
205
|
+
ss = delta_sigma_ss(c_wt_percent, k_ss, solute_type)
|
|
206
|
+
wh = delta_sigma_taylor(eps, mat, rho_0) if (eps > 0 or rho_0 > 0) else 0.0
|
|
207
|
+
ppt, mech = delta_sigma_ppt(r_ppt_nm, f_ppt, gamma_apb, mat, A_ppt)
|
|
208
|
+
return {
|
|
209
|
+
'sigma_y': base + ss + wh + ppt,
|
|
210
|
+
'sigma_base': base,
|
|
211
|
+
'delta_ss': ss,
|
|
212
|
+
'delta_wh': wh,
|
|
213
|
+
'delta_ppt': ppt,
|
|
214
|
+
'ppt_mechanism': mech,
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# ==============================================================================
|
|
218
|
+
# v4.1: α_s/α_t and τ/σ
|
|
219
|
+
# ==============================================================================
|
|
220
|
+
|
|
221
|
+
def get_bond_vectors(structure: str, c_a_ratio: float = 1.633):
|
|
222
|
+
bonds = []
|
|
223
|
+
if structure == 'BCC':
|
|
224
|
+
for i in (-1, 1):
|
|
225
|
+
for j in (-1, 1):
|
|
226
|
+
for k in (-1, 1):
|
|
227
|
+
v = np.array([i, j, k], dtype=float)
|
|
228
|
+
bonds.append(v / np.linalg.norm(v))
|
|
229
|
+
elif structure == 'FCC':
|
|
230
|
+
for i in (-1, 1):
|
|
231
|
+
for j in (-1, 1):
|
|
232
|
+
bonds.append(np.array([i, j, 0], dtype=float) / np.sqrt(2))
|
|
233
|
+
bonds.append(np.array([i, 0, j], dtype=float) / np.sqrt(2))
|
|
234
|
+
bonds.append(np.array([0, i, j], dtype=float) / np.sqrt(2))
|
|
235
|
+
elif structure == 'HCP':
|
|
236
|
+
# basal-plane
|
|
237
|
+
for i in range(6):
|
|
238
|
+
angle = i * PI / 3
|
|
239
|
+
bonds.append(np.array([np.cos(angle), np.sin(angle), 0.0]))
|
|
240
|
+
# out-of-plane
|
|
241
|
+
r_xy = 1.0 / np.sqrt(3)
|
|
242
|
+
z = c_a_ratio / 2
|
|
243
|
+
length = np.sqrt(r_xy**2 + z**2)
|
|
244
|
+
r_norm, z_norm = r_xy / length, z / length
|
|
245
|
+
for i in range(3):
|
|
246
|
+
angle = i * 2 * PI / 3 + PI / 6
|
|
247
|
+
bonds.append(np.array([r_norm * np.cos(angle), r_norm * np.sin(angle), z_norm]))
|
|
248
|
+
bonds.append(np.array([r_norm * np.cos(angle + PI), r_norm * np.sin(angle + PI), -z_norm]))
|
|
249
|
+
return bonds
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def calc_alpha_tensile(bonds, tensile_dir: np.ndarray) -> float:
|
|
253
|
+
d = tensile_dir / np.linalg.norm(tensile_dir)
|
|
254
|
+
Z = len(bonds)
|
|
255
|
+
return sum(max(float(np.dot(b, d)), 0.0) for b in bonds) / Z
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def calc_alpha_shear(bonds, n: np.ndarray, s: np.ndarray) -> float:
|
|
259
|
+
n = n / np.linalg.norm(n)
|
|
260
|
+
s = s / np.linalg.norm(s)
|
|
261
|
+
Z = len(bonds)
|
|
262
|
+
return sum(abs(float(np.dot(b, n))) * abs(float(np.dot(b, s))) for b in bonds) / Z
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
def get_slip_system(structure: str, variant: Optional[str] = None) -> Tuple[np.ndarray, np.ndarray]:
|
|
266
|
+
if structure == 'BCC':
|
|
267
|
+
v = variant or '112'
|
|
268
|
+
if v == '110':
|
|
269
|
+
return (np.array([1, 1, 0], dtype=float) / np.sqrt(2),
|
|
270
|
+
np.array([1, -1, 1], dtype=float) / np.sqrt(3))
|
|
271
|
+
return (np.array([1, 1, 2], dtype=float) / np.sqrt(6),
|
|
272
|
+
np.array([1, 1, -1], dtype=float) / np.sqrt(3))
|
|
273
|
+
if structure == 'FCC':
|
|
274
|
+
return (np.array([1, 1, 1], dtype=float) / np.sqrt(3),
|
|
275
|
+
np.array([1, -1, 0], dtype=float) / np.sqrt(2))
|
|
276
|
+
# HCP: basal (simplest)
|
|
277
|
+
return (np.array([0, 0, 1], dtype=float), np.array([1, 0, 0], dtype=float))
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def calc_alpha_ratio(structure: str, c_a: float = 1.633, bcc_w110: float = DEFAULT_BCC_W110) -> Dict[str, float]:
|
|
281
|
+
bonds = get_bond_vectors(structure, c_a)
|
|
282
|
+
alpha_t = calc_alpha_tensile(bonds, T_REF[structure])
|
|
283
|
+
|
|
284
|
+
if structure == 'BCC':
|
|
285
|
+
n110, s110 = get_slip_system('BCC', '110')
|
|
286
|
+
n112, s112 = get_slip_system('BCC', '112')
|
|
287
|
+
a110 = calc_alpha_shear(bonds, n110, s110)
|
|
288
|
+
a112 = calc_alpha_shear(bonds, n112, s112)
|
|
289
|
+
alpha_s = bcc_w110 * a110 + (1.0 - bcc_w110) * a112
|
|
290
|
+
return {
|
|
291
|
+
'alpha_t': alpha_t,
|
|
292
|
+
'alpha_s': alpha_s,
|
|
293
|
+
'ratio': alpha_s / alpha_t,
|
|
294
|
+
'ratio_110': a110 / alpha_t,
|
|
295
|
+
'ratio_112': a112 / alpha_t,
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
n, s = get_slip_system(structure)
|
|
299
|
+
alpha_s = calc_alpha_shear(bonds, n, s)
|
|
300
|
+
return {'alpha_t': alpha_t, 'alpha_s': alpha_s, 'ratio': alpha_s / alpha_t}
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def calibrate_C_class() -> float:
|
|
304
|
+
fcc = calc_alpha_ratio('FCC')
|
|
305
|
+
return CU_TAU_SIGMA_TORSION / fcc['ratio']
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
C_CLASS_DEFAULT = calibrate_C_class()
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def tau_over_sigma(mat: Material,
|
|
312
|
+
C_class: float = C_CLASS_DEFAULT,
|
|
313
|
+
bcc_w110: float = DEFAULT_BCC_W110,
|
|
314
|
+
apply_C_class_hcp: bool = False) -> float:
|
|
315
|
+
al = calc_alpha_ratio(mat.structure, mat.c_a, bcc_w110)
|
|
316
|
+
c_cls = C_class if (mat.structure != 'HCP' or apply_C_class_hcp) else 1.0
|
|
317
|
+
return al['ratio'] * c_cls * mat.T_twin * mat.A_texture
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def sigma_c_over_sigma_t(mat: Material) -> float:
|
|
321
|
+
return mat.R_comp
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def yield_by_mode(
|
|
325
|
+
mat: Material,
|
|
326
|
+
sigma_y_tension_MPa: float,
|
|
327
|
+
mode: Literal['tensile', 'compression', 'shear'] = 'tensile',
|
|
328
|
+
C_class: float = C_CLASS_DEFAULT,
|
|
329
|
+
bcc_w110: float = DEFAULT_BCC_W110,
|
|
330
|
+
apply_C_class_hcp: bool = False,
|
|
331
|
+
) -> Tuple[float, Dict[str, float]]:
|
|
332
|
+
"""Return yield level [MPa] in the requested loading mode + diagnostics."""
|
|
333
|
+
tau_sig = tau_over_sigma(mat, C_class=C_class, bcc_w110=bcc_w110, apply_C_class_hcp=apply_C_class_hcp)
|
|
334
|
+
R = sigma_c_over_sigma_t(mat)
|
|
335
|
+
sigma_y_comp = sigma_y_tension_MPa * R
|
|
336
|
+
tau_y = sigma_y_tension_MPa * tau_sig
|
|
337
|
+
|
|
338
|
+
if mode == 'tensile':
|
|
339
|
+
y = sigma_y_tension_MPa
|
|
340
|
+
elif mode == 'compression':
|
|
341
|
+
y = sigma_y_comp
|
|
342
|
+
else:
|
|
343
|
+
y = tau_y
|
|
344
|
+
|
|
345
|
+
return y, {
|
|
346
|
+
'tau_over_sigma': tau_sig,
|
|
347
|
+
'sigma_c_over_sigma_t': R,
|
|
348
|
+
'sigma_y_tension': sigma_y_tension_MPa,
|
|
349
|
+
'sigma_y_compression': sigma_y_comp,
|
|
350
|
+
'tau_y': tau_y,
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
# ==============================================================================
|
|
354
|
+
# v6.8 δ-fatigue damage model
|
|
355
|
+
# ==============================================================================
|
|
356
|
+
|
|
357
|
+
FATIGUE_CLASS_PRESET = {
|
|
358
|
+
'BCC': {'r_th': 0.65, 'n': 10.0},
|
|
359
|
+
'FCC': {'r_th': 0.02, 'n': 7.0},
|
|
360
|
+
'HCP': {'r_th': 0.20, 'n': 9.0},
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
# A_int from δ parameters (normalized to Fe)
|
|
364
|
+
A_INT_DB = {
|
|
365
|
+
'Fe': 1.00,
|
|
366
|
+
'Cu': 1.41,
|
|
367
|
+
'Al': 0.71,
|
|
368
|
+
'Ni': 1.37,
|
|
369
|
+
'W': 0.85,
|
|
370
|
+
'Ti': 1.10,
|
|
371
|
+
'Mg': 0.60,
|
|
372
|
+
'Zn': 0.75,
|
|
373
|
+
'Au': 1.00,
|
|
374
|
+
'Ag': 1.00,
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def lambda_from_damage(D: float) -> float:
|
|
379
|
+
"""Λ(D)=D/(1-D)."""
|
|
380
|
+
if D <= 0:
|
|
381
|
+
return 0.0
|
|
382
|
+
if D >= 1:
|
|
383
|
+
return float('inf')
|
|
384
|
+
return D / (1.0 - D)
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def solve_N_fail(
|
|
388
|
+
r: float,
|
|
389
|
+
r_th: float,
|
|
390
|
+
n: float,
|
|
391
|
+
A_eff: float,
|
|
392
|
+
D0: float = 0.0,
|
|
393
|
+
D_fail: float = 0.5,
|
|
394
|
+
) -> float:
|
|
395
|
+
"""Closed-form cycles to reach D_fail for constant r."""
|
|
396
|
+
if r <= r_th:
|
|
397
|
+
return float('inf')
|
|
398
|
+
if A_eff <= 0:
|
|
399
|
+
return float('inf')
|
|
400
|
+
rate = A_eff * (r - r_th) ** n
|
|
401
|
+
if rate <= 0:
|
|
402
|
+
return float('inf')
|
|
403
|
+
return (D_fail - D0) / rate
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def fatigue_life_const_amp(
|
|
407
|
+
mat: Material,
|
|
408
|
+
sigma_a_MPa: float,
|
|
409
|
+
sigma_y_tension_MPa: float,
|
|
410
|
+
A_ext: float,
|
|
411
|
+
mode: Literal['tensile', 'compression', 'shear'] = 'tensile',
|
|
412
|
+
D0: float = 0.0,
|
|
413
|
+
D_fail: float = 0.5,
|
|
414
|
+
C_class: float = C_CLASS_DEFAULT,
|
|
415
|
+
bcc_w110: float = DEFAULT_BCC_W110,
|
|
416
|
+
apply_C_class_hcp: bool = False,
|
|
417
|
+
) -> Dict[str, float | str]:
|
|
418
|
+
"""Fatigue life under constant amplitude for the chosen loading mode."""
|
|
419
|
+
|
|
420
|
+
preset = FATIGUE_CLASS_PRESET.get(mat.structure, FATIGUE_CLASS_PRESET['FCC'])
|
|
421
|
+
r_th = preset['r_th']
|
|
422
|
+
n = preset['n']
|
|
423
|
+
|
|
424
|
+
A_int = A_INT_DB.get(mat.name, 1.0)
|
|
425
|
+
A_eff = A_int * A_ext
|
|
426
|
+
|
|
427
|
+
y_mode, diag = yield_by_mode(
|
|
428
|
+
mat,
|
|
429
|
+
sigma_y_tension_MPa=sigma_y_tension_MPa,
|
|
430
|
+
mode=mode,
|
|
431
|
+
C_class=C_class,
|
|
432
|
+
bcc_w110=bcc_w110,
|
|
433
|
+
apply_C_class_hcp=apply_C_class_hcp,
|
|
434
|
+
)
|
|
435
|
+
|
|
436
|
+
# In shear mode, sigma_a_MPa is interpreted as τ_a [MPa]
|
|
437
|
+
r = sigma_a_MPa / y_mode if y_mode > 0 else float('inf')
|
|
438
|
+
|
|
439
|
+
N_fail = solve_N_fail(r, r_th, n, A_eff, D0=D0, D_fail=D_fail)
|
|
440
|
+
|
|
441
|
+
return {
|
|
442
|
+
'mode': mode,
|
|
443
|
+
'amp_input_MPa': sigma_a_MPa,
|
|
444
|
+
'yield_mode_MPa': y_mode,
|
|
445
|
+
'r': r,
|
|
446
|
+
'r_th': r_th,
|
|
447
|
+
'n': n,
|
|
448
|
+
'A_int': A_int,
|
|
449
|
+
'A_ext': A_ext,
|
|
450
|
+
'A_eff': A_eff,
|
|
451
|
+
'D0': D0,
|
|
452
|
+
'D_fail': D_fail,
|
|
453
|
+
'Lambda_fail': lambda_from_damage(D_fail),
|
|
454
|
+
**diag,
|
|
455
|
+
'N_fail': N_fail,
|
|
456
|
+
'log10_N_fail': (np.log10(N_fail) if np.isfinite(N_fail) and N_fail > 0 else float('inf')),
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
|
|
460
|
+
def generate_sn_curve(
|
|
461
|
+
mat: Material,
|
|
462
|
+
sigma_y_tension_MPa: float,
|
|
463
|
+
A_ext: float,
|
|
464
|
+
sigmas_MPa: np.ndarray,
|
|
465
|
+
mode: Literal['tensile', 'compression', 'shear'] = 'tensile',
|
|
466
|
+
D_fail: float = 0.5,
|
|
467
|
+
C_class: float = C_CLASS_DEFAULT,
|
|
468
|
+
bcc_w110: float = DEFAULT_BCC_W110,
|
|
469
|
+
apply_C_class_hcp: bool = False,
|
|
470
|
+
) -> np.ndarray:
|
|
471
|
+
Ns = []
|
|
472
|
+
for s in sigmas_MPa:
|
|
473
|
+
out = fatigue_life_const_amp(
|
|
474
|
+
mat,
|
|
475
|
+
sigma_a_MPa=float(s),
|
|
476
|
+
sigma_y_tension_MPa=sigma_y_tension_MPa,
|
|
477
|
+
A_ext=A_ext,
|
|
478
|
+
mode=mode,
|
|
479
|
+
D_fail=D_fail,
|
|
480
|
+
C_class=C_class,
|
|
481
|
+
bcc_w110=bcc_w110,
|
|
482
|
+
apply_C_class_hcp=apply_C_class_hcp,
|
|
483
|
+
)
|
|
484
|
+
Ns.append(out['N_fail'])
|
|
485
|
+
return np.array(Ns, dtype=float)
|
|
486
|
+
|
|
487
|
+
# ==============================================================================
|
|
488
|
+
# CLI
|
|
489
|
+
# ==============================================================================
|
|
490
|
+
|
|
491
|
+
def cmd_point(args: argparse.Namespace) -> None:
|
|
492
|
+
mat0 = MATERIALS[args.metal]
|
|
493
|
+
mat = replace(
|
|
494
|
+
mat0,
|
|
495
|
+
A_texture=float(args.A_texture),
|
|
496
|
+
T_twin=(float(args.T_twin) if args.T_twin is not None else mat0.T_twin),
|
|
497
|
+
R_comp=(float(args.R_comp) if args.R_comp is not None else mat0.R_comp),
|
|
498
|
+
c_a=float(args.c_a) if args.c_a is not None else mat0.c_a,
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
y = calc_sigma_y(
|
|
502
|
+
mat,
|
|
503
|
+
T_K=args.T_K,
|
|
504
|
+
c_wt_percent=args.c_wt,
|
|
505
|
+
k_ss=args.k_ss,
|
|
506
|
+
solute_type=args.solute_type,
|
|
507
|
+
eps=args.eps,
|
|
508
|
+
rho_0=args.rho_0,
|
|
509
|
+
r_ppt_nm=args.r_ppt_nm,
|
|
510
|
+
f_ppt=args.f_ppt,
|
|
511
|
+
gamma_apb=args.gamma_apb,
|
|
512
|
+
A_ppt=args.A_ppt,
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
# Fatigue (optional)
|
|
516
|
+
if args.sigma_a is not None:
|
|
517
|
+
out = fatigue_life_const_amp(
|
|
518
|
+
mat,
|
|
519
|
+
sigma_a_MPa=float(args.sigma_a),
|
|
520
|
+
sigma_y_tension_MPa=float(y['sigma_y']),
|
|
521
|
+
A_ext=float(args.A_ext),
|
|
522
|
+
mode=args.mode,
|
|
523
|
+
D_fail=args.D_fail,
|
|
524
|
+
C_class=args.C_class,
|
|
525
|
+
bcc_w110=args.bcc_w110,
|
|
526
|
+
apply_C_class_hcp=args.apply_C_class_hcp,
|
|
527
|
+
)
|
|
528
|
+
else:
|
|
529
|
+
out = None
|
|
530
|
+
|
|
531
|
+
print("=" * 88)
|
|
532
|
+
print(f"v6.9b point | metal={mat.name} ({mat.structure}) | mode={args.mode}")
|
|
533
|
+
print("=" * 88)
|
|
534
|
+
|
|
535
|
+
# Yield summary
|
|
536
|
+
y_mode, diag = yield_by_mode(
|
|
537
|
+
mat,
|
|
538
|
+
sigma_y_tension_MPa=float(y['sigma_y']),
|
|
539
|
+
mode=args.mode,
|
|
540
|
+
C_class=args.C_class,
|
|
541
|
+
bcc_w110=args.bcc_w110,
|
|
542
|
+
apply_C_class_hcp=args.apply_C_class_hcp,
|
|
543
|
+
)
|
|
544
|
+
|
|
545
|
+
print("[Yield v5.0]")
|
|
546
|
+
print(f" σ_base = {y['sigma_base']:.2f} MPa")
|
|
547
|
+
print(f" Δσ_ss = {y['delta_ss']:.2f} MPa")
|
|
548
|
+
print(f" Δσ_wh = {y['delta_wh']:.2f} MPa (rho_0={args.rho_0:.2e})")
|
|
549
|
+
print(f" Δσ_ppt = {y['delta_ppt']:.2f} MPa ({y['ppt_mechanism']})")
|
|
550
|
+
print(f" σ_y(t) = {y['sigma_y']:.2f} MPa")
|
|
551
|
+
print("[Class factors v4.1]")
|
|
552
|
+
print(f" C_class = {args.C_class:.4f} (apply to HCP: {args.apply_C_class_hcp})")
|
|
553
|
+
print(f" bcc_w110 = {args.bcc_w110:.3f}")
|
|
554
|
+
print(f" A_texture= {mat.A_texture:.3f}")
|
|
555
|
+
print(f" T_twin = {mat.T_twin:.3f}")
|
|
556
|
+
print(f" R_comp = {mat.R_comp:.3f} (σ_c/σ_t)")
|
|
557
|
+
print(f" τ/σ_pred = {diag['tau_over_sigma']:.4f}")
|
|
558
|
+
print(f" τ_y = {diag['tau_y']:.2f} MPa")
|
|
559
|
+
print(f" σ_y(c) = {diag['sigma_y_compression']:.2f} MPa")
|
|
560
|
+
print(f" Yield(mode) = {y_mode:.2f} MPa")
|
|
561
|
+
|
|
562
|
+
if out is None:
|
|
563
|
+
return
|
|
564
|
+
|
|
565
|
+
print("\n[Fatigue v6.8]")
|
|
566
|
+
unit = "MPa" if args.mode != 'shear' else "MPa (τ_a)"
|
|
567
|
+
print(f" amplitude input = {out['amp_input_MPa']:.2f} {unit}")
|
|
568
|
+
print(f" r = amp / yield(mode) = {out['r']:.5f}")
|
|
569
|
+
print(f" r_th={out['r_th']:.3f}, n={out['n']:.1f}")
|
|
570
|
+
print(f" A_int={out['A_int']:.3f}, A_ext={out['A_ext']:.3e} => A_eff={out['A_eff']:.3e}")
|
|
571
|
+
print(f" D_fail={out['D_fail']:.3f} (Λ_fail={out['Lambda_fail']:.3f})")
|
|
572
|
+
if np.isfinite(out['N_fail']):
|
|
573
|
+
print(f" N_fail = {out['N_fail']:.3e} cycles (log10={out['log10_N_fail']:.3f})")
|
|
574
|
+
else:
|
|
575
|
+
print(" N_fail = inf (fatigue limit region)")
|
|
576
|
+
|
|
577
|
+
|
|
578
|
+
def cmd_calibrate(args: argparse.Namespace) -> None:
|
|
579
|
+
"""Calibrate A_ext from one (σ_a, N_fail) point."""
|
|
580
|
+
mat0 = MATERIALS[args.metal]
|
|
581
|
+
mat = replace(
|
|
582
|
+
mat0,
|
|
583
|
+
A_texture=float(args.A_texture),
|
|
584
|
+
T_twin=(float(args.T_twin) if args.T_twin is not None else mat0.T_twin),
|
|
585
|
+
R_comp=(float(args.R_comp) if args.R_comp is not None else mat0.R_comp),
|
|
586
|
+
c_a=float(args.c_a) if args.c_a is not None else mat0.c_a,
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
y = calc_sigma_y(
|
|
590
|
+
mat,
|
|
591
|
+
T_K=args.T_K,
|
|
592
|
+
c_wt_percent=args.c_wt,
|
|
593
|
+
k_ss=args.k_ss,
|
|
594
|
+
solute_type=args.solute_type,
|
|
595
|
+
eps=args.eps,
|
|
596
|
+
rho_0=args.rho_0,
|
|
597
|
+
r_ppt_nm=args.r_ppt_nm,
|
|
598
|
+
f_ppt=args.f_ppt,
|
|
599
|
+
gamma_apb=args.gamma_apb,
|
|
600
|
+
A_ppt=args.A_ppt,
|
|
601
|
+
)
|
|
602
|
+
|
|
603
|
+
preset = FATIGUE_CLASS_PRESET.get(mat.structure, FATIGUE_CLASS_PRESET['FCC'])
|
|
604
|
+
r_th, n = preset['r_th'], preset['n']
|
|
605
|
+
|
|
606
|
+
y_mode, _ = yield_by_mode(
|
|
607
|
+
mat,
|
|
608
|
+
sigma_y_tension_MPa=float(y['sigma_y']),
|
|
609
|
+
mode=args.mode,
|
|
610
|
+
C_class=args.C_class,
|
|
611
|
+
bcc_w110=args.bcc_w110,
|
|
612
|
+
apply_C_class_hcp=args.apply_C_class_hcp,
|
|
613
|
+
)
|
|
614
|
+
|
|
615
|
+
r = args.sigma_a / y_mode
|
|
616
|
+
|
|
617
|
+
if r <= r_th:
|
|
618
|
+
raise SystemExit("Calibration point is below r_th (fatigue limit); choose a point with finite life.")
|
|
619
|
+
|
|
620
|
+
A_int = A_INT_DB.get(mat.name, 1.0)
|
|
621
|
+
D_fail = args.D_fail
|
|
622
|
+
D0 = 0.0
|
|
623
|
+
|
|
624
|
+
rate_needed = (D_fail - D0) / args.N_fail
|
|
625
|
+
A_eff = rate_needed / ((r - r_th) ** n)
|
|
626
|
+
A_ext = A_eff / A_int
|
|
627
|
+
|
|
628
|
+
print("=" * 88)
|
|
629
|
+
print("v6.9b calibrate A_ext")
|
|
630
|
+
print("=" * 88)
|
|
631
|
+
print(f"metal={mat.name} ({mat.structure}), mode={args.mode}")
|
|
632
|
+
print(f"σ_y(tension) = {y['sigma_y']:.3f} MPa")
|
|
633
|
+
print(f"yield(mode) = {y_mode:.3f} MPa")
|
|
634
|
+
print(f"amp = {args.sigma_a:.3f} MPa {'(τ_a)' if args.mode=='shear' else ''}")
|
|
635
|
+
print(f"r={r:.6f}, r_th={r_th:.3f}, n={n:.2f}")
|
|
636
|
+
print(f"D_fail={D_fail:.3f}, N_fail={args.N_fail:.3e}")
|
|
637
|
+
print(f"A_int={A_int:.3f} => A_ext={A_ext:.3e} (A_eff={A_eff:.3e})")
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def cmd_sn(args: argparse.Namespace) -> None:
|
|
641
|
+
mat0 = MATERIALS[args.metal]
|
|
642
|
+
mat = replace(
|
|
643
|
+
mat0,
|
|
644
|
+
A_texture=float(args.A_texture),
|
|
645
|
+
T_twin=(float(args.T_twin) if args.T_twin is not None else mat0.T_twin),
|
|
646
|
+
R_comp=(float(args.R_comp) if args.R_comp is not None else mat0.R_comp),
|
|
647
|
+
c_a=float(args.c_a) if args.c_a is not None else mat0.c_a,
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
y = calc_sigma_y(
|
|
651
|
+
mat,
|
|
652
|
+
T_K=args.T_K,
|
|
653
|
+
c_wt_percent=args.c_wt,
|
|
654
|
+
k_ss=args.k_ss,
|
|
655
|
+
solute_type=args.solute_type,
|
|
656
|
+
eps=args.eps,
|
|
657
|
+
rho_0=args.rho_0,
|
|
658
|
+
r_ppt_nm=args.r_ppt_nm,
|
|
659
|
+
f_ppt=args.f_ppt,
|
|
660
|
+
gamma_apb=args.gamma_apb,
|
|
661
|
+
A_ppt=args.A_ppt,
|
|
662
|
+
)
|
|
663
|
+
|
|
664
|
+
sigmas = np.linspace(args.sigma_min, args.sigma_max, args.num)
|
|
665
|
+
Ns = generate_sn_curve(
|
|
666
|
+
mat,
|
|
667
|
+
sigma_y_tension_MPa=float(y['sigma_y']),
|
|
668
|
+
A_ext=args.A_ext,
|
|
669
|
+
sigmas_MPa=sigmas,
|
|
670
|
+
mode=args.mode,
|
|
671
|
+
D_fail=args.D_fail,
|
|
672
|
+
C_class=args.C_class,
|
|
673
|
+
bcc_w110=args.bcc_w110,
|
|
674
|
+
apply_C_class_hcp=args.apply_C_class_hcp,
|
|
675
|
+
)
|
|
676
|
+
|
|
677
|
+
print("=" * 88)
|
|
678
|
+
print(f"v6.9b S-N | metal={mat.name} ({mat.structure}) | mode={args.mode}")
|
|
679
|
+
print("=" * 88)
|
|
680
|
+
print(f"σ_y(tension)={y['sigma_y']:.3f} MPa | A_ext={args.A_ext:.3e} | D_fail={args.D_fail:.3f}")
|
|
681
|
+
|
|
682
|
+
header_amp = 'sigma_a_MPa' if args.mode != 'shear' else 'tau_a_MPa'
|
|
683
|
+
print(f"{header_amp:>12} {'N_fail':>14} {'log10N':>10} {'r':>10} {'note':>10}")
|
|
684
|
+
for s, N in zip(sigmas, Ns):
|
|
685
|
+
y_mode, _ = yield_by_mode(
|
|
686
|
+
mat,
|
|
687
|
+
sigma_y_tension_MPa=float(y['sigma_y']),
|
|
688
|
+
mode=args.mode,
|
|
689
|
+
C_class=args.C_class,
|
|
690
|
+
bcc_w110=args.bcc_w110,
|
|
691
|
+
apply_C_class_hcp=args.apply_C_class_hcp,
|
|
692
|
+
)
|
|
693
|
+
r = s / y_mode
|
|
694
|
+
if np.isfinite(N):
|
|
695
|
+
print(f"{s:12.2f} {N:14.3e} {np.log10(N):10.3f} {r:10.4f} {'':>10}")
|
|
696
|
+
else:
|
|
697
|
+
print(f"{s:12.2f} {'inf':>14} {'inf':>10} {r:10.4f} {'limit':>10}")
|
|
698
|
+
|
|
699
|
+
|
|
700
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
701
|
+
p = argparse.ArgumentParser(description='v6.9b: v5.0 yield × v6.8 fatigue + τ/σ, twins, texture')
|
|
702
|
+
sub = p.add_subparsers(dest='cmd', required=True)
|
|
703
|
+
|
|
704
|
+
def add_common(sp: argparse.ArgumentParser):
|
|
705
|
+
sp.add_argument('--metal', required=True, choices=sorted(MATERIALS.keys()))
|
|
706
|
+
sp.add_argument('--T_K', type=float, default=300.0)
|
|
707
|
+
sp.add_argument('--c_wt', type=float, default=0.0, help='solute wt%% (e.g., 0.10 for 0.10 wt%%)')
|
|
708
|
+
sp.add_argument('--k_ss', type=float, default=0.0, help='solid-solution k [MPa/(wt%%)^n]')
|
|
709
|
+
sp.add_argument('--solute_type', choices=['interstitial', 'substitutional'], default=None)
|
|
710
|
+
sp.add_argument('--eps', type=float, default=0.0, help='monotonic strain for work hardening')
|
|
711
|
+
sp.add_argument('--rho_0', type=float, default=0.0, help='initial dislocation density [m^-2]')
|
|
712
|
+
sp.add_argument('--r_ppt_nm', type=float, default=0.0)
|
|
713
|
+
sp.add_argument('--f_ppt', type=float, default=0.0)
|
|
714
|
+
sp.add_argument('--gamma_apb', type=float, default=0.0)
|
|
715
|
+
sp.add_argument('--A_ppt', type=float, default=1.0)
|
|
716
|
+
|
|
717
|
+
# v4.1 class factors
|
|
718
|
+
sp.add_argument('--A_texture', type=float, default=1.0)
|
|
719
|
+
sp.add_argument('--T_twin', type=float, default=None, help='override T_twin')
|
|
720
|
+
sp.add_argument('--R_comp', type=float, default=None, help='override σ_c/σ_t')
|
|
721
|
+
sp.add_argument('--c_a', type=float, default=None, help='override HCP c/a')
|
|
722
|
+
sp.add_argument('--C_class', type=float, default=C_CLASS_DEFAULT)
|
|
723
|
+
sp.add_argument('--bcc_w110', type=float, default=DEFAULT_BCC_W110)
|
|
724
|
+
sp.add_argument('--apply_C_class_hcp', action='store_true')
|
|
725
|
+
|
|
726
|
+
def add_fatigue(sp: argparse.ArgumentParser):
|
|
727
|
+
sp.add_argument('--mode', choices=['tensile', 'compression', 'shear'], default='tensile')
|
|
728
|
+
sp.add_argument('--A_ext', type=float, default=2.46e-4, help='external factor (1-point calibration)')
|
|
729
|
+
sp.add_argument('--D_fail', type=float, default=0.5)
|
|
730
|
+
|
|
731
|
+
sp_point = sub.add_parser('point', help='compute yield (+ optional fatigue life)')
|
|
732
|
+
add_common(sp_point)
|
|
733
|
+
add_fatigue(sp_point)
|
|
734
|
+
sp_point.add_argument('--sigma_a', type=float, default=None, help='amplitude [MPa]. If mode=shear, this is τ_a.')
|
|
735
|
+
sp_point.set_defaults(func=cmd_point)
|
|
736
|
+
|
|
737
|
+
sp_cal = sub.add_parser('calibrate', help='calibrate A_ext from one fatigue point')
|
|
738
|
+
add_common(sp_cal)
|
|
739
|
+
add_fatigue(sp_cal)
|
|
740
|
+
sp_cal.add_argument('--sigma_a', type=float, required=True, help='amplitude [MPa]. If mode=shear, τ_a.')
|
|
741
|
+
sp_cal.add_argument('--N_fail', type=float, required=True)
|
|
742
|
+
sp_cal.set_defaults(func=cmd_calibrate)
|
|
743
|
+
|
|
744
|
+
sp_sn = sub.add_parser('sn', help='generate S-N table')
|
|
745
|
+
add_common(sp_sn)
|
|
746
|
+
add_fatigue(sp_sn)
|
|
747
|
+
sp_sn.add_argument('--sigma_min', type=float, required=True)
|
|
748
|
+
sp_sn.add_argument('--sigma_max', type=float, required=True)
|
|
749
|
+
sp_sn.add_argument('--num', type=int, default=25)
|
|
750
|
+
sp_sn.set_defaults(func=cmd_sn)
|
|
751
|
+
|
|
752
|
+
return p
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
def main() -> None:
|
|
756
|
+
parser = build_parser()
|
|
757
|
+
args = parser.parse_args()
|
|
758
|
+
args.func(args)
|
|
759
|
+
|
|
760
|
+
|
|
761
|
+
if __name__ == '__main__':
|
|
762
|
+
main()
|