Skip to main content

squid_n_core/
panel_zone.rs

1//! 仕口パネル(柱梁接合部パネル)の諸元解決。
2//!
3//! 柱・梁の断面形状からパネルの寸法(柱せい `dc`・板厚 `tp`・梁せい `db`)と、
4//! そこから定まる形状係数 κ・実効体積 `Ve` を解決する。
5//!
6//! # 単一の出所とする理由
7//!
8//! パネルの諸元は次の 2 箇所で必要になる。
9//!
10//! - **モデル化**(`squid_n_element::springs::panel`)— せん断剛性 `Kxp = Kyp = G・Ve`
11//! - **断面検定**(`squid_n_design_jp::steel::panel_zone`)— 降伏モーメント
12//!   `pMy = (Ve/κ)・√(1−n²)・Fy/√3`
13//!
14//! 両者が別々に断面形状を解釈すると、同じ接合部に対して剛性と耐力が食い違う
15//! 諸元で算定されうる。本モジュールを唯一の出所とし、双方がここを呼ぶ。
16//!
17//! # パネル板厚 `tp` の解決順
18//!
19//! 1. [`Section::panel_thickness`] が入力されていればその値(ダイアフラム補強・
20//!    ダブラープレートによる増厚の明示指定)
21//! 2. 未入力なら柱の断面形状から算出する(H 形=ウェブ厚、角形・円形=板厚)
22//!
23//! # 対象とする接合部
24//!
25//! 節点に次のすべてが揃うとき、その接合部を仕口パネルの対象とする
26//! ([`resolve_panel_joint`])。モデル化と断面検定は同じ判定を通る。
27//!
28//! - 柱(鉛直材)が 1 本以上・はり(水平材)が 1 本以上取り付く
29//! - 取り付く**柱・はりがすべて S/CFT 系**(`StructureKind::is_steel_like`)
30//! - 諸元を解決できる柱が 1 本以上あり、実効体積 `Ve` が正
31//!
32//! 斜材(ブレース等)は資料が接合位置と係数 ζ を定めておらずパネル自由度と
33//! 連成しないため、種別判定の対象にもしない。
34//!
35//! RC/SRC が 1 本でも混じる接合部を除くのは、コンクリートが接合部全体を拘束し、
36//! 鋼部材だけの実効体積による弾性せん断パネルでは挙動を表せないためである。
37//! これらの接合部は剛域と、RC 柱梁接合部・SRC パネルゾーンの断面検定が扱う。
38//!
39//! # 柱が複数取り付く場合
40//!
41//! 実効体積 `Ve` が最小になる柱の諸元を採る。要素の並び順に依存せず決定的で、
42//! かつ剛性・耐力とも安全側になる。`Ve` は `db` に比例するため、柱の選択は
43//! `db` の値によらない。
44//!
45//! # 柱断面ごとの扱い
46//!
47//! 諸元を解決できるのは H 形鋼・角形鋼管・円形鋼管・CFT(角形・円形)である。
48//! 組立 H 形(`SteelBuiltH`)は上下フランジが異なりパネル形状係数 κ の標準式が
49//! 適用できないため対象外とする。
50//!
51//! **モデル化と断面検定で対象範囲が異なる**点に注意する。
52//!
53//! | 柱断面 | モデル化 | 断面検定 |
54//! |---|---|---|
55//! | H 形鋼・角形鋼管・円形鋼管 | 対象 | 対象 |
56//! | CFT(角形・円形) | **対象外** | 対象 |
57//! | RC・SRC・組立 H 形 | 対象外 | 対象外 |
58//!
59//! CFT を諸元解決の対象に残したまま、モデル化からは
60//! [`PanelJoint::has_filled_column`] で除外する。CFT の接合部では充填コンクリートと
61//! 通しダイアフラムがせん断挙動に関与し、鋼管のみの実効体積による弾性せん断パネル
62//! `G・Ve` では剛性を表せないため、接合部を剛節点として扱う。
63
64use crate::geom::{self, MemberAxisClass};
65use crate::ids::{ElemId, NodeId};
66use crate::model::{ElementData, ElementKind, Model, Section};
67use crate::section_shape::SectionShape;
68use crate::structure_kind::member_structure_kind;
69
70/// 仕口パネルに対する部材の向き。
71///
72/// 斜材(鉛直成分が 0.2 と 0.8 の間)は、資料が接合位置と係数 ζ を定めていない
73/// ため、いずれにも分類しない(`None`)。
74#[derive(Clone, Copy, Debug, PartialEq, Eq)]
75pub enum MemberOrientation {
76    /// 柱(鉛直材)。パネルの上下面で接合する。
77    Column,
78    /// はり(水平材)。パネルの左右面(柱フェース)で接合する。
79    Beam,
80}
81
82/// 線材要素の単位材軸ベクトル(i 端 → j 端)。線材以外・退化長さは `None`。
83pub fn member_unit_axis(model: &Model, elem: &ElementData) -> Option<[f64; 3]> {
84    if !matches!(elem.kind, ElementKind::Beam) || elem.nodes.len() < 2 {
85        return None;
86    }
87    let p0 = model.nodes.get(elem.nodes[0].index())?.coord;
88    let p1 = model.nodes.get(elem.nodes[1].index())?.coord;
89    geom::vec3::unit_from(p0, p1)
90}
91
92/// 要素の材軸の鉛直成分から柱・はりを判定する。線材以外・退化長さは `None`。
93pub fn member_orientation(model: &Model, elem: &ElementData) -> Option<MemberOrientation> {
94    let ez = member_unit_axis(model, elem)?[2].abs();
95    match geom::classify_member_ez(ez) {
96        MemberAxisClass::Column => Some(MemberOrientation::Column),
97        MemberAxisClass::Beam => Some(MemberOrientation::Beam),
98        MemberAxisClass::Diagonal => None,
99    }
100}
101
102/// 接合部が占める領域の半寸法 [mm]。
103///
104/// 部材は節点そのものではなく、パネルの面で接合する。その面までの距離が
105/// パネル分のオフセットになる。
106///
107/// - はりは**柱フェース**で接合するため、オフセットは [`Self::column_half`]
108/// - 柱は**梁フェース**で接合するため、オフセットは [`Self::beam_half`]
109///
110/// # 危険断面位置(`RigidZone::face_i` / `face_j`)と分けている理由
111///
112/// フェース距離は現在「接合する直交部材せいの 1/2」だが、これは断面算定の
113/// **既定の**危険断面位置でもあり、将来は任意位置を取りうる。一方パネルの
114/// オフセットは接合部の物理的な寸法そのもので、設計上の評価位置がどこへ動いても
115/// 変わってはいけない。両者を別の量として扱う。
116///
117/// また `face_i` は「概ね直交する全部材の最大せい/2」であり、はりにとっては
118/// 直交する**はり**も候補に入る。本構造体は柱・はりを明示的に区別するため、
119/// はりのオフセットが直交ばりのせいで決まることがない。
120#[derive(Clone, Copy, Debug, Default, PartialEq)]
121pub struct PanelHalfExtent {
122    /// 柱せいの最大値の 1/2 [mm](=はりのオフセット、パネルの水平半寸法)。
123    pub column_half: f64,
124    /// はりせいの最大値の 1/2 [mm](=柱のオフセット、パネルの鉛直半寸法)。
125    pub beam_half: f64,
126}
127
128impl PanelHalfExtent {
129    /// 向き `orientation` の部材が接合する面までのオフセット [mm]。
130    pub fn offset_for(&self, orientation: MemberOrientation) -> f64 {
131        match orientation {
132            MemberOrientation::Beam => self.column_half,
133            MemberOrientation::Column => self.beam_half,
134        }
135    }
136}
137
138/// 節点 `node` に取り付く部材 `members` から接合部の半寸法を集める。
139///
140/// `members` は当該節点へ接続する要素(隣接マップを持つ呼び出し側はそれを渡し、
141/// 持たない側は全要素を渡してよい。節点を端点に持たない要素は読み飛ばす)。
142pub fn panel_half_extent<'a>(
143    model: &Model,
144    node: NodeId,
145    members: impl IntoIterator<Item = &'a ElementData>,
146) -> PanelHalfExtent {
147    let mut extent = PanelHalfExtent::default();
148    for e in members {
149        if !e.nodes.iter().take(2).any(|n| *n == node) {
150            continue;
151        }
152        let Some(orientation) = member_orientation(model, e) else {
153            continue;
154        };
155        let Some(sec) = e.section.and_then(|sid| model.sections.get(sid.index())) else {
156            continue;
157        };
158        let half = sec.depth / 2.0;
159        match orientation {
160            MemberOrientation::Column => extent.column_half = extent.column_half.max(half),
161            MemberOrientation::Beam => extent.beam_half = extent.beam_half.max(half),
162        }
163    }
164    extent
165}
166
167/// パネルの断面区分(形状係数 κ・実効体積 `Ve` の算定式を分ける)。
168#[derive(Clone, Copy, Debug, PartialEq)]
169pub enum PanelShapeKind {
170    /// H 形鋼柱。`bc`: フランジ幅、`tf`: フランジ厚。パネルはウェブ 1 枚。
171    H { bc: f64, tf: f64 },
172    /// 角形鋼管柱(CFT 角形を含む)。`bc`: 柱幅。パネルはウェブ 2 枚。
173    Box { bc: f64 },
174    /// 円形鋼管柱(CFT 円形を含む)。パネルは円筒。
175    Pipe,
176}
177
178/// 柱断面から解決した仕口パネルの諸元。
179#[derive(Clone, Copy, Debug, PartialEq)]
180pub struct PanelGeometry {
181    pub kind: PanelShapeKind,
182    /// 柱せい方向のパネル寸法 `dc` [mm](H 形・角形は板厚中心間、円形は径 − 板厚)。
183    pub dc: f64,
184    /// パネル板厚 `tp` [mm]。
185    pub tp: f64,
186    /// 充填コンクリートを持つ柱(CFT)か。
187    ///
188    /// 断面検定では鋼管部を S 造と同じ式で評価するため区別しないが、仕口パネルの
189    /// **モデル化からは除外する**([`PanelJoint::has_filled_column`])。
190    pub filled: bool,
191}
192
193impl PanelGeometry {
194    /// 柱の断面からパネル諸元を解決する。対象外の断面(RC・SRC・組立 H 形・
195    /// 形状未定義)は `None`。
196    ///
197    /// `tp` は [`Section::panel_thickness`] が正値で入力されていればそちらを
198    /// 優先し、未入力なら断面形状から算出する(モジュール冒頭「パネル板厚 `tp`
199    /// の解決順」)。
200    pub fn from_column(sec: &Section) -> Option<Self> {
201        let (kind, dc, tp, filled) = match sec.shape {
202            Some(SectionShape::SteelH {
203                height,
204                width,
205                web_thick,
206                flange_thick,
207            }) => (
208                PanelShapeKind::H {
209                    bc: width,
210                    tf: flange_thick,
211                },
212                height - flange_thick,
213                web_thick,
214                false,
215            ),
216            Some(SectionShape::SteelBox {
217                height,
218                width,
219                thick,
220                ..
221            }) => (
222                PanelShapeKind::Box { bc: width },
223                height - thick,
224                thick,
225                false,
226            ),
227            Some(SectionShape::CftBox {
228                height,
229                width,
230                thick,
231            }) => (
232                PanelShapeKind::Box { bc: width },
233                height - thick,
234                thick,
235                true,
236            ),
237            Some(SectionShape::SteelPipe { outer_dia, thick }) => {
238                (PanelShapeKind::Pipe, outer_dia - thick, thick, false)
239            }
240            Some(SectionShape::CftPipe { outer_dia, thick }) => {
241                (PanelShapeKind::Pipe, outer_dia - thick, thick, true)
242            }
243            _ => return None,
244        };
245        let tp = match sec.panel_thickness {
246            Some(t) if t > 0.0 => t,
247            _ => tp,
248        };
249        Some(Self {
250            kind,
251            dc,
252            tp,
253            filled,
254        })
255    }
256
257    /// パネルの形状係数 κ(鋼構造接合部設計指針)。
258    ///
259    /// - H 形: `κ = 1/(2/3 + 4・bc・tf/(dc・tp)) + 1/(1 + dc・tp/(6・bc・tf))`
260    /// - 角形: `κ = 1/(2/3 + 2・bc/dc) + 1/(1 + dc/(3・bc))`
261    /// - 円形: `κ = 4/π`
262    pub fn kappa(&self) -> f64 {
263        match self.kind {
264            PanelShapeKind::H { bc, tf } => {
265                1.0 / (2.0 / 3.0 + (4.0 * bc * tf) / (self.dc * self.tp))
266                    + 1.0 / (1.0 + (self.dc * self.tp) / (6.0 * bc * tf))
267            }
268            PanelShapeKind::Box { bc } => {
269                1.0 / (2.0 / 3.0 + 2.0 * bc / self.dc) + 1.0 / (1.0 + self.dc / (3.0 * bc))
270            }
271            PanelShapeKind::Pipe => 4.0 / std::f64::consts::PI,
272        }
273    }
274
275    /// パネルの実効体積 `Ve` [mm³](`db` は梁フランジ板厚中心間距離)。
276    ///
277    /// - H 形: `Ve = dc・db・tp`(ウェブ 1 枚)
278    /// - 角形・円形: `Ve = 2・dc・db・tp`(ウェブ 2 枚相当)
279    ///
280    /// せん断剛性 `Kxp = Kyp = G・Ve` と降伏モーメント
281    /// `pMy = (Ve/κ)・√(1−n²)・Fy/√3` の双方がこの体積を用いる。
282    /// 資料の 6 面体体積 `V = Bx・By・Dz` に対し、H 形柱ではウェブ厚方向の寸法を
283    /// `By = tp` と対応させたものであり、中実断面ではなく板厚分の実効体積となる。
284    pub fn effective_volume(&self, db: f64) -> f64 {
285        let base = self.dc * db * self.tp;
286        match self.kind {
287            PanelShapeKind::H { .. } => base,
288            PanelShapeKind::Box { .. } | PanelShapeKind::Pipe => 2.0 * base,
289        }
290    }
291}
292
293/// 仕口パネルを設ける接合部の諸元。
294#[derive(Clone, Copy, Debug, PartialEq)]
295pub struct PanelJoint {
296    /// 諸元を採った柱の断面区分・`dc`・`tp`。
297    pub geometry: PanelGeometry,
298    /// 梁フランジ板厚中心間距離 `db` [mm](取り付くはりの最大)。
299    pub db: f64,
300    /// 実効体積 `Ve` [mm³]。
301    pub ve: f64,
302    /// 諸元を採った柱の要素 ID(軸力比 `n`・基準強度 `F` の解決に用いる)。
303    pub column: ElemId,
304    /// 取り付く柱に充填断面(CFT)が 1 本でもあるか。
305    ///
306    /// モデル化はこれが `true` の接合部を対象外とする(モジュール冒頭
307    /// 「柱断面ごとの扱い」)。断面検定は鋼管部を S 造と同じ式で評価するため
308    /// 区別しない。
309    pub has_filled_column: bool,
310}
311
312/// 節点 `node` に仕口パネルを設けられるか判定し、諸元を解決する。
313///
314/// `members` は当該節点へ接続する要素(隣接マップを持つ呼び出し側はそれを渡し、
315/// 持たない側は全要素を渡してよい)。
316///
317/// 判定規則はモジュール冒頭「対象とする接合部」のとおりで、モデル化と断面検定の
318/// 双方がこの関数を通る。モデル化はさらに [`PanelJoint::has_filled_column`] が
319/// `false` であることを要求する。
320pub fn resolve_panel_joint<'a>(
321    model: &Model,
322    node: NodeId,
323    members: impl IntoIterator<Item = &'a ElementData>,
324) -> Option<PanelJoint> {
325    let mut columns: Vec<&ElementData> = Vec::new();
326    let mut beams: Vec<&ElementData> = Vec::new();
327    for e in members {
328        if !e.nodes.iter().take(2).any(|n| *n == node) {
329            continue;
330        }
331        // 斜材はパネル自由度と連成しないため、種別判定の対象にもしない。
332        match member_orientation(model, e) {
333            Some(MemberOrientation::Column) => columns.push(e),
334            Some(MemberOrientation::Beam) => beams.push(e),
335            None => {}
336        }
337    }
338    if columns.is_empty() || beams.is_empty() {
339        return None;
340    }
341    // 取り付く柱・はりがすべて S/CFT 系であること。1 本でも RC/SRC が混じる
342    // 接合部は、コンクリートが接合部全体を拘束するため鋼部材だけの実効体積
343    // では挙動を表せない。
344    if columns
345        .iter()
346        .chain(beams.iter())
347        .any(|e| !member_structure_kind(model, e).is_steel_like())
348    {
349        return None;
350    }
351
352    let section_of = |e: &ElementData| e.section.and_then(|sid| model.sections.get(sid.index()));
353    let db = beams
354        .iter()
355        .filter_map(|e| section_of(e))
356        .map(beam_panel_depth)
357        .fold(0.0_f64, f64::max);
358    if db <= 0.0 {
359        return None;
360    }
361
362    let geometry_of = |e: &&ElementData| section_of(e).and_then(PanelGeometry::from_column);
363    let has_filled_column = columns.iter().filter_map(geometry_of).any(|g| g.filled);
364
365    // 柱が複数取り付く場合は Ve が最小になる柱を採る。要素の並び順に依存せず
366    // 決定的で、かつ剛性・耐力とも安全側になる。
367    let (column, geometry, ve) = columns
368        .iter()
369        .filter_map(|e| {
370            let geometry = geometry_of(e)?;
371            let ve = geometry.effective_volume(db);
372            (ve > 0.0).then_some((e.id, geometry, ve))
373        })
374        .min_by(|a, b| a.2.total_cmp(&b.2))?;
375
376    Some(PanelJoint {
377        geometry,
378        db,
379        ve,
380        column,
381        has_filled_column,
382    })
383}
384
385/// 梁のフランジ板厚中心間距離 `db` [mm]。
386///
387/// H 形鋼は `せい − フランジ厚`、それ以外の断面は情報がないため `0.9・せい` で
388/// 近似する(S 造パネルゾーン検定と同じ近似)。
389pub fn beam_panel_depth(sec: &Section) -> f64 {
390    match sec.shape {
391        Some(SectionShape::SteelH { flange_thick, .. }) => sec.depth - flange_thick,
392        _ => 0.9 * sec.depth,
393    }
394}
395
396#[cfg(test)]
397mod tests {
398    use super::*;
399    use crate::ids::SectionId;
400    use crate::model::MaterialCategory;
401    use crate::section_shape::{BarSet, RcRebar, ShearBar};
402
403    fn sec(shape: SectionShape, depth: f64, panel_thickness: Option<f64>) -> Section {
404        Section {
405            id: SectionId(0),
406            name: String::new(),
407            floor: None,
408            area: 1.0e4,
409            iy: 1.0e8,
410            iz: 1.0e8,
411            j: 1.0e8,
412            depth,
413            width: depth,
414            as_y: 0.0,
415            as_z: 0.0,
416            panel_thickness,
417            thickness: None,
418            shape: Some(shape),
419            material: None,
420            rebar_material: None,
421            shear_rebar_material: None,
422            steel_material: None,
423        }
424    }
425
426    /// [`sec`] の id と主材料も指定できる版。
427    fn sec_with_mat(
428        shape: SectionShape,
429        depth: f64,
430        panel_thickness: Option<f64>,
431        id: u32,
432        mat: u32,
433    ) -> Section {
434        Section {
435            id: SectionId(id),
436            material: Some(crate::ids::MaterialId(mat)),
437            ..sec(shape, depth, panel_thickness)
438        }
439    }
440
441    /// H 形柱: dc = せい − フランジ厚、tp = ウェブ厚、Ve = dc・db・tp。
442    #[test]
443    fn test_h_column_geometry() {
444        let s = sec(
445            SectionShape::SteelH {
446                height: 400.0,
447                width: 400.0,
448                web_thick: 13.0,
449                flange_thick: 21.0,
450            },
451            400.0,
452            None,
453        );
454        let g = PanelGeometry::from_column(&s).expect("H 形は対象");
455        assert!((g.dc - (400.0 - 21.0)).abs() < 1e-9);
456        assert!((g.tp - 13.0).abs() < 1e-9);
457        let db = 600.0;
458        assert!((g.effective_volume(db) - g.dc * db * g.tp).abs() < 1e-6);
459    }
460
461    /// 角形・円形はウェブ 2 枚相当で Ve が 2 倍になる。
462    #[test]
463    fn test_box_and_pipe_double_volume() {
464        let b = sec(
465            SectionShape::SteelBox {
466                height: 400.0,
467                width: 400.0,
468                thick: 16.0,
469                corner_r: 0.0,
470            },
471            400.0,
472            None,
473        );
474        let g = PanelGeometry::from_column(&b).expect("角形は対象");
475        let db = 500.0;
476        assert!((g.effective_volume(db) - 2.0 * g.dc * db * g.tp).abs() < 1e-6);
477
478        let p = sec(
479            SectionShape::SteelPipe {
480                outer_dia: 400.0,
481                thick: 12.0,
482            },
483            400.0,
484            None,
485        );
486        let gp = PanelGeometry::from_column(&p).expect("円形は対象");
487        assert!((gp.dc - (400.0 - 12.0)).abs() < 1e-9);
488        assert!((gp.effective_volume(db) - 2.0 * gp.dc * db * gp.tp).abs() < 1e-6);
489        assert!((gp.kappa() - 4.0 / std::f64::consts::PI).abs() < 1e-12);
490    }
491
492    /// `panel_thickness` の明示入力は断面形状から算出した板厚より優先される
493    /// (ダイアフラム補強・ダブラープレートによる増厚)。
494    #[test]
495    fn test_panel_thickness_overrides_shape() {
496        let s = sec(
497            SectionShape::SteelH {
498                height: 400.0,
499                width: 400.0,
500                web_thick: 13.0,
501                flange_thick: 21.0,
502            },
503            400.0,
504            Some(25.0),
505        );
506        let g = PanelGeometry::from_column(&s).expect("H 形は対象");
507        assert!((g.tp - 25.0).abs() < 1e-9, "明示入力が優先される: {}", g.tp);
508
509        // 0 以下は未入力扱いとし、断面形状の値へフォールバックする。
510        let z = sec(
511            SectionShape::SteelH {
512                height: 400.0,
513                width: 400.0,
514                web_thick: 13.0,
515                flange_thick: 21.0,
516            },
517            400.0,
518            Some(0.0),
519        );
520        let gz = PanelGeometry::from_column(&z).expect("H 形は対象");
521        assert!((gz.tp - 13.0).abs() < 1e-9);
522    }
523
524    /// CFT は諸元を解決できるが、モデル化の対象外とする。断面検定は鋼管部を
525    /// S 造と同じ式で評価するため、諸元解決自体は成功させる必要がある。
526    #[test]
527    fn test_cft_resolves_but_is_not_modeling_target() {
528        let cases = [
529            (
530                SectionShape::CftBox {
531                    height: 400.0,
532                    width: 400.0,
533                    thick: 16.0,
534                },
535                PanelShapeKind::Box { bc: 400.0 },
536            ),
537            (
538                SectionShape::CftPipe {
539                    outer_dia: 400.0,
540                    thick: 12.0,
541                },
542                PanelShapeKind::Pipe,
543            ),
544        ];
545        for (shape, kind) in cases {
546            let s = sec(shape, 400.0, None);
547            let g = PanelGeometry::from_column(&s).expect("CFT も諸元は解決できる");
548            assert_eq!(g.kind, kind);
549            assert!(g.filled, "CFT は充填断面");
550            assert!(g.filled, "CFT はモデル化の対象外");
551            // 検定に使う Ve・κ は鋼管と同じ式で求まる。
552            assert!(g.effective_volume(500.0) > 0.0);
553            assert!(g.kappa() > 0.0);
554        }
555    }
556
557    /// CFT でない鋼管(角形・円形)と H 形はモデル化の対象。
558    #[test]
559    fn test_steel_sections_are_modeling_targets() {
560        let shapes = [
561            SectionShape::SteelH {
562                height: 400.0,
563                width: 400.0,
564                web_thick: 13.0,
565                flange_thick: 21.0,
566            },
567            SectionShape::SteelBox {
568                height: 400.0,
569                width: 400.0,
570                thick: 16.0,
571                corner_r: 0.0,
572            },
573            SectionShape::SteelPipe {
574                outer_dia: 400.0,
575                thick: 12.0,
576            },
577        ];
578        for shape in shapes {
579            let s = sec(shape, 400.0, None);
580            let g = PanelGeometry::from_column(&s).expect("S 造は対象");
581            assert!(!g.filled);
582            assert!(!g.filled);
583        }
584    }
585
586    /// CFT と対応する鋼管は、断面検定に使う Ve・κ が同一になる
587    /// (CFT を検定対象から外していないことの裏付け)。
588    #[test]
589    fn test_cft_and_steel_tube_share_check_properties() {
590        let steel = sec(
591            SectionShape::SteelBox {
592                height: 400.0,
593                width: 400.0,
594                thick: 16.0,
595                corner_r: 0.0,
596            },
597            400.0,
598            None,
599        );
600        let cft = sec(
601            SectionShape::CftBox {
602                height: 400.0,
603                width: 400.0,
604                thick: 16.0,
605            },
606            400.0,
607            None,
608        );
609        let (gs, gc) = (
610            PanelGeometry::from_column(&steel).expect("角形"),
611            PanelGeometry::from_column(&cft).expect("CFT 角形"),
612        );
613        assert!((gs.dc - gc.dc).abs() < 1e-12);
614        assert!((gs.tp - gc.tp).abs() < 1e-12);
615        assert!((gs.kappa() - gc.kappa()).abs() < 1e-12);
616        assert!((gs.effective_volume(500.0) - gc.effective_volume(500.0)).abs() < 1e-9);
617    }
618
619    /// RC 柱はパネルの対象外(剛域と RC 柱梁接合部検定で扱う)。
620    #[test]
621    fn test_rc_column_is_not_panel_target() {
622        let s = sec(rc_rect_shape(700.0, 700.0), 700.0, None);
623        assert!(PanelGeometry::from_column(&s).is_none());
624    }
625
626    // ===== 接合部の判定(モデル化・断面検定の共通規則)=====
627
628    fn node(id: u32, coord: [f64; 3]) -> crate::model::Node {
629        crate::model::Node {
630            id: NodeId(id),
631            coord,
632            restraint: crate::dof::Dof6Mask::FREE,
633            mass: None,
634            story: None,
635            support_spring: None,
636        }
637    }
638
639    fn member(id: u32, n0: u32, n1: u32, sec: u32) -> ElementData {
640        ElementData {
641            id: ElemId(id),
642            kind: ElementKind::Beam,
643            nodes: smallvec::smallvec![NodeId(n0), NodeId(n1)],
644            section: Some(crate::ids::SectionId(sec)),
645            local_axis: crate::model::LocalAxis {
646                ref_vector: [0.0, 1.0, 0.0],
647            },
648            end_cond: [
649                crate::model::EndCondition::Fixed,
650                crate::model::EndCondition::Fixed,
651            ],
652            force_regime: crate::model::ForceRegime::Auto,
653            rigid_zone: Default::default(),
654            plastic_zone: None,
655            spring: None,
656        }
657    }
658
659    fn h_col() -> SectionShape {
660        SectionShape::SteelH {
661            height: 400.0,
662            width: 400.0,
663            web_thick: 13.0,
664            flange_thick: 21.0,
665        }
666    }
667
668    fn h_beam() -> SectionShape {
669        SectionShape::SteelH {
670            height: 600.0,
671            width: 200.0,
672            web_thick: 11.0,
673            flange_thick: 17.0,
674        }
675    }
676
677    /// RC 矩形断面。
678    fn rc_rect_shape(b: f64, d: f64) -> SectionShape {
679        let bars = BarSet {
680            dia: 25.0,
681            count: 4,
682            layers: 1,
683        };
684        SectionShape::RcRect {
685            b,
686            d,
687            rebar: RcRebar {
688                main_x: bars.clone(),
689                main_y: bars,
690                cover: 40.0,
691                shear: ShearBar {
692                    dia: 10.0,
693                    pitch: 100.0,
694                    legs: 2,
695                },
696            },
697        }
698    }
699
700    fn mat(id: u32, category: MaterialCategory) -> crate::model::Material {
701        crate::model::Material {
702            strength_factor: None,
703            concrete_class: Default::default(),
704            id: crate::ids::MaterialId(id),
705            name: String::new(),
706            category,
707            young: 205_000.0,
708            poisson: 0.3,
709            density: 0.0,
710            shear: None,
711            fc: None,
712            fy: None,
713        }
714    }
715
716    /// 節点 0 を接合部とする T 型(梁 1 本・柱 1 本)のモデル。
717    /// 断面 0 が梁、断面 1 が柱。材料 0 が鋼材、材料 1 がコンクリート。
718    fn joint_model(beam: SectionShape, beam_depth: f64, col: SectionShape) -> Model {
719        joint_model_with_mats(beam, beam_depth, col, 0, 0)
720    }
721
722    /// [`joint_model`] の材料も指定できる版(0 = 鋼材、1 = コンクリート)。
723    fn joint_model_with_mats(
724        beam: SectionShape,
725        beam_depth: f64,
726        col: SectionShape,
727        beam_mat: u32,
728        col_mat: u32,
729    ) -> Model {
730        Model {
731            nodes: vec![
732                node(0, [0.0, 0.0, 3000.0]),
733                node(1, [6000.0, 0.0, 3000.0]),
734                node(2, [0.0, 0.0, 0.0]),
735            ],
736            // 材料は断面が持つ。断面 0(梁)・断面 1(柱)へそれぞれ割り当てる。
737            sections: vec![
738                sec_with_mat(beam, beam_depth, None, 0, beam_mat),
739                sec_with_mat(col, 400.0, None, 1, col_mat),
740            ],
741            materials: vec![
742                mat(0, MaterialCategory::Steel),
743                mat(1, MaterialCategory::Concrete),
744            ],
745            elements: vec![member(0, 0, 1, 0), member(1, 2, 0, 1)],
746            ..Default::default()
747        }
748    }
749
750    /// 柱・はりがすべて S なら対象になる。
751    #[test]
752    fn test_all_steel_joint_is_target() {
753        let m = joint_model(h_beam(), 600.0, h_col());
754        let joint = resolve_panel_joint(&m, NodeId(0), &m.elements).expect("S 造接合部");
755        assert!((joint.db - (600.0 - 17.0)).abs() < 1e-9);
756        assert_eq!(joint.column, ElemId(1));
757        assert!(!joint.has_filled_column);
758    }
759
760    /// RC 梁が 1 本でもあれば対象外(柱が S でも接合部は RC になる)。
761    /// 判定は材料の区分によるため、断面形状ではなく材料で RC にする。
762    #[test]
763    fn test_rc_beam_disqualifies_joint() {
764        let m = joint_model_with_mats(rc_rect_shape(400.0, 700.0), 700.0, h_col(), 1, 0);
765        assert!(resolve_panel_joint(&m, NodeId(0), &m.elements).is_none());
766    }
767
768    /// RC 柱が 1 本でもあれば対象外。
769    #[test]
770    fn test_rc_column_disqualifies_joint() {
771        let m = joint_model_with_mats(h_beam(), 600.0, rc_rect_shape(400.0, 700.0), 0, 1);
772        assert!(resolve_panel_joint(&m, NodeId(0), &m.elements).is_none());
773    }
774
775    /// H 形の断面でも材料がコンクリートなら対象外になる。
776    /// 断面形状ではなく材料の区分で判定していることの裏付け。
777    #[test]
778    fn test_steel_shape_with_concrete_material_is_excluded() {
779        let m = joint_model_with_mats(h_beam(), 600.0, h_col(), 1, 0);
780        assert!(
781            resolve_panel_joint(&m, NodeId(0), &m.elements).is_none(),
782            "H 形でも材料がコンクリートなら S 造の接合部ではない"
783        );
784    }
785
786    /// 柱だけ・はりだけの節点は対象外。
787    #[test]
788    fn test_column_or_beam_only_node_is_not_target() {
789        let mut m = joint_model(h_beam(), 600.0, h_col());
790        let beam_only = {
791            let mut mm = m.clone();
792            mm.elements.retain(|e| e.id != ElemId(1));
793            mm
794        };
795        assert!(resolve_panel_joint(&beam_only, NodeId(0), &beam_only.elements).is_none());
796        m.elements.retain(|e| e.id != ElemId(0));
797        assert!(resolve_panel_joint(&m, NodeId(0), &m.elements).is_none());
798    }
799
800    /// 柱が複数あれば Ve が最小の柱を採り、要素の並び順に依存しない。
801    #[test]
802    fn test_smallest_ve_column_is_selected() {
803        let thin = SectionShape::SteelH {
804            height: 400.0,
805            width: 400.0,
806            web_thick: 9.0,
807            flange_thick: 21.0,
808        };
809        let build = |upper_first: bool| {
810            let mut m = joint_model(h_beam(), 600.0, h_col());
811            m.nodes.push(node(3, [0.0, 0.0, 6000.0]));
812            m.sections.push(sec(thin.clone(), 400.0, None));
813            let upper = member(2, 0, 3, 2);
814            if upper_first {
815                m.elements.insert(0, upper);
816            } else {
817                m.elements.push(upper);
818            }
819            m
820        };
821        let a = build(true);
822        let b = build(false);
823        let ja = resolve_panel_joint(&a, NodeId(0), &a.elements).expect("接合部");
824        let jb = resolve_panel_joint(&b, NodeId(0), &b.elements).expect("接合部");
825        assert!((ja.geometry.tp - 9.0).abs() < 1e-9, "Ve 最小の柱を採る");
826        assert_eq!(ja.ve, jb.ve, "要素の並び順に依存しない");
827        assert_eq!(ja.column, jb.column);
828    }
829
830    /// CFT 柱の接合部は解決できるが、モデル化からは除外する目印が立つ。
831    #[test]
832    fn test_cft_column_flags_filled() {
833        let m = joint_model(
834            h_beam(),
835            600.0,
836            SectionShape::CftBox {
837                height: 400.0,
838                width: 400.0,
839                thick: 16.0,
840            },
841        );
842        let joint = resolve_panel_joint(&m, NodeId(0), &m.elements).expect("検定の対象にはなる");
843        assert!(joint.has_filled_column, "モデル化からは除外する");
844    }
845
846    // ===== 接合部の半寸法 =====
847
848    /// 半寸法は柱せい・梁せいの 1/2。危険断面位置(face)は参照しない。
849    #[test]
850    fn test_panel_half_extent_uses_member_depths() {
851        let mut m = joint_model(h_beam(), 600.0, h_col());
852        for e in &mut m.elements {
853            e.rigid_zone.face_i = Some(9999.0);
854            e.rigid_zone.face_j = Some(9999.0);
855        }
856        let extent = panel_half_extent(&m, NodeId(0), &m.elements);
857        assert!((extent.column_half - 200.0).abs() < 1e-9);
858        assert!((extent.beam_half - 300.0).abs() < 1e-9);
859        assert!((extent.offset_for(MemberOrientation::Beam) - 200.0).abs() < 1e-9);
860        assert!((extent.offset_for(MemberOrientation::Column) - 300.0).abs() < 1e-9);
861    }
862
863    /// 斜材は柱にもはりにも分類しない(オフセット・ζ が資料で定義されないため)。
864    #[test]
865    fn test_diagonal_member_has_no_orientation() {
866        let mut m = joint_model(h_beam(), 600.0, h_col());
867        m.nodes[1].coord = [4000.0, 0.0, 6000.0];
868        assert!(member_orientation(&m, &m.elements[0]).is_none());
869    }
870
871    /// 梁の db: H 形はせい − フランジ厚、それ以外は 0.9・せい。
872    #[test]
873    fn test_beam_panel_depth() {
874        let h = sec(
875            SectionShape::SteelH {
876                height: 600.0,
877                width: 200.0,
878                web_thick: 11.0,
879                flange_thick: 17.0,
880            },
881            600.0,
882            None,
883        );
884        assert!((beam_panel_depth(&h) - (600.0 - 17.0)).abs() < 1e-9);
885
886        let b = sec(
887            SectionShape::SteelBox {
888                height: 500.0,
889                width: 300.0,
890                thick: 12.0,
891                corner_r: 0.0,
892            },
893            500.0,
894            None,
895        );
896        assert!((beam_panel_depth(&b) - 0.9 * 500.0).abs() < 1e-9);
897    }
898}