Skip to main content

squid_n_core/
adjacency.rs

1//! 節点 → 接続する線材要素の隣接関係。
2//!
3//! 剛域の自動算定・仕口パネルの生成・座屈長さ係数の剛度比・モデル化図の描画は、
4//! いずれも「この節点にどの線材が取り付くか」を繰り返し引く。節点ごとに全要素を
5//! 走査すると `O(節点数 × 要素数)` になるため、隣接関係を 1 回だけ構築して共有する。
6//!
7//! # 分類前の素の隣接関係だけを持つ
8//!
9//! 用途ごとに必要な分類が異なるため、本モジュールは**分類しない**。
10//!
11//! - 剛域は「概ね直交する部材」(材軸の内積で判定)
12//! - 剛度比 `G` は「柱・梁」(材軸の鉛直成分で判定)
13//! - 仕口パネルは「柱・はり・斜材」(同上、ただし斜材を独立に扱う)
14//!
15//! 分類済みで持つと、どの分類軸で持つかを 1 つに決めねばならず、結局どこかが
16//! 自前で再分類する。共有できるのは構築コストなので、そこだけを共有する。
17//!
18//! # 対象は線材のみ
19//!
20//! [`ElementKind::Beam`] の 2 節点要素だけを収める。耐震壁・シェル等が混ざると、
21//! 剛域の直交材探索へ壁の名目せいが紛れ込む(「耐震壁周辺の柱・梁の剛域は
22//! 考慮しない」という方針に反する)。
23
24use crate::ids::NodeId;
25use crate::model::{ElementData, ElementKind, Model};
26use std::collections::HashMap;
27
28/// 節点 → その節点に接続する線材要素の添字。
29#[derive(Clone, Debug, Default)]
30pub struct NodeAdjacency {
31    by_node: HashMap<usize, Vec<usize>>,
32}
33
34impl NodeAdjacency {
35    /// モデル全体から 1 回だけ構築する(`O(要素数)`)。
36    pub fn build(model: &Model) -> Self {
37        let mut by_node: HashMap<usize, Vec<usize>> = HashMap::new();
38        for (ei, e) in model.elements.iter().enumerate() {
39            if !matches!(e.kind, ElementKind::Beam) || e.nodes.len() < 2 {
40                continue;
41            }
42            // 中間節点を持つ要素でも、隣接するのは両端だけとする。
43            for n in e.nodes.iter().take(2) {
44                let list = by_node.entry(n.index()).or_default();
45                if !list.contains(&ei) {
46                    list.push(ei);
47                }
48            }
49        }
50        Self { by_node }
51    }
52
53    /// 節点 `node` に接続する線材要素の添字(接続がなければ空)。
54    pub fn indices_at(&self, node: NodeId) -> &[usize] {
55        self.by_node
56            .get(&node.index())
57            .map(|v| v.as_slice())
58            .unwrap_or(&[])
59    }
60
61    /// 節点 `node` に接続する線材要素(添字を解決したもの)。
62    pub fn elements_at<'a>(
63        &'a self,
64        model: &'a Model,
65        node: NodeId,
66    ) -> impl Iterator<Item = &'a ElementData> + 'a {
67        self.indices_at(node)
68            .iter()
69            .filter_map(move |&ei| model.elements.get(ei))
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::*;
76    use crate::dof::Dof6Mask;
77    use crate::ids::{ElemId, SectionId};
78    use crate::model::{EndCondition, ForceRegime, LocalAxis, Node};
79
80    fn node(id: u32) -> Node {
81        Node {
82            id: NodeId(id),
83            coord: [id as f64 * 1000.0, 0.0, 0.0],
84            restraint: Dof6Mask::FREE,
85            mass: None,
86            story: None,
87            support_spring: None,
88        }
89    }
90
91    fn elem(id: u32, kind: ElementKind, nodes: &[u32]) -> ElementData {
92        ElementData {
93            id: ElemId(id),
94            kind,
95            nodes: nodes.iter().map(|&n| NodeId(n)).collect(),
96            section: Some(SectionId(0)),
97            local_axis: LocalAxis {
98                ref_vector: [0.0, 1.0, 0.0],
99            },
100            end_cond: [EndCondition::Fixed, EndCondition::Fixed],
101            force_regime: ForceRegime::Auto,
102            rigid_zone: Default::default(),
103            plastic_zone: None,
104            spring: None,
105        }
106    }
107
108    fn model() -> Model {
109        Model {
110            nodes: (0..4).map(node).collect(),
111            elements: vec![
112                elem(0, ElementKind::Beam, &[0, 1]),
113                elem(1, ElementKind::Beam, &[1, 2]),
114                // 壁は線材ではないため隣接に含めない。
115                elem(2, ElementKind::Wall, &[0, 1, 2, 3]),
116                // 1 節点しか持たない要素も含めない。
117                elem(3, ElementKind::Beam, &[3]),
118            ],
119            ..Default::default()
120        }
121    }
122
123    /// 線材だけが隣接に入り、共有節点では両方が引ける。
124    #[test]
125    fn test_collects_line_members_only() {
126        let m = model();
127        let adj = NodeAdjacency::build(&m);
128        assert_eq!(adj.indices_at(NodeId(0)), &[0]);
129        assert_eq!(adj.indices_at(NodeId(1)), &[0, 1], "共有節点は 2 本");
130        assert_eq!(adj.indices_at(NodeId(2)), &[1]);
131        assert!(
132            adj.indices_at(NodeId(3)).is_empty(),
133            "壁・単節点要素は入らない"
134        );
135    }
136
137    /// 接続がない節点・範囲外の節点は空を返す。
138    #[test]
139    fn test_unknown_node_is_empty() {
140        let m = model();
141        let adj = NodeAdjacency::build(&m);
142        assert!(adj.indices_at(NodeId(99)).is_empty());
143        assert_eq!(adj.elements_at(&m, NodeId(99)).count(), 0);
144    }
145
146    /// 要素の参照を直接引ける。
147    #[test]
148    fn test_elements_at_resolves_references() {
149        let m = model();
150        let adj = NodeAdjacency::build(&m);
151        let ids: Vec<_> = adj.elements_at(&m, NodeId(1)).map(|e| e.id).collect();
152        assert_eq!(ids, vec![ElemId(0), ElemId(1)]);
153    }
154}