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,334 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
FatigueData-AM2022 Upstash Redis API
|
|
4
|
+
=====================================
|
|
5
|
+
|
|
6
|
+
δ理論検証用 疲労データベースAPI
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from fatigue_redis_api import FatigueDB
|
|
10
|
+
|
|
11
|
+
db = FatigueDB()
|
|
12
|
+
|
|
13
|
+
# 材料一覧
|
|
14
|
+
materials = db.list_materials()
|
|
15
|
+
|
|
16
|
+
# S-Nデータ取得
|
|
17
|
+
ti64_sn = db.get_sn('Ti-6Al-4V')
|
|
18
|
+
|
|
19
|
+
# R=-1 のデータのみ
|
|
20
|
+
ti64_r1 = db.get_sn('Ti-6Al-4V', R=-1.0)
|
|
21
|
+
|
|
22
|
+
# σ_y付きデータのみ
|
|
23
|
+
ti64_sy = db.get_sn('Ti-6Al-4V', with_sigma_y=True)
|
|
24
|
+
|
|
25
|
+
# da/dN データ
|
|
26
|
+
dadn = db.get_dadn('Ti-6Al-4V')
|
|
27
|
+
|
|
28
|
+
Author: 環 & ご主人さま
|
|
29
|
+
Date: 2026-02-02
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
import json
|
|
33
|
+
from typing import Dict, List, Optional, Any
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
from upstash_redis import Redis
|
|
38
|
+
except ImportError:
|
|
39
|
+
raise ImportError("pip install upstash-redis --break-system-packages")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# =============================================================================
|
|
43
|
+
# Configuration
|
|
44
|
+
# =============================================================================
|
|
45
|
+
UPSTASH_URL = "https://casual-crayfish-21486.upstash.io"
|
|
46
|
+
UPSTASH_TOKEN = "AVPuAAIncDFiNWEyN2ExYmEzZGU0N2I5ODlkNmUxMjRkM2UzNWMwY3AxMjE0ODY"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# =============================================================================
|
|
50
|
+
# Data Classes
|
|
51
|
+
# =============================================================================
|
|
52
|
+
@dataclass
|
|
53
|
+
class SNPoint:
|
|
54
|
+
"""S-N データポイント"""
|
|
55
|
+
N: float # 破断サイクル数
|
|
56
|
+
S: float # 応力振幅 [MPa]
|
|
57
|
+
runout: bool # ランアウト(未破断)フラグ
|
|
58
|
+
R: Optional[float] # 応力比
|
|
59
|
+
sigma_y: Optional[float] # 降伏応力 [MPa]
|
|
60
|
+
sigma_uts: Optional[float] # 引張強さ [MPa]
|
|
61
|
+
doi: Optional[str] # 出典DOI
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ENPoint:
|
|
66
|
+
"""ε-N データポイント"""
|
|
67
|
+
N: float # 破断サイクル数
|
|
68
|
+
e: float # ひずみ振幅
|
|
69
|
+
runout: bool # ランアウト
|
|
70
|
+
R: Optional[float]
|
|
71
|
+
sigma_y: Optional[float]
|
|
72
|
+
E: Optional[float] # ヤング率 [GPa]
|
|
73
|
+
doi: Optional[str]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dataclass
|
|
77
|
+
class DaDNPoint:
|
|
78
|
+
"""da/dN-ΔK データポイント"""
|
|
79
|
+
dK: float # 応力拡大係数範囲 [MPa√m]
|
|
80
|
+
dadn: float # き裂進展速度 [m/cycle]
|
|
81
|
+
R: Optional[float]
|
|
82
|
+
doi: Optional[str]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# =============================================================================
|
|
86
|
+
# Main API Class
|
|
87
|
+
# =============================================================================
|
|
88
|
+
class FatigueDB:
|
|
89
|
+
"""FatigueData-AM2022 Upstash Redis API"""
|
|
90
|
+
|
|
91
|
+
def __init__(self, url: str = UPSTASH_URL, token: str = UPSTASH_TOKEN):
|
|
92
|
+
"""Initialize connection to Upstash Redis"""
|
|
93
|
+
self.redis = Redis(url=url, token=token)
|
|
94
|
+
self._meta = None
|
|
95
|
+
self._materials = None
|
|
96
|
+
|
|
97
|
+
# -------------------------------------------------------------------------
|
|
98
|
+
# Meta & Materials
|
|
99
|
+
# -------------------------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
def get_meta(self) -> Dict[str, Any]:
|
|
102
|
+
"""データベースメタ情報"""
|
|
103
|
+
if self._meta is None:
|
|
104
|
+
self._meta = json.loads(self.redis.get('fatigue:meta'))
|
|
105
|
+
return self._meta
|
|
106
|
+
|
|
107
|
+
def list_materials(self, sort_by: str = 'sn_count') -> List[Dict]:
|
|
108
|
+
"""材料一覧を取得
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
sort_by: ソートキー ('sn_count', 'en_count', 'dadn_count', 'name')
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
材料リスト [{name, sn_count, en_count, dadn_count, sigma_y_min, sigma_y_max}, ...]
|
|
115
|
+
"""
|
|
116
|
+
if self._materials is None:
|
|
117
|
+
self._materials = json.loads(self.redis.get('fatigue:materials'))
|
|
118
|
+
|
|
119
|
+
result = []
|
|
120
|
+
for name, info in self._materials.items():
|
|
121
|
+
result.append({'name': name, **info})
|
|
122
|
+
|
|
123
|
+
if sort_by == 'name':
|
|
124
|
+
result.sort(key=lambda x: x['name'])
|
|
125
|
+
elif sort_by in ['sn_count', 'en_count', 'dadn_count']:
|
|
126
|
+
result.sort(key=lambda x: x.get(sort_by, 0), reverse=True)
|
|
127
|
+
|
|
128
|
+
return result
|
|
129
|
+
|
|
130
|
+
def get_material_info(self, material: str) -> Optional[Dict]:
|
|
131
|
+
"""特定材料の情報"""
|
|
132
|
+
if self._materials is None:
|
|
133
|
+
self.list_materials()
|
|
134
|
+
return self._materials.get(material)
|
|
135
|
+
|
|
136
|
+
# -------------------------------------------------------------------------
|
|
137
|
+
# S-N Data
|
|
138
|
+
# -------------------------------------------------------------------------
|
|
139
|
+
|
|
140
|
+
def get_sn(
|
|
141
|
+
self,
|
|
142
|
+
material: str,
|
|
143
|
+
R: Optional[float] = None,
|
|
144
|
+
with_sigma_y: bool = False,
|
|
145
|
+
as_dataclass: bool = False
|
|
146
|
+
) -> List[Dict | SNPoint]:
|
|
147
|
+
"""S-Nデータを取得
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
material: 材料名 (e.g., 'Ti-6Al-4V', '316L')
|
|
151
|
+
R: 応力比でフィルタ (e.g., -1.0, 0.1)
|
|
152
|
+
with_sigma_y: σ_yが存在するデータのみ
|
|
153
|
+
as_dataclass: SNPointクラスで返す
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
S-Nデータリスト
|
|
157
|
+
"""
|
|
158
|
+
key = f'fatigue:sn:{material}'
|
|
159
|
+
raw = self.redis.get(key)
|
|
160
|
+
if raw is None:
|
|
161
|
+
return []
|
|
162
|
+
|
|
163
|
+
data = json.loads(raw)
|
|
164
|
+
|
|
165
|
+
# フィルタ
|
|
166
|
+
if R is not None:
|
|
167
|
+
data = [d for d in data if d.get('R') == R]
|
|
168
|
+
if with_sigma_y:
|
|
169
|
+
data = [d for d in data if d.get('sigma_y') is not None]
|
|
170
|
+
|
|
171
|
+
if as_dataclass:
|
|
172
|
+
return [SNPoint(
|
|
173
|
+
N=d['N'], S=d['S'], runout=bool(d.get('runout', 0)),
|
|
174
|
+
R=d.get('R'), sigma_y=d.get('sigma_y'),
|
|
175
|
+
sigma_uts=d.get('sigma_uts'), doi=d.get('doi')
|
|
176
|
+
) for d in data]
|
|
177
|
+
|
|
178
|
+
return data
|
|
179
|
+
|
|
180
|
+
def get_sn_for_delta(
|
|
181
|
+
self,
|
|
182
|
+
material: str,
|
|
183
|
+
R: float = -1.0
|
|
184
|
+
) -> List[Dict]:
|
|
185
|
+
"""δ理論検証用にr = σ_a/σ_y を計算済みのデータを取得
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
material: 材料名
|
|
189
|
+
R: 応力比 (default: -1.0 for fully reversed)
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
[{N, S, sigma_y, r, runout}, ...]
|
|
193
|
+
"""
|
|
194
|
+
data = self.get_sn(material, R=R, with_sigma_y=True)
|
|
195
|
+
|
|
196
|
+
result = []
|
|
197
|
+
for d in data:
|
|
198
|
+
sigma_y = d['sigma_y']
|
|
199
|
+
# R=-1 の場合、S = σ_max = 2 * σ_a なので σ_a = S/2... ではない
|
|
200
|
+
# 実際は S が応力振幅として記録されていることが多い
|
|
201
|
+
# データセットの定義を確認: S は stress amplitude or max stress?
|
|
202
|
+
# AM2022では Sは応力振幅(amplitude)として扱う
|
|
203
|
+
sigma_a = d['S'] # stress amplitude [MPa]
|
|
204
|
+
r = sigma_a / sigma_y
|
|
205
|
+
|
|
206
|
+
result.append({
|
|
207
|
+
'N': d['N'],
|
|
208
|
+
'S': sigma_a,
|
|
209
|
+
'sigma_y': sigma_y,
|
|
210
|
+
'r': r,
|
|
211
|
+
'runout': d.get('runout', 0),
|
|
212
|
+
'doi': d.get('doi')
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
return result
|
|
216
|
+
|
|
217
|
+
# -------------------------------------------------------------------------
|
|
218
|
+
# ε-N Data
|
|
219
|
+
# -------------------------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def get_en(
|
|
222
|
+
self,
|
|
223
|
+
material: str,
|
|
224
|
+
R: Optional[float] = None,
|
|
225
|
+
as_dataclass: bool = False
|
|
226
|
+
) -> List[Dict | ENPoint]:
|
|
227
|
+
"""ε-Nデータを取得"""
|
|
228
|
+
key = f'fatigue:en:{material}'
|
|
229
|
+
raw = self.redis.get(key)
|
|
230
|
+
if raw is None:
|
|
231
|
+
return []
|
|
232
|
+
|
|
233
|
+
data = json.loads(raw)
|
|
234
|
+
|
|
235
|
+
if R is not None:
|
|
236
|
+
data = [d for d in data if d.get('R') == R]
|
|
237
|
+
|
|
238
|
+
if as_dataclass:
|
|
239
|
+
return [ENPoint(
|
|
240
|
+
N=d['N'], e=d['e'], runout=bool(d.get('runout', 0)),
|
|
241
|
+
R=d.get('R'), sigma_y=d.get('sigma_y'),
|
|
242
|
+
E=d.get('E'), doi=d.get('doi')
|
|
243
|
+
) for d in data]
|
|
244
|
+
|
|
245
|
+
return data
|
|
246
|
+
|
|
247
|
+
# -------------------------------------------------------------------------
|
|
248
|
+
# da/dN-ΔK Data
|
|
249
|
+
# -------------------------------------------------------------------------
|
|
250
|
+
|
|
251
|
+
def get_dadn(
|
|
252
|
+
self,
|
|
253
|
+
material: str,
|
|
254
|
+
R: Optional[float] = None,
|
|
255
|
+
as_dataclass: bool = False
|
|
256
|
+
) -> List[Dict | DaDNPoint]:
|
|
257
|
+
"""da/dN-ΔKデータを取得(チャンク対応)"""
|
|
258
|
+
# チャンクがあるか確認
|
|
259
|
+
chunks_key = f'fatigue:dadn:{material}:chunks'
|
|
260
|
+
n_chunks = self.redis.get(chunks_key)
|
|
261
|
+
|
|
262
|
+
if n_chunks:
|
|
263
|
+
# チャンクから読み込み
|
|
264
|
+
data = []
|
|
265
|
+
for i in range(int(n_chunks)):
|
|
266
|
+
chunk = json.loads(self.redis.get(f'fatigue:dadn:{material}:chunk{i}'))
|
|
267
|
+
data.extend(chunk)
|
|
268
|
+
else:
|
|
269
|
+
# 単一キー
|
|
270
|
+
key = f'fatigue:dadn:{material}'
|
|
271
|
+
raw = self.redis.get(key)
|
|
272
|
+
if raw is None:
|
|
273
|
+
return []
|
|
274
|
+
data = json.loads(raw)
|
|
275
|
+
|
|
276
|
+
if R is not None:
|
|
277
|
+
data = [d for d in data if d.get('R') == R]
|
|
278
|
+
|
|
279
|
+
if as_dataclass:
|
|
280
|
+
return [DaDNPoint(
|
|
281
|
+
dK=d['dK'], dadn=d['dadn'],
|
|
282
|
+
R=d.get('R'), doi=d.get('doi')
|
|
283
|
+
) for d in data]
|
|
284
|
+
|
|
285
|
+
return data
|
|
286
|
+
|
|
287
|
+
# -------------------------------------------------------------------------
|
|
288
|
+
# Utilities
|
|
289
|
+
# -------------------------------------------------------------------------
|
|
290
|
+
|
|
291
|
+
def search_materials(self, query: str) -> List[str]:
|
|
292
|
+
"""材料名を検索"""
|
|
293
|
+
if self._materials is None:
|
|
294
|
+
self.list_materials()
|
|
295
|
+
|
|
296
|
+
query_lower = query.lower()
|
|
297
|
+
return [name for name in self._materials.keys()
|
|
298
|
+
if query_lower in name.lower()]
|
|
299
|
+
|
|
300
|
+
def summary(self) -> str:
|
|
301
|
+
"""データベースサマリー"""
|
|
302
|
+
meta = self.get_meta()
|
|
303
|
+
return f"""
|
|
304
|
+
FatigueData-AM2022 @ Upstash Redis
|
|
305
|
+
===================================
|
|
306
|
+
Source: {meta['source']}
|
|
307
|
+
DOI: {meta['doi']}
|
|
308
|
+
License: {meta['license']}
|
|
309
|
+
|
|
310
|
+
Materials: {meta['total_materials']}
|
|
311
|
+
S-N points: {meta['total_sn_points']:,}
|
|
312
|
+
ε-N points: {meta['total_en_points']:,}
|
|
313
|
+
da/dN points: {meta['total_dadn_points']:,}
|
|
314
|
+
TOTAL: {meta['total_sn_points'] + meta['total_en_points'] + meta['total_dadn_points']:,}
|
|
315
|
+
|
|
316
|
+
Updated: {meta['updated']}
|
|
317
|
+
"""
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
# =============================================================================
|
|
321
|
+
# CLI
|
|
322
|
+
# =============================================================================
|
|
323
|
+
if __name__ == '__main__':
|
|
324
|
+
db = FatigueDB()
|
|
325
|
+
print(db.summary())
|
|
326
|
+
|
|
327
|
+
print("\n📦 Top 5 materials by S-N count:")
|
|
328
|
+
for mat in db.list_materials()[:5]:
|
|
329
|
+
print(f" {mat['name']}: {mat['sn_count']} S-N, σ_y={mat.get('sigma_y_min')}-{mat.get('sigma_y_max')} MPa")
|
|
330
|
+
|
|
331
|
+
print("\n🔍 Ti-6Al-4V S-N (R=-1, with σ_y):")
|
|
332
|
+
ti64 = db.get_sn_for_delta('Ti-6Al-4V', R=-1.0)
|
|
333
|
+
print(f" {len(ti64)} points")
|
|
334
|
+
print(f" r range: {min(d['r'] for d in ti64):.4f} - {max(d['r'] for d in ti64):.4f}")
|