Skip to main content

squid_n_core/
dof.rs

1use crate::model::Model;
2
3#[derive(Clone, Copy, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
4pub enum Dof {
5    Ux = 0,
6    Uy = 1,
7    Uz = 2,
8    Rx = 3,
9    Ry = 4,
10    Rz = 5,
11}
12
13pub const DOF_PER_NODE: usize = 6;
14
15/// 仕口パネルが設けられた節点が追加で持つ自由度の数。
16///
17/// せん断変形角 `γX`・`γY`(基準座標系。X'-Z' 平面と Y'-Z' 平面のパネルせん断
18/// 変形角)の 2 個。標準の 6 自由度とは別枠で、[`DofMap`] のグローバル自由度
19/// 空間の末尾(`節点数 × DOF_PER_NODE` の後ろ)へ払い出す。
20///
21/// この置き方にすることで、`節点番号 × DOF_PER_NODE + 成分` でグローバル自由度を
22/// 求める既存コードは追加自由度に一切触れず、パネルを持たないモデルでは追加
23/// 自由度が 1 つも払い出されないため剛性行列・独立自由度数が従来と完全に一致する。
24pub const PANEL_DOF_PER_NODE: usize = 2;
25
26#[derive(Clone, Copy, Default, PartialEq, Eq, Debug, serde::Serialize, serde::Deserialize)]
27pub struct Dof6Mask(pub u8);
28
29impl Dof6Mask {
30    pub const FREE: Self = Dof6Mask(0b000000);
31    pub const FIXED: Self = Dof6Mask(0b111111);
32    pub const PINNED: Self = Dof6Mask(0b000111);
33    pub fn is_fixed(self, d: Dof) -> bool {
34        self.0 & (1 << d as u8) != 0
35    }
36    pub fn set_fixed(&mut self, d: Dof) {
37        self.0 |= 1 << d as u8;
38    }
39    /// 指定自由度の拘束を解除(ビットを下ろす)。
40    pub fn set_free(&mut self, d: Dof) {
41        self.0 &= !(1 << d as u8);
42    }
43    /// 指定自由度の拘束を ON/OFF で設定する。
44    pub fn set(&mut self, d: Dof, fixed: bool) {
45        if fixed {
46            self.set_fixed(d);
47        } else {
48            self.set_free(d);
49        }
50    }
51}
52
53pub type GlobalDof = usize;
54
55/// 解析自由度を持つ節点(**構造節点**)の判定を節点 index ごとの真偽で返す。
56///
57/// 構造節点 = 要素(部材)が接続する節点、または拘束(剛床・剛リンク・MPC)の
58/// マスター節点。どちらでもない節点(二次部材(小梁・間柱)の支持点・床境界専用の
59/// 幾何節点など)は剛性が一切組み上がらず零剛性の自由度=特異行列の原因になるため、
60/// [`DofMap::build`] が全自由度を不活性にする(解析上は存在しない扱い)。
61///
62/// 解析([`DofMap::build`])と表示(解析対象外の節点を描かない・剛床スレーブから
63/// 除く)で同じ規則を使うため、判定をここへ一元化する。
64pub fn structural_nodes(model: &Model) -> Vec<bool> {
65    let mut structural = vec![false; model.nodes.len()];
66    for e in &model.elements {
67        for n in &e.nodes {
68            if let Some(slot) = structural.get_mut(n.index()) {
69                *slot = true;
70            }
71        }
72    }
73    for c in &model.constraints {
74        use crate::model::Constraint;
75        match c {
76            Constraint::RigidDiaphragm { master, .. } | Constraint::RigidLink { master, .. } => {
77                if let Some(slot) = structural.get_mut(master.index()) {
78                    *slot = true;
79                }
80            }
81            // MPC は `master` フィールドがスレーブ節点、`terms` がマスター側。
82            Constraint::Mpc { terms, .. } => {
83                for (n, _, _) in terms {
84                    if let Some(slot) = structural.get_mut(n.index()) {
85                        *slot = true;
86                    }
87                }
88            }
89        }
90    }
91    structural
92}
93
94/// 仕口パネル(`ElementKind::PanelZone`)が設けられた節点を、節点 index ごとの
95/// 真偽で返す。パネル要素の先頭節点(`nodes[0]`)が接合部の節点である。
96///
97/// 該当する節点は [`PANEL_DOF_PER_NODE`] 個の追加自由度(せん断変形角)を持つ。
98pub fn panel_zone_nodes(model: &Model) -> Vec<bool> {
99    let mut is_panel = vec![false; model.nodes.len()];
100    for e in &model.elements {
101        if !matches!(e.kind, crate::model::ElementKind::PanelZone) {
102            continue;
103        }
104        if let Some(n) = e.nodes.first() {
105            if let Some(slot) = is_panel.get_mut(n.index()) {
106                *slot = true;
107            }
108        }
109    }
110    is_panel
111}
112
113#[derive(Clone, Debug, Default)]
114pub struct DofMap {
115    active_of: Vec<Option<u32>>,
116    global_of: Vec<GlobalDof>,
117    n_active: usize,
118    /// 節点 index → 仕口パネル自由度のスロット番号。パネルを持たない節点は `None`。
119    /// スロット `s` の `d` 番目のグローバル自由度は
120    /// `n_node_global + s * PANEL_DOF_PER_NODE + d`。
121    panel_slot_of: Vec<Option<u32>>,
122    /// スロット番号 → 節点 index([`Self::panel_slot_of`] の逆写像)。
123    panel_node_of: Vec<u32>,
124    /// 標準自由度(節点 × 6)の総数。仕口パネル自由度のグローバル番号はここから始まる。
125    n_node_global: usize,
126}
127
128impl DofMap {
129    pub fn build(model: &Model) -> Self {
130        // 構造節点(解析自由度を持つ節点)以外は全自由度を不活性にする
131        // (解析上は存在しない扱い。変位は 0 で出力され、そこへの節点荷重は
132        // 無視される。荷重は同期側で主架構へ変換する規約)。判定規則は
133        // [`structural_nodes`] を参照。
134        let structural = structural_nodes(model);
135        let is_panel = panel_zone_nodes(model);
136
137        // 仕口パネル自由度は標準自由度の後ろへ連続して並べる。パネルが 1 つも
138        // なければ `n_panel_slots == 0` となり、以降は従来と完全に同一の写像になる。
139        let n_node_global = model.nodes.len() * DOF_PER_NODE;
140        let mut panel_slot_of = vec![None; model.nodes.len()];
141        let mut panel_node_of = Vec::new();
142        for (ni, &p) in is_panel.iter().enumerate() {
143            // 構造節点でない節点にパネルは付かない(パネル要素が接続していれば
144            // その節点は必ず構造節点になるため、通常この分岐は成立しない)。
145            if p && structural[ni] {
146                panel_slot_of[ni] = Some(panel_node_of.len() as u32);
147                panel_node_of.push(ni as u32);
148            }
149        }
150
151        let n_global = n_node_global + panel_node_of.len() * PANEL_DOF_PER_NODE;
152        let mut active_of = vec![None; n_global];
153        let mut global_of = Vec::new();
154        let mut counter = 0u32;
155        for (ni, node) in model.nodes.iter().enumerate() {
156            if !structural[ni] {
157                continue;
158            }
159            for d in 0..DOF_PER_NODE {
160                let g = ni * DOF_PER_NODE + d;
161                let dof = match d {
162                    0 => Dof::Ux,
163                    1 => Dof::Uy,
164                    2 => Dof::Uz,
165                    3 => Dof::Rx,
166                    4 => Dof::Ry,
167                    _ => Dof::Rz,
168                };
169                if !node.restraint.is_fixed(dof) {
170                    active_of[g] = Some(counter);
171                    global_of.push(g);
172                    counter += 1;
173                }
174            }
175        }
176        // 仕口パネル自由度は `Node::restraint`(6 成分のマスク)の対象外であり、
177        // 拘束する手段を持たない。パネル要素が必ず剛性 `Kxp`・`Kyp` を与えるため
178        // 零剛性にはならず、常に活性としてよい。
179        for (ni, slot) in panel_slot_of.iter().enumerate() {
180            let Some(s) = slot else { continue };
181            let _ = ni;
182            for d in 0..PANEL_DOF_PER_NODE {
183                let g = n_node_global + *s as usize * PANEL_DOF_PER_NODE + d;
184                active_of[g] = Some(counter);
185                global_of.push(g);
186                counter += 1;
187            }
188        }
189        DofMap {
190            active_of,
191            global_of,
192            n_active: counter as usize,
193            panel_slot_of,
194            panel_node_of,
195            n_node_global,
196        }
197    }
198
199    pub fn n_active(&self) -> usize {
200        self.n_active
201    }
202    pub fn active(&self, g: GlobalDof) -> Option<u32> {
203        self.active_of.get(g).copied().flatten()
204    }
205
206    /// 自由 DOF 空間のベクトル(`active` 添字順。従属自由度は `expand_u` 済み)を
207    /// 節点×6 成分の配列へ展開する。拘束・非構造自由度は 0 のまま。
208    ///
209    /// 静的解析の変位・時刻歴の節点変位・固有モード形状の散布で同一の展開が
210    /// 必要になるため、単一実装としてここに置く(各ソルバでの手書きコピーの
211    /// 再発防止)。
212    pub fn expand_to_nodes(&self, u_free: &[f64], n_nodes: usize) -> Vec<[f64; 6]> {
213        let mut out = vec![[0.0f64; 6]; n_nodes];
214        for (ni, d6) in out.iter_mut().enumerate() {
215            for (d, slot) in d6.iter_mut().enumerate() {
216                if let Some(a) = self.active(ni * DOF_PER_NODE + d) {
217                    *slot = u_free[a as usize];
218                }
219            }
220        }
221        out
222    }
223    pub fn global(&self, a: u32) -> GlobalDof {
224        self.global_of[a as usize]
225    }
226
227    /// 節点 `node_idx` の仕口パネル自由度(`d = 0` が γX、`1` が γY)の独立自由度番号。
228    /// パネルを持たない節点・範囲外は `None`。
229    pub fn panel_dof(&self, node_idx: usize, d: usize) -> Option<u32> {
230        let slot = (*self.panel_slot_of.get(node_idx)?)? as usize;
231        if d >= PANEL_DOF_PER_NODE {
232            return None;
233        }
234        self.active(self.n_node_global + slot * PANEL_DOF_PER_NODE + d)
235    }
236
237    /// 節点 `node_idx` に仕口パネル自由度が払い出されているか。
238    pub fn has_panel_dof(&self, node_idx: usize) -> bool {
239        self.panel_slot_of
240            .get(node_idx)
241            .is_some_and(|s| s.is_some())
242    }
243
244    /// グローバル自由度 `g` が標準自由度(節点 × 6)の範囲にあるか。
245    /// `false` は仕口パネルの追加自由度を指す(`g / DOF_PER_NODE` で節点番号へ
246    /// 換算してはならない)。
247    pub fn is_node_dof(&self, g: GlobalDof) -> bool {
248        g < self.n_node_global
249    }
250
251    /// グローバル自由度 `g` が仕口パネルの追加自由度であれば
252    /// `(節点 index, 成分(0 = γX, 1 = γY))` を返す。標準自由度・範囲外は `None`。
253    pub fn panel_dof_ref(&self, g: GlobalDof) -> Option<(usize, usize)> {
254        let off = g.checked_sub(self.n_node_global)?;
255        let slot = off / PANEL_DOF_PER_NODE;
256        let d = off % PANEL_DOF_PER_NODE;
257        Some((*self.panel_node_of.get(slot)? as usize, d))
258    }
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264    use crate::dof::Dof6Mask;
265    use crate::ids::*;
266    use crate::model::*;
267
268    fn make_model_with_restraints(restraints: &[Dof6Mask]) -> Model {
269        let nodes: Vec<Node> = restraints
270            .iter()
271            .enumerate()
272            .map(|(i, &r)| Node {
273                id: NodeId(i as u32),
274                coord: [i as f64 * 1000.0, 0.0, 0.0],
275                restraint: r,
276                mass: None,
277                story: None,
278                support_spring: None,
279            })
280            .collect();
281        // 要素が接続しない節点は解析自由度から除外されるため、拘束マスキングの
282        // 検証用に全節点を鎖状の梁要素でつなぐ(1 節点のみの場合は自己参照でよい)。
283        let elements: Vec<ElementData> = (0..restraints.len().max(2) - 1)
284            .map(|i| ElementData {
285                id: ElemId(i as u32),
286                kind: ElementKind::Beam,
287                nodes: [
288                    NodeId(i as u32),
289                    NodeId(((i + 1) % restraints.len()) as u32),
290                ]
291                .into_iter()
292                .collect(),
293                section: None,
294                local_axis: LocalAxis {
295                    ref_vector: [0.0, 0.0, 1.0],
296                },
297                end_cond: [EndCondition::Fixed, EndCondition::Fixed],
298                force_regime: ForceRegime::Auto,
299                rigid_zone: Default::default(),
300                plastic_zone: None,
301                spring: None,
302            })
303            .collect();
304        Model {
305            nodes,
306            elements,
307            ..Default::default()
308        }
309    }
310
311    #[test]
312    fn test_set_free_and_set_toggle() {
313        let mut m = Dof6Mask::FIXED;
314        m.set_free(Dof::Ux);
315        assert!(!m.is_fixed(Dof::Ux));
316        assert!(m.is_fixed(Dof::Uy));
317        // set(false) は解除、set(true) は拘束
318        m.set(Dof::Uy, false);
319        assert!(!m.is_fixed(Dof::Uy));
320        m.set(Dof::Ux, true);
321        assert!(m.is_fixed(Dof::Ux));
322        // PINNED から Rz を拘束すると並進3 + Rz が拘束される
323        let mut p = Dof6Mask::PINNED;
324        p.set(Dof::Rz, true);
325        assert!(p.is_fixed(Dof::Ux) && p.is_fixed(Dof::Uy) && p.is_fixed(Dof::Uz));
326        assert!(p.is_fixed(Dof::Rz));
327        assert!(!p.is_fixed(Dof::Rx) && !p.is_fixed(Dof::Ry));
328    }
329
330    #[test]
331    fn test_all_free() {
332        let model = make_model_with_restraints(&[Dof6Mask::FREE; 3]);
333        let map = DofMap::build(&model);
334        assert_eq!(map.n_active(), 18);
335    }
336
337    /// 仕口パネルが 1 つもないモデルでは追加自由度が払い出されず、独立自由度数・
338    /// 写像とも従来(節点 × 6)と完全に一致する(既存モデルの回帰防止)。
339    #[test]
340    fn test_no_panel_keeps_dof_map_identical() {
341        let model = make_model_with_restraints(&[Dof6Mask::FREE; 3]);
342        let map = DofMap::build(&model);
343        assert_eq!(map.n_active(), 3 * DOF_PER_NODE);
344        for ni in 0..3 {
345            assert!(!map.has_panel_dof(ni));
346            assert!(map.panel_dof(ni, 0).is_none());
347        }
348        // 全独立自由度が標準自由度(節点 × 6)の範囲に収まる。
349        for a in 0..map.n_active() {
350            assert!(map.is_node_dof(map.global(a as u32)));
351        }
352    }
353
354    /// 仕口パネル要素がある節点には γX・γY の 2 自由度が追加され、標準自由度の
355    /// 後ろへ連続して並ぶ。標準自由度側の番号は従来と変わらない。
356    #[test]
357    fn test_panel_node_gets_two_extra_dofs() {
358        let mut model = make_model_with_restraints(&[Dof6Mask::FREE; 3]);
359        let base = DofMap::build(&model).n_active();
360
361        // 節点 1 に仕口パネルを設ける(先頭節点が接合部の節点)。
362        model.elements.push(ElementData {
363            id: ElemId(100),
364            kind: ElementKind::PanelZone,
365            nodes: [NodeId(1), NodeId(0), NodeId(2)].into_iter().collect(),
366            section: None,
367            local_axis: LocalAxis {
368                ref_vector: [0.0, 0.0, 1.0],
369            },
370            end_cond: [EndCondition::Fixed, EndCondition::Fixed],
371            force_regime: ForceRegime::Auto,
372            rigid_zone: Default::default(),
373            plastic_zone: None,
374            spring: None,
375        });
376
377        let map = DofMap::build(&model);
378        assert_eq!(map.n_active(), base + PANEL_DOF_PER_NODE);
379        assert!(map.has_panel_dof(1));
380        assert!(!map.has_panel_dof(0) && !map.has_panel_dof(2));
381
382        // パネル自由度は標準自由度の後ろ(=既存の番号を押し出さない)。
383        let gx = map.panel_dof(1, 0).expect("γX");
384        let gy = map.panel_dof(1, 1).expect("γY");
385        assert_eq!(gy, gx + 1);
386        assert!(gx as usize >= base);
387        assert!(map.panel_dof(1, 2).is_none(), "成分は 2 個まで");
388
389        // 逆写像で節点・成分が引ける(増分解析の特異診断が使う)。
390        assert!(!map.is_node_dof(map.global(gx)));
391        assert_eq!(map.panel_dof_ref(map.global(gx)), Some((1, 0)));
392        assert_eq!(map.panel_dof_ref(map.global(gy)), Some((1, 1)));
393    }
394
395    #[test]
396    fn test_one_fixed() {
397        let model = make_model_with_restraints(&[Dof6Mask::FREE, Dof6Mask::FIXED, Dof6Mask::FREE]);
398        let map = DofMap::build(&model);
399        assert_eq!(map.n_active(), 12);
400    }
401
402    #[test]
403    fn test_all_fixed() {
404        let model = make_model_with_restraints(&[Dof6Mask::FIXED]);
405        let map = DofMap::build(&model);
406        assert_eq!(map.n_active(), 0);
407    }
408
409    #[test]
410    fn test_pinned() {
411        let model = make_model_with_restraints(&[Dof6Mask::PINNED]);
412        let map = DofMap::build(&model);
413        assert_eq!(map.n_active(), 3);
414    }
415
416    #[test]
417    fn test_mixed() {
418        let model = make_model_with_restraints(&[Dof6Mask::FREE, Dof6Mask::PINNED]);
419        let map = DofMap::build(&model);
420        assert_eq!(map.n_active(), 6 + 3);
421    }
422
423    /// 要素が接続しない節点(二次部材の支持点など)は解析自由度から除外される。
424    /// 拘束(剛床)のマスター節点は要素非接続でも自由度を持つ。
425    #[test]
426    fn test_unreferenced_node_is_inactive() {
427        let mut model = make_model_with_restraints(&[Dof6Mask::FREE, Dof6Mask::FREE]);
428        // 要素が接続しない自由節点を追加 → 自由度は増えない。
429        model.nodes.push(Node {
430            id: NodeId(2),
431            coord: [500.0, 0.0, 0.0],
432            restraint: Dof6Mask::FREE,
433            mass: None,
434            story: None,
435            support_spring: None,
436        });
437        let map = DofMap::build(&model);
438        assert_eq!(map.n_active(), 12, "孤立自由節点は自由度を持たない");
439        assert!(map.active(2 * DOF_PER_NODE).is_none());
440
441        // 剛床マスターに指定すると自由度を持つ(拘束されない DOF 分)。
442        model.constraints.push(Constraint::rigid_diaphragm(
443            StoryId(0),
444            NodeId(2),
445            vec![NodeId(0), NodeId(1)],
446        ));
447        let map = DofMap::build(&model);
448        assert_eq!(map.n_active(), 18, "拘束マスターは自由度を持つ");
449    }
450}