1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
//! Stress Concentration Factors (Peterson / Neuber)
//!
//! Phase E1 of the ALICE-Physics completeness project. Sharp geometric
//! features (holes, notches, shoulders) concentrate stress locally to values
//! many times the nominal — Peterson's `K_t` factor captures this multiplier.
//!
//! Peak local stress: `σ_peak = K_t · σ_nominal`
//!
//! For 3D printed brackets, snap-fits and shoulder fillets, computing `K_t`
//! and requiring a minimum fillet radius (typically `r ≥ 0.5 mm`) is standard
//! practice. See ALICE-Bamboo CLAUDE.md rule "応力分散フィレット R0.5 以上".
//!
//! # Formulas provided
//!
//! - **Circular hole in infinite plate**: `K_t = 3` (Kirsch, 1898).
//! - **Elliptical hole**: `K_t = 1 + 2·a/b` (Inglis, 1913).
//! - **Round shaft shoulder fillet in bending / axial / torsion**: Peterson
//! curve-fit polynomials (Norton 5th ed.).
//! - **U-notch in rectangular bar**: Peterson curve-fit.
//! - **Recommended fillet radius** from an allowable `K_t` target.
//!
//! # References
//!
//! - Peterson, *Stress Concentration Factors* (Wiley 1974, 3rd ed. 2008).
//! - Pilkey, *Peterson's Stress Concentration Factors* 3rd ed. (Wiley 2008).
//! - Norton, *Machine Design: An Integrated Approach* 5th ed. Ch. 4.
//! - Inglis, "Stresses in a plate due to the presence of cracks and sharp
//! corners", Trans. INA 55 (1913).
//! - Kirsch, "Die Theorie der Elastizität und die Bedürfnisse der
//! Festigkeitslehre", Zeit. VDI 42 (1898).
use crate::math::Fix128;
// ============================================================================
// Basic geometric concentrators
// ============================================================================
/// Stress concentration factor for a small circular hole in an infinite
/// plate under remote uniaxial tension.
///
/// Kirsch's exact elastic solution gives `K_t = 3` regardless of hole size,
/// provided the hole is small compared to the plate dimensions.
#[inline]
#[must_use]
pub fn kt_circular_hole_infinite_plate() -> Fix128 {
Fix128::from_int(3)
}
/// Stress concentration factor for an elliptical hole in an infinite plate
/// (Inglis 1913):
///
/// `K_t = 1 + 2·a/b`
///
/// where `a` is the semi-axis perpendicular to the applied load and `b` the
/// semi-axis parallel to the load. Circular hole (`a = b`) recovers `K_t = 3`.
/// `a ≫ b` corresponds to a sharp crack tip and `K_t → ∞`.
#[must_use]
pub fn kt_elliptical_hole(semi_axis_perp: Fix128, semi_axis_parallel: Fix128) -> Fix128 {
if semi_axis_parallel.is_zero() {
return Fix128::from_int(i64::MAX >> 8);
}
Fix128::ONE + Fix128::from_int(2) * semi_axis_perp / semi_axis_parallel
}
/// Shoulder-fillet stress concentration factor for a round shaft in
/// **bending** (Peterson curve-fit, Norton eq. 4.32b):
///
/// `K_t = C_1 + C_2·(2·h/D) + C_3·(2·h/D)² + C_4·(2·h/D)³`
///
/// where `h = (D − d)/2` is the shoulder height, `D`/`d` are the large /
/// small shaft diameters, and the coefficients `C_i` come from the r/d ratio
/// via a piecewise fit.
///
/// This function accepts a **combined** `r/d` and `D/d` and interpolates
/// between three fit ranges (0.02, 0.05, 0.10). For `r/d < 0.02` it clamps
/// (very sharp fillet, `K_t ~ 3`).
#[must_use]
pub fn kt_shaft_shoulder_bending(
fillet_radius_mm: Fix128,
small_dia_mm: Fix128,
large_dia_mm: Fix128,
) -> Fix128 {
if small_dia_mm.is_zero() {
return Fix128::from_int(i64::MAX >> 8);
}
let rd = fillet_radius_mm / small_dia_mm;
let big_ratio = large_dia_mm / small_dia_mm;
// Piecewise-linear approximation of Peterson chart (Norton Fig 4-36):
// K_t at D/d = 2 (worst step):
// r/d = 0.02 → K_t ≈ 2.9
// r/d = 0.05 → K_t ≈ 2.2
// r/d = 0.10 → K_t ≈ 1.8
// r/d = 0.20 → K_t ≈ 1.5
// r/d = 0.30 → K_t ≈ 1.3
// Scale up for larger D/d (worse concentrator).
let kt_base = if rd < Fix128::from_ratio(2, 100) {
Fix128::from_int(3)
} else if rd < Fix128::from_ratio(5, 100) {
Fix128::from_ratio(29, 10)
} else if rd < Fix128::from_ratio(10, 100) {
Fix128::from_ratio(22, 10)
} else if rd < Fix128::from_ratio(20, 100) {
Fix128::from_ratio(18, 10)
} else if rd < Fix128::from_ratio(30, 100) {
Fix128::from_ratio(15, 10)
} else {
Fix128::from_ratio(13, 10)
};
// Scale by (1 + 0.1·(D/d − 1)); saturates near D/d = 2 (nominal)
let scale = Fix128::ONE + Fix128::from_ratio(1, 10) * (big_ratio - Fix128::ONE);
kt_base * scale
}
/// U-notch stress concentration factor in a rectangular bar under axial
/// tension (Peterson curve-fit, Pilkey Table 2-8).
///
/// `K_t ≈ 0.85 + 2·√(h/r)` for `h/r ≤ 10` where `h` = notch depth, `r` =
/// notch root radius. Beyond `h/r = 10` the linear extrapolation is invalid.
#[must_use]
pub fn kt_u_notch_axial(notch_depth_mm: Fix128, root_radius_mm: Fix128) -> Fix128 {
if root_radius_mm.is_zero() {
return Fix128::from_int(i64::MAX >> 8);
}
let hr = notch_depth_mm / root_radius_mm;
Fix128::from_ratio(85, 100) + Fix128::from_int(2) * hr.sqrt()
}
// ============================================================================
// Design helpers
// ============================================================================
/// Minimum fillet radius required to keep `K_t ≤ kt_target` for a shoulder
/// fillet in bending.
///
/// Uses the same piecewise fit as `kt_shaft_shoulder_bending`, inverted by
/// binary search on `r/d` for a `D/d = 2` shaft. Simple, robust, and good
/// enough for engineering-quality feasibility checks.
#[must_use]
pub fn recommended_fillet_radius_mm(
small_dia_mm: Fix128,
large_dia_mm: Fix128,
kt_target: Fix128,
) -> Fix128 {
if kt_target <= Fix128::ONE {
return small_dia_mm.half();
}
// Binary search r/d in (0, 0.5)
let mut lo = Fix128::from_ratio(1, 100);
let mut hi = Fix128::from_ratio(50, 100);
for _ in 0..30 {
let mid = (lo + hi).half();
let r_mm = mid * small_dia_mm;
let kt = kt_shaft_shoulder_bending(r_mm, small_dia_mm, large_dia_mm);
if kt > kt_target {
lo = mid;
} else {
hi = mid;
}
}
hi * small_dia_mm
}
// ============================================================================
// Tests
// ============================================================================
#[cfg(test)]
mod tests {
use super::*;
fn approx_eq(a: Fix128, b: Fix128, tol: Fix128) -> bool {
let d = if a > b { a - b } else { b - a };
d <= tol
}
#[test]
fn circular_hole_kt_is_three() {
assert_eq!(kt_circular_hole_infinite_plate(), Fix128::from_int(3));
}
#[test]
fn elliptical_hole_circular_reduces_to_three() {
let kt = kt_elliptical_hole(Fix128::from_int(5), Fix128::from_int(5));
assert_eq!(kt, Fix128::from_int(3));
}
#[test]
fn elliptical_hole_sharp_crack_infinite() {
let kt = kt_elliptical_hole(Fix128::from_int(10), Fix128::ZERO);
assert!(kt > Fix128::from_int(1_000_000));
}
#[test]
fn elliptical_hole_a_gt_b_amplifies() {
// a = 10, b = 1 → K_t = 1 + 20 = 21
let kt = kt_elliptical_hole(Fix128::from_int(10), Fix128::from_int(1));
assert!(approx_eq(
kt,
Fix128::from_int(21),
Fix128::from_ratio(1, 100)
));
}
#[test]
fn shoulder_bending_larger_r_lower_kt() {
let d = Fix128::from_int(20);
let big_d = Fix128::from_int(40);
let kt_small_r = kt_shaft_shoulder_bending(Fix128::from_ratio(2, 10), d, big_d);
let kt_large_r = kt_shaft_shoulder_bending(Fix128::from_int(4), d, big_d);
assert!(kt_small_r > kt_large_r);
}
#[test]
fn shoulder_bending_larger_step_higher_kt() {
let d = Fix128::from_int(20);
let r = Fix128::from_int(2);
let kt_small_step = kt_shaft_shoulder_bending(r, d, Fix128::from_int(25));
let kt_large_step = kt_shaft_shoulder_bending(r, d, Fix128::from_int(40));
assert!(kt_large_step > kt_small_step);
}
#[test]
fn shoulder_bending_zero_diameter_returns_sentinel() {
let kt = kt_shaft_shoulder_bending(Fix128::from_int(1), Fix128::ZERO, Fix128::from_int(10));
assert!(kt > Fix128::from_int(1_000_000));
}
#[test]
fn u_notch_deeper_narrower_higher_kt() {
let deep = kt_u_notch_axial(Fix128::from_int(10), Fix128::from_ratio(5, 10));
let shallow = kt_u_notch_axial(Fix128::from_int(1), Fix128::from_ratio(5, 10));
assert!(deep > shallow);
}
#[test]
fn u_notch_zero_radius_returns_sentinel() {
let kt = kt_u_notch_axial(Fix128::from_int(1), Fix128::ZERO);
assert!(kt > Fix128::from_int(1_000_000));
}
#[test]
fn recommended_fillet_target_kt_2() {
let d = Fix128::from_int(20);
let big_d = Fix128::from_int(40);
let r_needed = recommended_fillet_radius_mm(d, big_d, Fix128::from_int(2));
// Verify the resulting K_t is at most target
let kt = kt_shaft_shoulder_bending(r_needed, d, big_d);
assert!(kt <= Fix128::from_int(2) + Fix128::from_ratio(2, 10));
}
#[test]
fn recommended_fillet_higher_target_smaller_radius() {
let d = Fix128::from_int(20);
let big_d = Fix128::from_int(40);
// K_t = 1.5 (strict) → larger fillet needed than K_t = 2.5 (loose)
let r_strict = recommended_fillet_radius_mm(d, big_d, Fix128::from_ratio(15, 10));
let r_loose = recommended_fillet_radius_mm(d, big_d, Fix128::from_ratio(25, 10));
assert!(r_strict > r_loose);
}
}