Skip to main content

squid_n_core/
rc_capacity.rs

1//! RC 矩形断面の簡易終局耐力算定(部材ランク判定・プッシュオーバーせん断降伏判定用)。
2//!
3//! squid-n-skeleton のファイバ解析(`build_rc_member_skeleton`)は Mu を精算できるが、
4//! 保有水平耐力の部材ランク自動判定(RC 部材の脆性破壊判定 Qsu/Qmu)
5//! は毎フレーム実行されるため重すぎる。また `squid_n_solver::nonlinear::pushover` のせん断降伏判定
6//! (`compute_shear_yield_qy`)も同様に軽量な閉形式解を必要とする。本モジュールは
7//! 閉形式の簡易式で Mu・Qsu・Qmu を算定し、両者の入力とする。係数は靭性指針・
8//! 技術基準解説書等の略算式に基づく代表値であり、全て要・原典照合
9//! (dev_docs/specs/原典照合リスト.md)。
10//!
11//! squid-n-solver(Layer 4)が squid-n-design-jp(Layer 5)に依存できない
12//! (循環依存になる)ため、本体は Layer 0 の squid-n-core に置き、
13//! `squid_n_design_jp::secondary::rc_capacity` は本モジュールの再エクスポートとする
14//! (既存呼び出し
15//! `squid_n_design_jp::secondary::rc_capacity::{rc_qsu_simple, RcCapacityInput}` 等は
16//! 無修正で動作する)。
17
18/// RC 矩形断面の簡易終局耐力算定用の入力一式。
19///
20/// `Clone, Copy` はプッシュオーバー解析(`squid_n_solver::nonlinear::pushover`)が σ0 を
21/// 除く入力一式を保持し、各ステップで σ0 のみ差し替えて `rc_qsu_simple` を
22/// 呼び直す用途(`DirThreshold::RcArakawa`)のために付与する。全フィールドが
23/// f64 のみのため、値のコピーは軽量。
24#[derive(Clone, Copy)]
25pub struct RcCapacityInput {
26    /// 断面幅 b \[mm\]
27    pub b: f64,
28    /// 断面せい D \[mm\]
29    pub d: f64,
30    /// 引張側主筋の総断面積 at \[mm²\](片側)
31    pub at: f64,
32    /// 有効せい d_e \[mm\](= D − dt。dt は [`crate::rc_rebar_geom::tension_dt`] と同規約)
33    pub d_eff: f64,
34    /// 主筋降伏強度 σy \[N/mm²\]
35    pub sigma_y: f64,
36    /// コンクリート強度 Fc \[N/mm²\]
37    pub fc: f64,
38    /// せん断補強筋比 pw(= aw・組数/(b・ピッチ))
39    pub pw: f64,
40    /// せん断補強筋降伏強度 σwy \[N/mm²\]
41    pub sigma_wy: f64,
42    /// 内法スパン h0 \[mm\](反曲点中央を仮定し Qmu = 2Mu/h0)
43    pub clear_span: f64,
44    /// 軸方向圧縮応力度 σ0 \[N/mm²\](既定 0)。`rc_qsu_simple` の軸力項
45    /// `0.1・σ0・b・j` に用いる。荒川式の適用範囲である 0〜0.4Fc に
46    /// `rc_qsu_simple` 内でクランプされる(要・原典照合)。
47    pub sigma_0: f64,
48}
49
50/// 曲げ終局モーメント Mu = 0.9・at・σy・d(引張鉄筋降伏型の略算式、d = 有効せい)。
51///
52/// 2007年版建築物の構造関係技術基準解説書 P.623 の略算式。係数 0.9 が応力中心間
53/// 距離のせい比(j/d 相当)を既に織り込んでいるため、d には有効せい d_e を
54/// そのまま用いる(さらに j = 7d/8 を乗じていた従来実装は Mu を 12.5%
55/// 過小評価する誤りだった)。
56///
57/// 不正入力(at, d_eff, σy のいずれかが 0 以下)は 0.0 を返す(呼び出し側の
58/// 部材ランク判定は qmu<=0 を判定不能=最不利側として扱う)。
59pub fn rc_mu_simple(inp: &RcCapacityInput) -> f64 {
60    if inp.at <= 0.0 || inp.d_eff <= 0.0 || inp.sigma_y <= 0.0 {
61        return 0.0;
62    }
63    0.9 * inp.at * inp.sigma_y * inp.d_eff
64}
65
66/// 曲げ終局時せん断力 Qmu = 2・Mu / h0(両端曲げ降伏・反曲点中央を仮定)。
67///
68/// `clear_span`(h0)が 0 以下の場合は 0.0 を返す。
69pub fn rc_qmu_simple(inp: &RcCapacityInput) -> f64 {
70    if inp.clear_span <= 0.0 {
71        return 0.0;
72    }
73    2.0 * rc_mu_simple(inp) / inp.clear_span
74}
75
76/// RC 柱の曲げ終局モーメント Mu \[N·mm\](軸力を考慮した略算式。
77/// 2007年版建築物の構造関係技術基準解説書 付録1-3 の閉形式、要・原典照合)。
78///
79/// ```text
80/// Nmax = b・D・Fc + ag・σy
81/// Nmin = −ag・σy
82/// N > 0.4・b・D・Fc:
83///   Mu = {0.8・at・σy・D + 0.12・b・D²・Fc}・(Nmax − N)/(Nmax − 0.4・b・D・Fc)
84/// 0 ≤ N ≤ 0.4・b・D・Fc:
85///   Mu = 0.8・at・σy・D + 0.5・N・D・(1 − N/(b・D・Fc))
86/// Nmin ≤ N < 0:
87///   Mu = 0.8・at・σy・D + 0.4・N・D
88/// ```
89///
90/// - `ag`: 全主筋断面積 \[mm²\]、`n_axial`: 設計軸力 \[N\](**圧縮を正**)。
91/// - `N` は適用範囲 \[Nmin, Nmax\] にクランプし、結果が負となる場合は 0 を返す
92///   (N=Nmax(全断面圧縮)・N=Nmin(全主筋引張降伏)で曲げ余力なし)。
93/// - `inp.b`, `inp.d`(=D), `inp.at`, `inp.sigma_y`, `inp.fc` を用いる。
94///   不正入力(b, d, at, σy, Fc のいずれかが 0 以下)は 0.0 を返す。
95///
96/// RC 規準の柱設計用せん断力 QD1 = ΣcMy/h′
97/// における柱の終局曲げ(cMy)の算定に用いる。
98pub fn rc_column_mu_simple(inp: &RcCapacityInput, ag: f64, n_axial: f64) -> f64 {
99    if inp.b <= 0.0 || inp.d <= 0.0 || inp.at <= 0.0 || inp.sigma_y <= 0.0 || inp.fc <= 0.0 {
100        return 0.0;
101    }
102    let (b, d, at, sy, fc) = (inp.b, inp.d, inp.at, inp.sigma_y, inp.fc);
103    let ag = ag.max(at);
104    let n_max = b * d * fc + ag * sy;
105    let n_min = -ag * sy;
106    let n = n_axial.clamp(n_min, n_max);
107    let n_bal = 0.4 * b * d * fc;
108
109    let mu = if n > n_bal {
110        let m_bal = 0.8 * at * sy * d + 0.12 * b * d * d * fc;
111        m_bal * (n_max - n) / (n_max - n_bal)
112    } else if n >= 0.0 {
113        0.8 * at * sy * d + 0.5 * n * d * (1.0 - n / (b * d * fc))
114    } else {
115        0.8 * at * sy * d + 0.4 * n * d
116    };
117    mu.max(0.0)
118}
119
120/// せん断終局耐力 Qsu \[N\](荒川mean式系の略算式、要・原典照合)。
121///
122/// ```text
123/// Qsu = { 0.068・pt^0.23・(Fc+18) / (M/(Q・d_e)+0.12) + 0.85・√(pw・σwy) + 0.1・σ0 }・b・j
124/// ```
125/// - `pt = 100・at/(b・d_e)` \[%\](引張鉄筋比)
126/// - `j = 7・d_e/8`
127/// - せん断スパン比 `M/(Q・d_e) = h0/(2・d_e)` は反曲点中央(等曲げ勾配)の仮定から
128///   導く略算のため、式の適用範囲である 1.0〜3.0 にクランプする。
129/// - `pw` は式の適用範囲の上限 0.012 でクランプする(下限は 0)。
130/// - 軸力項 `0.1・σ0`(σ0: 軸方向圧縮応力度)は荒川式の適用範囲である
131///   0〜0.4Fc にクランプする(負の σ0(引張)は 0 とみなし、Qsu を低減しない
132///   安全側の扱いとする)。
133///
134/// 全係数は要・原典照合(靭性指針/技術基準解説書等)。
135/// 不正入力(b, d_eff, at, Fc, clear_span のいずれかが 0 以下)は 0.0 を返す。
136pub fn rc_qsu_simple(inp: &RcCapacityInput) -> f64 {
137    if inp.b <= 0.0 || inp.d_eff <= 0.0 || inp.at <= 0.0 || inp.fc <= 0.0 || inp.clear_span <= 0.0 {
138        return 0.0;
139    }
140    let pt = 100.0 * inp.at / (inp.b * inp.d_eff);
141    let j = 7.0 * inp.d_eff / 8.0;
142    let shear_span_ratio = (inp.clear_span / (2.0 * inp.d_eff)).clamp(1.0, 3.0);
143    let pw = inp.pw.clamp(0.0, 0.012);
144    let concrete_term = 0.068 * pt.powf(0.23) * (inp.fc + 18.0) / (shear_span_ratio + 0.12);
145    let hoop_term = 0.85 * (pw * inp.sigma_wy).max(0.0).sqrt();
146    // 軸力項: 適用範囲 0〜0.4Fc にクランプ(荒川式の適用範囲、要・原典照合)。
147    let sigma_0 = inp.sigma_0.clamp(0.0, 0.4 * inp.fc);
148    let axial_term = 0.1 * sigma_0;
149    (concrete_term + hoop_term + axial_term) * inp.b * j
150}
151
152/// RC 梁の曲げ降伏時剛性低下率 αy(菅野式)。
153///
154/// ```text
155/// αy = (0.043 + 1.635·n·pt + 0.043·(a/D))·(d/D)²   (2.0 ≤ a/D ≤ 5.0)
156///      (−0.0836 + 0.159·(a/D))·(d/D)²              (1.0 ≤ a/D < 2.0)
157/// ```
158/// - `pt`: 引張鉄筋比(小数)
159/// - `a_over_d`: シアスパン比 a/D(a=l0/2)。適用範囲 [1.0, 5.0] にクランプする。
160/// - `d_over_full`: 有効せい/全せい d/D
161/// - `n`: ヤング係数比 Es/Ec
162///
163/// 出典: 梅村魁『鉄筋コンクリート建物の動的耐震設計法』P.106-108(要・原典照合)。
164/// トリリニア骨格の降伏点変形(θy=θe/αy)に用いる剛性低下率で、0〜1 に収まる想定。
165/// 負となる異常入力は 0 にクランプする(1 超は補正しない=呼び出し側で扱う)。
166pub fn rc_alpha_y_sugano(pt: f64, a_over_d: f64, d_over_full: f64, n: f64) -> f64 {
167    let ad = a_over_d.clamp(1.0, 5.0);
168    let base = if ad >= 2.0 {
169        0.043 + 1.635 * n * pt + 0.043 * ad
170    } else {
171        -0.0836 + 0.159 * ad
172    };
173    (base * d_over_full * d_over_full).max(0.0)
174}
175
176/// ひび割れ強度の係数 κ(技術基準解説書 P.621-623)。
177///
178/// 曲げひび割れ `Mc = κ·√Fc·Ze`、引張ひび割れ `Nct = κ·√Fc·Ac` の双方に用いる。
179pub const RC_CRACK_COEF: f64 = 0.56;
180
181/// RC 断面の曲げひび割れモーメント Mc \[N·mm\](技術基準解説書 P.621-623)。
182///
183/// `Mc = κ·√Fc·Ze`(κ=[`RC_CRACK_COEF`]、Fc \[N/mm²\]、Ze=引張側断面係数 \[mm³\])。
184/// 不正入力(Fc・Ze のいずれかが 0 以下)は 0.0 を返す。
185///
186/// 材端曲げバネ・プッシュオーバーのヒンジ閾値・RC 梁のトリリニア骨格が
187/// 共通で用いる(算定の情報源を 1 つに保つ)。Mc を降伏モーメント My との
188/// 関係でクランプするかどうかは用途ごとに異なるため、**呼び出し側**で行う。
189pub fn rc_crack_moment(fc: f64, ze: f64) -> f64 {
190    if fc <= 0.0 || ze <= 0.0 {
191        return 0.0;
192    }
193    RC_CRACK_COEF * fc.sqrt() * ze
194}
195
196/// `SectionShape::RcRect` 相当の配筋から [`RcCapacityInput`] を組み立てる。
197///
198/// - `main`: 引張側として用いる主筋セット(強軸なら `main_x`、弱軸なら `main_y`)
199/// - `rebar`: かぶり・帯筋(`d_eff`・`pw` 用)。`main` と別軸の主筋は見ない
200/// - σy は主筋材質 → 材料 `fy` → 345(SD345 相当)の順。**材料強度割増は掛けない**
201///   (保有水平耐力など割増が要る呼び出し側が後掛けする)
202/// - σwy はせん断補強筋材質 → SD295 相当既定。割増対象外
203/// - σ0 は 0(プレースホルダ)。軸力反映は呼び出し側
204/// - `fc` 未設定なら `None`
205#[allow(clippy::too_many_arguments)]
206pub fn rc_capacity_input_from_rect(
207    b: f64,
208    d: f64,
209    main: &crate::section_shape::BarSet,
210    rebar: &crate::section_shape::RcRebar,
211    mat: &crate::model::Material,
212    rebar_mat: Option<&crate::model::Material>,
213    shear_mat: Option<&crate::model::Material>,
214    clear_span: f64,
215) -> Option<RcCapacityInput> {
216    let fc = mat.fc?;
217    let at = crate::section_shape::bar_set_area(main) / 2.0;
218    let d_eff =
219        crate::rc_rebar_geom::tension_effective_depth(d, rebar.cover, rebar.shear.dia, main);
220    let pw = crate::rc_rebar_geom::pw_ratio(&rebar.shear, b);
221    Some(RcCapacityInput {
222        b,
223        d,
224        at,
225        d_eff,
226        sigma_y: crate::material_grade::rebar_yield_strength(rebar_mat)
227            .or(mat.fy)
228            .unwrap_or(345.0),
229        fc,
230        pw,
231        sigma_wy: crate::material_grade::shear_rebar_yield_strength(shear_mat)
232            .unwrap_or(crate::material_grade::SHEAR_REBAR_DEFAULT_FY),
233        clear_span,
234        sigma_0: 0.0,
235    })
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241    use crate::ids::MaterialId;
242    use crate::model::{Material, MaterialCategory};
243    use crate::section_shape::{BarSet, RcRebar, ShearBar};
244
245    /// 代表断面: b=400, D=600, at=1935(D25×3程度), d_eff=530, σy=345, Fc=24,
246    /// pw=0.002, σwy=295, h0=3000。
247    fn sample_input() -> RcCapacityInput {
248        RcCapacityInput {
249            b: 400.0,
250            d: 600.0,
251            at: 1935.0,
252            d_eff: 530.0,
253            sigma_y: 345.0,
254            fc: 24.0,
255            pw: 0.002,
256            sigma_wy: 295.0,
257            clear_span: 3000.0,
258            sigma_0: 0.0,
259        }
260    }
261
262    #[test]
263    fn rc_capacity_input_from_rect_matches_handcalc_without_strength_factor() {
264        let rebar = RcRebar {
265            main_x: BarSet {
266                count: 8,
267                dia: 22.0,
268                layers: 1,
269            },
270            main_y: BarSet {
271                count: 4,
272                dia: 22.0,
273                layers: 1,
274            },
275            cover: 40.0,
276            shear: ShearBar {
277                dia: 10.0,
278                pitch: 150.0,
279                legs: 2,
280            },
281        };
282        let mat = Material {
283            strength_factor: None,
284            concrete_class: Default::default(),
285            id: MaterialId(0),
286            name: "FC24".into(),
287            category: MaterialCategory::Concrete,
288            young: 23000.0,
289            poisson: 0.2,
290            density: 2.4e-9,
291            shear: None,
292            fc: Some(24.0),
293            fy: None,
294        };
295        let input = rc_capacity_input_from_rect(
296            400.0,
297            600.0,
298            &rebar.main_x,
299            &rebar,
300            &mat,
301            None,
302            None,
303            3000.0,
304        )
305        .expect("fc set");
306        let at_expected = crate::section_shape::bar_set_area(&rebar.main_x) / 2.0;
307        let d_eff_expected =
308            crate::rc_rebar_geom::tension_effective_depth(600.0, 40.0, 10.0, &rebar.main_x);
309        assert!((input.at - at_expected).abs() < 1e-9);
310        assert!((input.d_eff - d_eff_expected).abs() < 1e-9);
311        assert_eq!(input.sigma_y, 345.0);
312        assert_eq!(input.sigma_wy, 295.0);
313    }
314
315    #[test]
316    fn test_rc_mu_simple_matches_handcalc() {
317        let inp = sample_input();
318        // 手計算: Mu = 0.9·at·σy·d = 0.9*1935*345*530(技術基準解説書 P.623)
319        let mu_handcalc = 0.9 * 1935.0 * 345.0 * 530.0;
320        let mu = rc_mu_simple(&inp);
321        assert!(
322            (mu - mu_handcalc).abs() < 1e-6,
323            "Mu={} vs handcalc={}",
324            mu,
325            mu_handcalc
326        );
327    }
328
329    #[test]
330    fn test_rc_column_mu_simple_branches() {
331        let inp = sample_input();
332        let (b, d, at, sy, fc) = (400.0_f64, 600.0, 1935.0, 345.0, 24.0);
333        let ag = 2.0 * at; // 対称配筋の全主筋
334        let n_bal = 0.4 * b * d * fc; // 2,304,000 N
335        let n_max = b * d * fc + ag * sy;
336
337        // N=0: Mu = 0.8・at・σy・D。
338        let mu0 = rc_column_mu_simple(&inp, ag, 0.0);
339        assert!((mu0 - 0.8 * at * sy * d).abs() < 1e-6);
340
341        // 中間圧縮軸力(N=0.2bDFc): 軸力項で Mu が増える。
342        let n1 = 0.2 * b * d * fc;
343        let mu1 = rc_column_mu_simple(&inp, ag, n1);
344        let expect1 = 0.8 * at * sy * d + 0.5 * n1 * d * (1.0 - n1 / (b * d * fc));
345        assert!((mu1 - expect1).abs() < 1e-6);
346        assert!(mu1 > mu0);
347
348        // 高圧縮域(N>0.4bDFc): Nmax で 0 に線形低減。
349        let mu_at_nmax = rc_column_mu_simple(&inp, ag, n_max);
350        assert!(mu_at_nmax.abs() < 1e-6);
351        let n2 = 0.7 * n_max + 0.3 * n_bal;
352        let mu2 = rc_column_mu_simple(&inp, ag, n2);
353        let m_bal = 0.8 * at * sy * d + 0.12 * b * d * d * fc;
354        let expect2 = m_bal * (n_max - n2) / (n_max - n_bal);
355        assert!((mu2 - expect2).abs() < 1e-6);
356
357        // 引張軸力: Mu = 0.8atσyD + 0.4ND(N<0)で減少、Nmin 以下で 0。
358        let n3 = -0.5 * ag * sy;
359        let mu3 = rc_column_mu_simple(&inp, ag, n3);
360        assert!((mu3 - (0.8 * at * sy * d + 0.4 * n3 * d)).abs() < 1e-6);
361        assert!(mu3 < mu0);
362        // 境界の連続性: N=0.4bDFc で両分岐が一致する。
363        let lo = rc_column_mu_simple(&inp, ag, n_bal - 1e-6);
364        let hi = rc_column_mu_simple(&inp, ag, n_bal + 1e-6);
365        assert!(
366            (lo - hi).abs() / lo < 1e-6,
367            "branch continuity: {lo} vs {hi}"
368        );
369    }
370
371    #[test]
372    fn test_rc_qmu_simple_matches_handcalc() {
373        let inp = sample_input();
374        let mu_handcalc = 0.9 * 1935.0 * 345.0 * 530.0;
375        let qmu_handcalc = 2.0 * mu_handcalc / 3000.0;
376        let qmu = rc_qmu_simple(&inp);
377        assert!(
378            (qmu - qmu_handcalc).abs() < 1e-6,
379            "Qmu={} vs handcalc={}",
380            qmu,
381            qmu_handcalc
382        );
383    }
384
385    #[test]
386    fn test_rc_alpha_y_sugano_matches_handcalc() {
387        // a/D=3.0(2.0-5.0域), pt=0.008, n=15, d/D=0.9
388        let ay = rc_alpha_y_sugano(0.008, 3.0, 0.9, 15.0);
389        let base = 0.043 + 1.635 * 15.0 * 0.008 + 0.043 * 3.0;
390        assert!((ay - base * 0.9 * 0.9).abs() < 1e-9, "αy={ay}");
391        // 代表値は 0.2〜0.4 程度。
392        assert!(ay > 0.15 && ay < 0.5, "αy={ay}");
393
394        // a/D=1.5(1.0-2.0域)は別分岐。
395        let ay2 = rc_alpha_y_sugano(0.008, 1.5, 0.9, 15.0);
396        let base2 = -0.0836 + 0.159 * 1.5;
397        assert!((ay2 - base2 * 0.81).abs() < 1e-9);
398
399        // a/D クランプ: 0.5→1.0, 8.0→5.0。
400        let lo = rc_alpha_y_sugano(0.008, 0.5, 0.9, 15.0);
401        let at1 = rc_alpha_y_sugano(0.008, 1.0, 0.9, 15.0);
402        assert!((lo - at1).abs() < 1e-12);
403        let hi = rc_alpha_y_sugano(0.008, 8.0, 0.9, 15.0);
404        let at5 = rc_alpha_y_sugano(0.008, 5.0, 0.9, 15.0);
405        assert!((hi - at5).abs() < 1e-12);
406    }
407
408    #[test]
409    fn test_rc_qsu_simple_matches_handcalc() {
410        let inp = sample_input();
411        // 手計算(クランプ域内): pt=100*1935/(400*530)=0.912736%,
412        // shear_span_ratio=3000/(2*530)=2.830189(1.0-3.0の範囲内なのでクランプなし),
413        // pw=0.002(0.012以下なのでクランプなし)。
414        let pt: f64 = 100.0 * 1935.0 / (400.0 * 530.0);
415        let j = 7.0 * 530.0 / 8.0;
416        let shear_span_ratio: f64 = 3000.0 / (2.0 * 530.0);
417        let concrete_term = 0.068 * pt.powf(0.23) * (24.0 + 18.0) / (shear_span_ratio + 0.12);
418        let hoop_term = 0.85 * (0.002_f64 * 295.0).sqrt();
419        let qsu_handcalc = (concrete_term + hoop_term) * 400.0 * j;
420
421        let qsu = rc_qsu_simple(&inp);
422        assert!(
423            (qsu - qsu_handcalc).abs() < 1e-6,
424            "Qsu={} vs handcalc={}",
425            qsu,
426            qsu_handcalc
427        );
428        // 参考: せん断余裕度 Qsu/Qmu ≈ 1.6 程度(曲げ降伏が先行する健全な部材の目安)。
429        let qmu = rc_qmu_simple(&inp);
430        assert!(qsu / qmu > 1.0, "Qsu/Qmu={}", qsu / qmu);
431    }
432
433    #[test]
434    fn test_rc_qsu_simple_clamps_shear_span_ratio_low() {
435        // h0 を極端に短くすると shear_span_ratio = h0/(2*d_eff) < 1.0 → 1.0 にクランプ。
436        let mut inp = sample_input();
437        inp.clear_span = 200.0; // 200/(2*530)=0.1887 < 1.0
438        let qsu = rc_qsu_simple(&inp);
439
440        let pt: f64 = 100.0 * 1935.0 / (400.0 * 530.0);
441        let j = 7.0 * 530.0 / 8.0;
442        let concrete_term = 0.068 * pt.powf(0.23) * (24.0 + 18.0) / (1.0 + 0.12); // クランプ後 1.0
443        let hoop_term = 0.85 * (0.002_f64 * 295.0).sqrt();
444        let qsu_handcalc = (concrete_term + hoop_term) * 400.0 * j;
445        assert!(
446            (qsu - qsu_handcalc).abs() < 1e-6,
447            "Qsu={} vs handcalc(clamped)={}",
448            qsu,
449            qsu_handcalc
450        );
451    }
452
453    #[test]
454    fn test_rc_qsu_simple_clamps_shear_span_ratio_high() {
455        // h0 を極端に長くすると shear_span_ratio = h0/(2*d_eff) > 3.0 → 3.0 にクランプ。
456        let mut inp = sample_input();
457        inp.clear_span = 6000.0; // 6000/(2*530)=5.660 > 3.0
458        let qsu = rc_qsu_simple(&inp);
459
460        let pt: f64 = 100.0 * 1935.0 / (400.0 * 530.0);
461        let j = 7.0 * 530.0 / 8.0;
462        let concrete_term = 0.068 * pt.powf(0.23) * (24.0 + 18.0) / (3.0 + 0.12); // クランプ後 3.0
463        let hoop_term = 0.85 * (0.002_f64 * 295.0).sqrt();
464        let qsu_handcalc = (concrete_term + hoop_term) * 400.0 * j;
465        assert!(
466            (qsu - qsu_handcalc).abs() < 1e-6,
467            "Qsu={} vs handcalc(clamped)={}",
468            qsu,
469            qsu_handcalc
470        );
471    }
472
473    #[test]
474    fn test_rc_qsu_simple_clamps_pw_upper_bound() {
475        // pw が適用範囲の上限 0.012 を超える場合は 0.012 にクランプされる。
476        let mut inp_over = sample_input();
477        inp_over.pw = 0.05;
478        let mut inp_clamped = sample_input();
479        inp_clamped.pw = 0.012;
480
481        let qsu_over = rc_qsu_simple(&inp_over);
482        let qsu_clamped = rc_qsu_simple(&inp_clamped);
483        assert!(
484            (qsu_over - qsu_clamped).abs() < 1e-9,
485            "qsu_over={} vs qsu_clamped={}",
486            qsu_over,
487            qsu_clamped
488        );
489        // クランプなしでは pw=0.05 の方が pw=0.002 より Qsu が大きくなるはず。
490        assert!(qsu_clamped > rc_qsu_simple(&sample_input()));
491    }
492
493    #[test]
494    fn test_rc_mu_simple_invalid_inputs_are_zero() {
495        let base = sample_input();
496
497        let mut at_zero = sample_input();
498        at_zero.at = 0.0;
499        assert_eq!(rc_mu_simple(&at_zero), 0.0);
500
501        let mut d_eff_zero = sample_input();
502        d_eff_zero.d_eff = 0.0;
503        assert_eq!(rc_mu_simple(&d_eff_zero), 0.0);
504
505        let mut sigma_y_zero = sample_input();
506        sigma_y_zero.sigma_y = 0.0;
507        assert_eq!(rc_mu_simple(&sigma_y_zero), 0.0);
508
509        // 妥当な入力は正の値になることの確認(比較対象)。
510        assert!(rc_mu_simple(&base) > 0.0);
511    }
512
513    #[test]
514    fn test_rc_qmu_simple_zero_clear_span_is_zero() {
515        let mut inp = sample_input();
516        inp.clear_span = 0.0;
517        assert_eq!(rc_qmu_simple(&inp), 0.0);
518
519        let mut inp_neg = sample_input();
520        inp_neg.clear_span = -100.0;
521        assert_eq!(rc_qmu_simple(&inp_neg), 0.0);
522    }
523
524    #[test]
525    fn test_rc_qsu_simple_invalid_inputs_are_zero() {
526        let mut b_zero = sample_input();
527        b_zero.b = 0.0;
528        assert_eq!(rc_qsu_simple(&b_zero), 0.0);
529
530        let mut d_eff_zero = sample_input();
531        d_eff_zero.d_eff = 0.0;
532        assert_eq!(rc_qsu_simple(&d_eff_zero), 0.0);
533
534        let mut at_zero = sample_input();
535        at_zero.at = 0.0;
536        assert_eq!(rc_qsu_simple(&at_zero), 0.0);
537
538        let mut fc_zero = sample_input();
539        fc_zero.fc = 0.0;
540        assert_eq!(rc_qsu_simple(&fc_zero), 0.0);
541
542        let mut span_zero = sample_input();
543        span_zero.clear_span = 0.0;
544        assert_eq!(rc_qsu_simple(&span_zero), 0.0);
545    }
546
547    #[test]
548    fn test_rc_qsu_simple_sigma_0_zero_matches_original() {
549        // sigma_0=0.0(既定)は従来値と一致すること。
550        let inp = sample_input();
551        assert_eq!(inp.sigma_0, 0.0);
552        let qsu = rc_qsu_simple(&inp);
553        let pt: f64 = 100.0 * 1935.0 / (400.0 * 530.0);
554        let j = 7.0 * 530.0 / 8.0;
555        let shear_span_ratio: f64 = 3000.0 / (2.0 * 530.0);
556        let concrete_term = 0.068 * pt.powf(0.23) * (24.0 + 18.0) / (shear_span_ratio + 0.12);
557        let hoop_term = 0.85 * (0.002_f64 * 295.0).sqrt();
558        let qsu_handcalc = (concrete_term + hoop_term) * 400.0 * j;
559        assert!((qsu - qsu_handcalc).abs() < 1e-6);
560    }
561
562    #[test]
563    fn test_rc_qsu_simple_axial_term_adds_01_sigma0_b_j() {
564        // 適用範囲内(0〜0.4Fc=9.6)の sigma_0=5.0 のとき、Qsu は
565        // sigma_0=0 の場合に対して厳密に 0.1・σ0・b・j 分だけ増える。
566        let mut inp = sample_input();
567        let qsu_base = rc_qsu_simple(&inp);
568        inp.sigma_0 = 5.0;
569        let qsu_with_axial = rc_qsu_simple(&inp);
570        let j = 7.0 * 530.0 / 8.0;
571        let expected_delta = 0.1 * 5.0 * 400.0 * j;
572        assert!(
573            (qsu_with_axial - qsu_base - expected_delta).abs() < 1e-6,
574            "delta={} expected={}",
575            qsu_with_axial - qsu_base,
576            expected_delta
577        );
578    }
579
580    #[test]
581    fn test_rc_qsu_simple_sigma_0_clamped_to_upper_bound_04fc() {
582        // Fc=24.0 → 上限 0.4*24=9.6。これを超える sigma_0=20.0 は 9.6 にクランプされる。
583        let mut inp_over = sample_input();
584        inp_over.sigma_0 = 20.0;
585        let mut inp_clamped = sample_input();
586        inp_clamped.sigma_0 = 0.4 * 24.0;
587        assert!((rc_qsu_simple(&inp_over) - rc_qsu_simple(&inp_clamped)).abs() < 1e-9);
588        // クランプなしでは sigma_0=9.6 の方が sigma_0=0 より Qsu が大きいはず。
589        assert!(rc_qsu_simple(&inp_clamped) > rc_qsu_simple(&sample_input()));
590    }
591
592    #[test]
593    fn test_rc_qsu_simple_sigma_0_negative_is_clamped_to_zero() {
594        // 負の sigma_0(引張)は 0 とみなす(Qsu を低減しない安全側)。
595        let mut inp_neg = sample_input();
596        inp_neg.sigma_0 = -10.0;
597        let qsu_neg = rc_qsu_simple(&inp_neg);
598        let qsu_zero = rc_qsu_simple(&sample_input());
599        assert!((qsu_neg - qsu_zero).abs() < 1e-9);
600    }
601}