Skip to main content

squid_n_core/
load_combo.rs

1use crate::ids::LoadCaseId;
2use crate::model::LoadCombination;
3
4/// 多雪区域の積雪荷重低減係数(令86条 多雪区域の荷重組合せ)。
5///
6/// - `delta1`: 長期積雪 `DL+LL+δ1・SL` の低減係数(既定 0.7)
7/// - `delta3`: 地震時 `DL+LL+δ3・SL±EX/EY` の低減係数(既定 0.35)
8///
9/// 本実装では直接入力が可能(デフォルト δ1=0.7、δ3=0.35)。
10/// 暴風時の δ2 は、風荷重の組合せを生成しないため持たない。
11#[derive(Clone, Copy, Debug, PartialEq)]
12pub struct SnowFactors {
13    pub delta1: f64,
14    pub delta3: f64,
15}
16
17impl Default for SnowFactors {
18    fn default() -> Self {
19        SnowFactors {
20            delta1: 0.7,
21            delta3: 0.35,
22        }
23    }
24}
25
26/// [`standard_combinations`] への入力ケース指定。
27pub struct ComboInput {
28    pub dl: LoadCaseId,
29    pub ll: LoadCaseId,
30    pub seismic_x: Option<LoadCaseId>,
31    pub seismic_y: Option<LoadCaseId>,
32    pub snow: Option<LoadCaseId>,
33    /// 多雪区域か否か(建築基準法施行令86条・同82条)。
34    /// `true` の場合、長期に `δ1・S` を加算し、短期地震に `δ3・S` を
35    /// 加算した組合せも追加で生成する。
36    pub heavy_snow_zone: bool,
37    /// 多雪区域の積雪荷重低減係数。`None` は既定値(δ1=0.7、δ2=δ3=0.35)。
38    pub snow_factors: Option<SnowFactors>,
39}
40
41fn push_gp(combos: &mut Vec<LoadCombination>, dl: LoadCaseId, ll: LoadCaseId) {
42    combos.push(LoadCombination {
43        name: "DL + LL".into(),
44        terms: vec![(dl, 1.0), (ll, 1.0)],
45    });
46}
47
48/// 建築基準法施行令82条の標準荷重組合せを生成する。
49///
50/// 組合せ名は荷重ケースの直接的な名前(DL:固定・LL:積載・EX/EY:地震 X/Y・
51/// SL:積雪)で表す。算定式との対応は
52/// G→DL・P→LL・K→EX/EY・S→SL(積雪は単一ケースのため方向なし)。
53///
54/// - 長期: `DL+LL`。多雪区域はさらに `DL+LL+0.7SL`。
55/// - 短期積雪: `DL+LL+SL`(`snow` が指定されている場合)。
56/// - 短期地震: `DL+LL±EX`・`DL+LL±EY`(±両方向)。多雪区域はさらに
57///   `DL+LL+0.35SL±EX`(X・Y 各方向)。
58///
59/// 各ケースは `seismic_x`/`seismic_y`/`snow` が `Some` の場合のみ生成される
60/// (レビュー §1.10)。
61///
62/// 風荷重は算定・生成の対象外のため、暴風の組合せは作らない。風荷重ケースを
63/// 定義しても、そのケースを含む組合せは自動生成されない。
64pub fn standard_combinations(input: &ComboInput) -> Vec<LoadCombination> {
65    let mut combos = Vec::new();
66    let dl = input.dl;
67    let ll = input.ll;
68    let sf = input.snow_factors.unwrap_or_default();
69
70    // 長期: DL+LL
71    push_gp(&mut combos, dl, ll);
72
73    // 多雪区域の長期: DL+LL+δ1・SL
74    if input.heavy_snow_zone {
75        if let Some(snow) = input.snow {
76            combos.push(LoadCombination {
77                name: format!("DL + LL + {}SL", trim_f64(sf.delta1)),
78                terms: vec![(dl, 1.0), (ll, 1.0), (snow, sf.delta1)],
79            });
80        }
81    }
82
83    // 短期積雪: DL+LL+SL
84    if let Some(snow) = input.snow {
85        combos.push(LoadCombination {
86            name: "DL + LL + SL".into(),
87            terms: vec![(dl, 1.0), (ll, 1.0), (snow, 1.0)],
88        });
89    }
90
91    // 短期地震(±両方向、多雪区域は δ3・SL 付きも追加)。
92    push_directional(
93        &mut combos,
94        dl,
95        ll,
96        input.seismic_x,
97        "EX",
98        input.snow,
99        input.heavy_snow_zone,
100        sf.delta3,
101    );
102    push_directional(
103        &mut combos,
104        dl,
105        ll,
106        input.seismic_y,
107        "EY",
108        input.snow,
109        input.heavy_snow_zone,
110        sf.delta3,
111    );
112
113    combos
114}
115
116/// 係数を組合せ名向けに整形する(末尾の 0 を落とす。例: 0.70 → "0.7")。
117fn trim_f64(v: f64) -> String {
118    let s = format!("{v:.3}");
119    s.trim_end_matches('0').trim_end_matches('.').to_string()
120}
121
122/// 地震・暴風いずれかの片方向(EX/EY/WX/WY)について、`DL+LL±X` と
123/// 多雪区域なら `DL+LL+δ・SL±X`(δ: 暴風時 δ2/地震時 δ3)を追加する共通ヘルパー。
124#[allow(clippy::too_many_arguments)]
125fn push_directional(
126    combos: &mut Vec<LoadCombination>,
127    dl: LoadCaseId,
128    ll: LoadCaseId,
129    case: Option<LoadCaseId>,
130    label: &str,
131    snow: Option<LoadCaseId>,
132    heavy_snow_zone: bool,
133    delta: f64,
134) {
135    let Some(case) = case else {
136        return;
137    };
138    combos.push(LoadCombination {
139        name: format!("DL + LL + {label}"),
140        terms: vec![(dl, 1.0), (ll, 1.0), (case, 1.0)],
141    });
142    combos.push(LoadCombination {
143        name: format!("DL + LL - {label}"),
144        terms: vec![(dl, 1.0), (ll, 1.0), (case, -1.0)],
145    });
146    if heavy_snow_zone {
147        if let Some(snow) = snow {
148            let d = trim_f64(delta);
149            combos.push(LoadCombination {
150                name: format!("DL + LL + {d}SL + {label}"),
151                terms: vec![(dl, 1.0), (ll, 1.0), (snow, delta), (case, 1.0)],
152            });
153            combos.push(LoadCombination {
154                name: format!("DL + LL + {d}SL - {label}"),
155                terms: vec![(dl, 1.0), (ll, 1.0), (snow, delta), (case, -1.0)],
156            });
157        }
158    }
159}
160
161/// 旧API(後方互換)。断面検定などから使う単純版
162/// (長期 DL+LL / 短期積雪 DL+LL+SL / 短期地震 DL+LL±EX/EY の正負両加力)。
163/// 内部では [`standard_combinations`] に委譲する。多雪区域の係数付き組合せが
164/// 必要な場合は [`standard_combinations`] を直接使う。
165pub fn auto_combinations(
166    dl_case: LoadCaseId,
167    ll_case: LoadCaseId,
168    seismic_x: Option<LoadCaseId>,
169    seismic_y: Option<LoadCaseId>,
170    snow_case: Option<LoadCaseId>,
171) -> Vec<LoadCombination> {
172    let input = ComboInput {
173        dl: dl_case,
174        ll: ll_case,
175        seismic_x,
176        seismic_y,
177        snow: snow_case,
178        heavy_snow_zone: false,
179        snow_factors: None,
180    };
181    standard_combinations(&input)
182}
183
184/// 荷重組合せ名から断面検定の荷重継続性区分(長期/短期)を判定する。
185///
186/// 令82条(標準組合せ)・令86条(多雪区域): DL+LL(多雪区域では
187/// DL+LL+0.7SL も)が長期(常時・積雪時の長期)、地震(EX/EY)を含む組合せ
188/// および短期積雪(DL+LL+SL)は短期(令82条)。
189/// [`standard_combinations`] の命名規約("DL + LL ± EX"・"DL + LL + 0.7SL" 等)に
190/// 基づき、追加項の記号で判定する。
191///
192/// 風記号 W も短期として扱う。自動生成では暴風の組合せを作らないが、風荷重を
193/// 含む組合せを保存データが持つ場合に長期と誤判定しないためである。
194pub fn is_short_term_combo(name: &str) -> bool {
195    let upper = name.to_uppercase();
196    // 地震(記号 E。旧名の K も含む)・風(W)を含めば短期。
197    if upper.contains('K') || upper.contains('E') || upper.contains('W') {
198        return true;
199    }
200    // 多雪区域の長期積雪 δ1・S(係数 <1.0 の S 項。例 "0.7SL"・"0.65SL")は
201    // 長期(令82条一号)。係数なしの S(DL+LL+SL)は短期積雪。
202    if let Some(pos) = upper.find('S') {
203        let coef: String = upper[..pos]
204            .chars()
205            .rev()
206            .take_while(|c| c.is_ascii_digit() || *c == '.')
207            .collect::<Vec<_>>()
208            .into_iter()
209            .rev()
210            .collect();
211        if let Ok(v) = coef.parse::<f64>() {
212            return v >= 1.0;
213        }
214        return true;
215    }
216    false
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_auto_combos() {
225        let combos = auto_combinations(
226            LoadCaseId(1),
227            LoadCaseId(2),
228            Some(LoadCaseId(3)),
229            Some(LoadCaseId(4)),
230            None,
231        );
232        // DL+LL, DL+LL±EX, DL+LL±EY の 5 組合せ
233        assert_eq!(combos.len(), 5);
234        assert_eq!(combos[0].name, "DL + LL");
235        assert_eq!(combos[1].name, "DL + LL + EX");
236        assert_eq!(combos[2].name, "DL + LL - EX");
237        assert_eq!(combos[3].name, "DL + LL + EY");
238        assert_eq!(combos[4].name, "DL + LL - EY");
239        // 負側加力は係数 -1.0
240        assert_eq!(combos[2].terms[2].1, -1.0);
241        assert_eq!(combos[4].terms[2].1, -1.0);
242    }
243
244    #[test]
245    fn test_is_short_term_combo() {
246        assert!(!is_short_term_combo("DL + LL"));
247        assert!(is_short_term_combo("DL + LL + EX"));
248        assert!(is_short_term_combo("DL + LL - EX"));
249        assert!(is_short_term_combo("DL + LL + EY"));
250        assert!(is_short_term_combo("DL + LL + SL"));
251        assert!(is_short_term_combo("DL + LL + WX"));
252        assert!(is_short_term_combo("DL + LL - WY"));
253        // 多雪区域: 長期 0.7SL は長期、0.35SL 付き短期は短期。
254        assert!(!is_short_term_combo("DL + LL + 0.7SL"));
255        assert!(is_short_term_combo("DL + LL + 0.35SL + EX"));
256        assert!(is_short_term_combo("DL + LL + 0.35SL - WY"));
257        // 旧名(G+P・Kx/Wx 等)の保存データも従来どおり判定できる(後方互換)。
258        assert!(!is_short_term_combo("G + P"));
259        assert!(is_short_term_combo("G + P + Kx"));
260        assert!(is_short_term_combo("G + P + Wx"));
261        assert!(!is_short_term_combo("G + P + 0.7S"));
262    }
263
264    #[test]
265    fn test_auto_combos_no_snow_matches_legacy_shape() {
266        // 多雪区域=false・風=None の従来相当構成では、長期1 + 短期積雪0
267        // + 地震(±EX,±EY)=4 の計 5 ケース。
268        let combos = auto_combinations(
269            LoadCaseId(1),
270            LoadCaseId(2),
271            Some(LoadCaseId(3)),
272            Some(LoadCaseId(4)),
273            None,
274        );
275        assert_eq!(combos.len(), 5);
276        let names: Vec<&str> = combos.iter().map(|c| c.name.as_str()).collect();
277        assert_eq!(
278            names,
279            vec![
280                "DL + LL",
281                "DL + LL + EX",
282                "DL + LL - EX",
283                "DL + LL + EY",
284                "DL + LL - EY"
285            ]
286        );
287    }
288
289    #[test]
290    fn test_standard_combinations_all_cases_heavy_snow() {
291        let input = ComboInput {
292            dl: LoadCaseId(1),
293            ll: LoadCaseId(2),
294            seismic_x: Some(LoadCaseId(3)),
295            seismic_y: Some(LoadCaseId(4)),
296            snow: Some(LoadCaseId(7)),
297            heavy_snow_zone: true,
298            snow_factors: None,
299        };
300        let combos = standard_combinations(&input);
301        // DL+LL(1) + DL+LL+0.7SL(1) + DL+LL+SL(1) + EX系4 + EY系4 = 3 + 8 = 11
302        assert_eq!(combos.len(), 11);
303
304        let by_name = |n: &str| {
305            combos
306                .iter()
307                .find(|c| c.name == n)
308                .unwrap_or_else(|| panic!("missing combo {n}"))
309        };
310
311        assert_eq!(
312            by_name("DL + LL").terms,
313            vec![(LoadCaseId(1), 1.0), (LoadCaseId(2), 1.0)]
314        );
315        assert_eq!(
316            by_name("DL + LL + 0.7SL").terms,
317            vec![
318                (LoadCaseId(1), 1.0),
319                (LoadCaseId(2), 1.0),
320                (LoadCaseId(7), 0.7)
321            ]
322        );
323        assert_eq!(
324            by_name("DL + LL + SL").terms,
325            vec![
326                (LoadCaseId(1), 1.0),
327                (LoadCaseId(2), 1.0),
328                (LoadCaseId(7), 1.0)
329            ]
330        );
331        assert_eq!(
332            by_name("DL + LL + EX").terms,
333            vec![
334                (LoadCaseId(1), 1.0),
335                (LoadCaseId(2), 1.0),
336                (LoadCaseId(3), 1.0)
337            ]
338        );
339        assert_eq!(
340            by_name("DL + LL - EX").terms,
341            vec![
342                (LoadCaseId(1), 1.0),
343                (LoadCaseId(2), 1.0),
344                (LoadCaseId(3), -1.0)
345            ]
346        );
347        assert_eq!(
348            by_name("DL + LL + 0.35SL + EX").terms,
349            vec![
350                (LoadCaseId(1), 1.0),
351                (LoadCaseId(2), 1.0),
352                (LoadCaseId(7), 0.35),
353                (LoadCaseId(3), 1.0)
354            ]
355        );
356        assert_eq!(
357            by_name("DL + LL + 0.35SL - EY").terms,
358            vec![
359                (LoadCaseId(1), 1.0),
360                (LoadCaseId(2), 1.0),
361                (LoadCaseId(7), 0.35),
362                (LoadCaseId(4), -1.0)
363            ]
364        );
365        // 風荷重は生成対象外のため、暴風の組合せは 1 件も作られない。
366        assert!(combos.iter().all(|c| !c.name.contains('W')));
367    }
368
369    #[test]
370    fn test_snow_factors_direct_input() {
371        // δ1/δ3 の直接入力(デフォルト 0.7/0.35、直接入力可能)。
372        // 名前・係数の両方に反映される。
373        let input = ComboInput {
374            dl: LoadCaseId(1),
375            ll: LoadCaseId(2),
376            seismic_x: Some(LoadCaseId(3)),
377            seismic_y: None,
378            snow: Some(LoadCaseId(7)),
379            heavy_snow_zone: true,
380            snow_factors: Some(SnowFactors {
381                delta1: 0.65,
382                delta3: 0.4,
383            }),
384        };
385        let combos = standard_combinations(&input);
386        let by_name = |n: &str| {
387            combos
388                .iter()
389                .find(|c| c.name == n)
390                .unwrap_or_else(|| panic!("missing combo {n}"))
391        };
392        // 長期積雪: δ1=0.65
393        assert_eq!(by_name("DL + LL + 0.65SL").terms[2], (LoadCaseId(7), 0.65));
394        // 地震時: δ3=0.4
395        assert_eq!(
396            by_name("DL + LL + 0.4SL + EX").terms[2],
397            (LoadCaseId(7), 0.4)
398        );
399        // 長短期判定: δ1 付きは長期、δ3 付き地震は短期。
400        assert!(!is_short_term_combo("DL + LL + 0.65SL"));
401        assert!(is_short_term_combo("DL + LL + 0.4SL + EX"));
402        assert!(is_short_term_combo("DL + LL + SL"));
403    }
404
405    #[test]
406    fn test_standard_combinations_no_heavy_snow() {
407        let input = ComboInput {
408            dl: LoadCaseId(1),
409            ll: LoadCaseId(2),
410            seismic_x: Some(LoadCaseId(3)),
411            seismic_y: Some(LoadCaseId(4)),
412            snow: Some(LoadCaseId(5)),
413            heavy_snow_zone: false,
414            snow_factors: None,
415        };
416        let combos = standard_combinations(&input);
417        // DL+LL(1) + DL+LL+SL(1) + EX系2 + EY系2 = 6(多雪でないので 0.7SL・0.35SL 系は無し)
418        assert_eq!(combos.len(), 6);
419        assert!(combos.iter().all(|c| !c.name.contains("0.35SL")));
420        assert!(combos.iter().all(|c| !c.name.contains("0.7SL")));
421        assert!(combos.iter().all(|c| !c.name.contains('W')));
422    }
423
424    #[test]
425    fn test_default_combinations_matches_auto_combinations() {
426        // default_combinations(新規モデルの既定)は standard_combinations の
427        // 固定引数版として実装されているため、生成規則そのものは一致が保証されている。
428        // 本テストが確かめるのは**ケース ID の割り当て**(標準ケースの並び
429        // 0:DL, 1:LL(架構用), 3:EX, 4:EY を正しく渡しているか)と、生成された
430        // 名前で長短期の判別が機能することの 2 点。
431        // (かつて default_combinations は組合せを手書きしており、本テストが
432        // 唯一の同期手段だった。実装を共有した現在は割り当ての検査に役割が変わる。)
433        let expected = auto_combinations(
434            LoadCaseId(0),
435            LoadCaseId(1),
436            Some(LoadCaseId(3)),
437            Some(LoadCaseId(4)),
438            None,
439        );
440        let actual = crate::model::default_combinations();
441        assert_eq!(
442            actual, expected,
443            "default_combinations が auto_combinations(DL/LL/EX/EY)と一致していない"
444        );
445        // 表示名で長短期の判別が正しく機能する(DL+LL は長期、地震4件は短期)。
446        assert!(!is_short_term_combo(&actual[0].name));
447        for c in &actual[1..] {
448            assert!(is_short_term_combo(&c.name), "{} は短期のはず", c.name);
449        }
450    }
451
452    #[test]
453    fn test_standard_combinations_empty_optional_cases() {
454        let input = ComboInput {
455            dl: LoadCaseId(1),
456            ll: LoadCaseId(2),
457            seismic_x: None,
458            seismic_y: None,
459            snow: None,
460            heavy_snow_zone: false,
461            snow_factors: None,
462        };
463        let combos = standard_combinations(&input);
464        assert_eq!(combos.len(), 1);
465        assert_eq!(combos[0].name, "DL + LL");
466    }
467}