Skip to main content

squid_n_core/model/
story.rs

1//! 階・層関連の型。
2//!
3//! **階([`Story`])は床であり、層([`Layer`])は隣り合う 2 つの床の間である。**
4//! [`Model::stories`] は基部の床から屋根の床までの床レベル列で、
5//! **先頭は必ず基部**(`stories[0].elevation == ` [`Model::base_elevation`])。
6//! これを不変条件とし、階生成(`squid_n_load::story_gen`)が必ず成立させる。
7//! したがって層の数は階の数より 1 つ少ない。
8//!
9//! 法規上の「i 階」(層間変形角・層せん断力・剛性率・偏心率が対象とする範囲)は
10//! 層のことであり、[`Model::layers`] が唯一の情報源である。層を数える処理は
11//! [`Model::stories`] を直接走査してはならない(1 つ多く数えてしまう)。
12//!
13//! 層と床の対応は実務の慣行に合わせる([`Layer`] 参照)。層の名前は**下端床**、
14//! 重量・所属節点・階種別は**上端床**から採る。層 i の重量 Wi が上端床の重量なのは、
15//! `Q1 = C1·(W1+W2+…)` が成り立つためである(基部の重量は地盤が直接受けるので
16//! 最下層のせん断力に寄与しない)。
17//!
18//! **階と剛床も別の概念である。** 階が剛床を持たないことも、1 つの階が複数の
19//! 剛床を持つこと(段差床)もある。そのため剛床は階の一部としてではなく、拘束
20//! ([`Constraint::RigidDiaphragm`])として単一の情報源に保持する。
21//! 階から剛床を引くときは [`Model::diaphragms_of`] を使う。
22//!
23//! - [`DiaphragmRef`] — 階に属する剛床の参照ビュー。
24//! - [`StoryStructure`] — 階の主要構造種別。
25//! - [`StoryLevelKind`] — 層の種別(一般/PH/地下)。
26//! - [`Story`] — 階(床)の定義。
27//! - [`Layer`] — 層(階と階の間)。[`Model::layers`] が組み立てる導出値。
28
29use super::*;
30
31/// 剛床のレベル許容差 [mm]。剛床のスレーブ節点は「階のレベルからこの範囲内に
32/// ある節点」とする([`Model::on_diaphragm_level`])。
33///
34/// 階への帰属([`Model::story_of_elevation`])が**区間**であるのに対し、剛床への
35/// 帰属は**床面**である。中間高さの節点(柱の分割点・階高の途中に取り付く梁)は
36/// 階には属するが剛床には入らない。面内剛体として拘束してよいのは同一床面の
37/// 節点だけであり、中間節点を含めると存在しない水平剛性が生じるためである。
38pub const DIAPHRAGM_LEVEL_TOL_MM: f64 = 1.0;
39
40/// 階名が与えられていないときの既定の階名(**床基準**)。
41///
42/// 階は床そのものである([`Story::elevation`] はその階が代表する床のレベル)ため、
43/// 階名も床の呼び名に合わせる。下から `index` 番目(0 始まり)の階は
44/// `index == 0` が基部の床であり、`1F`・`2F` … と付ける。
45///
46/// 最上階も `RF` とはせず数字で通す。モデルの最上レベルが本当に屋根なのかは
47/// モデルからは決められず(塔屋の床であることも、あとで上へ階を足すこともある)、
48/// 確定していないものを屋根と名乗らせないためである。屋根であれば利用者が
49/// `RF` へ付け替える。
50///
51/// ST-Bridge の `StbStory` も床基準(`1F` の `height` が GL)であるため、
52/// 取り込んだモデルとアプリ内で作ったモデルで階名の意味が一致する。
53pub fn default_story_name(index: usize) -> String {
54    format!("{}F", index + 1)
55}
56
57/// 階に属する剛床の参照ビュー([`Constraint::RigidDiaphragm`] の内容)。
58///
59/// 剛床の実体は拘束として保持されるため、階から剛床を辿る側は本ビューを介する
60/// ([`Model::diaphragms_of`])。
61#[derive(Clone, Copy, Debug, PartialEq)]
62pub struct DiaphragmRef<'a> {
63    pub story: StoryId,
64    pub master: NodeId,
65    pub slaves: &'a [NodeId],
66    /// この剛床が負担する地震用重量 [N]。多剛床の階では層の水平力 Pi を
67    /// 剛床ごとの重量比で分配するために用いる(多剛床の設計用せん断力。
68    /// 令88条・昭55建告1793号)。None は未算定(階に単一剛床なら層重量全量)。
69    pub weight: Option<f64>,
70    /// 副剛床の層せん断力係数 Ci の直接入力(令88条・昭55建告1793号の
71    /// 層せん断力係数)。Some の剛床は主系統の Ai 分布から
72    /// 除外され、水平力 = ci_override × 剛床重量(等価震度扱い。上階に同一系統の
73    /// 剛床が積み上がらない副剛床を想定)として作用する。None は主系統(Ai 分布)。
74    pub ci_override: Option<f64>,
75}
76
77/// 動的解析(固有値・時刻歴・精算周期)の質量モデルの方式。
78///
79/// 階の自動生成が剛床マスター節点へ与える質点質量([`super::Node::mass`])の
80/// 算定方法と、解析側の全体質量行列の組立方法(部材密度による分布質量を
81/// 含めるか)の両方を規定する。生成と組立で方式が食い違うと自重の二重計上や
82/// 質量欠落が起きるため、モデル自身([`super::Model::mass_method`])が
83/// 単一情報源として保持し、双方がこれを参照する。
84#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
85pub enum MassMethod {
86    /// 補正質点方式(既定): 部材密度による分布質量に加え、剛床マスターへ
87    /// 「地震用重量のうち分布質量として計上されない分」(床・仕上げ・積載・
88    /// 二次部材・雑壁など)を補正質点として与える。階の合計質量が地震用重量に
89    /// 一致し、部材の分布質量による鉛直・局部の振動モードも保たれる。
90    #[default]
91    CorrectedLumped,
92    /// 質点のみ方式: 質量は節点質量(剛床マスターへ与えた地震用重量の質点等)
93    /// のみを用い、部材密度による分布質量は質量行列に算入しない
94    /// (実務の水平質点系モデル化。鉛直方向・局部の振動モードは表現されない)。
95    LumpedOnly,
96}
97
98/// 階の主要構造種別。設計用一次固有周期の略算式 T=h(0.02+0.01α) の
99/// α(柱梁の大部分が鉄骨造である階の高さ比)の算定に用いる(令88条・告示1793号)。
100///
101/// 値は階に属する柱・梁の構造種別から自動判定する(準備計算の階生成。
102/// [`StoryStructure::of_structure_kind`])ため、利用者は入力しない。
103#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
104pub enum StoryStructure {
105    #[default]
106    Rc,
107    S,
108    Src,
109}
110
111impl StoryStructure {
112    /// 部材の構造種別を階の構造種別へ畳み込む。
113    ///
114    /// CFT は SRC へ寄せる。略算周期 T = h(0.02 + 0.01α) は S の階が増えるほど
115    /// T が長く Rt が小さくなって地震力が下がるため、鋼管にコンクリートを充填した
116    /// CFT を S へ寄せないのが安全側になる。
117    pub fn of_structure_kind(kind: crate::structure_kind::StructureKind) -> Self {
118        use crate::structure_kind::StructureKind;
119        match kind {
120            StructureKind::Rc => StoryStructure::Rc,
121            StructureKind::S => StoryStructure::S,
122            StructureKind::Src | StructureKind::Cft => StoryStructure::Src,
123        }
124    }
125
126    /// 種別ごとの部材本数から階の主要構造種別を決める。最多の種別を採用し、
127    /// 同数の場合は RC → SRC → S の順で優先する。
128    ///
129    /// 略算周期 T = h(0.02 + 0.01α) は S の階が増えるほど T が長く、Rt が
130    /// 小さくなって地震力が下がるため、判定が割れた場合は S を採らないのが
131    /// 安全側になる(同数時の優先順の根拠)。対象部材が 1 本もない階は RC。
132    pub fn majority(n_rc: usize, n_s: usize, n_src: usize) -> Self {
133        let max = n_rc.max(n_s).max(n_src);
134        if max == 0 || n_rc == max {
135            StoryStructure::Rc
136        } else if n_src == max {
137            StoryStructure::Src
138        } else {
139            StoryStructure::S
140        }
141    }
142}
143
144/// 層の種別。地震層せん断力の算定方法を切り替える
145/// (一般階=Ai分布、PH階=震度 k、地下階=水平震度 K)。
146///
147/// 層の属性だが、保持先は層の**上端床**の [`Story`] である(重量と同じ場所に
148/// 集約する。[`Layer`] 参照)。最下の階(基部の床)はどの層の上端でもないため、
149/// そこに設定された値は用いられない。
150#[derive(Clone, Copy, Debug, Default, PartialEq, serde::Serialize, serde::Deserialize)]
151pub enum StoryLevelKind {
152    #[default]
153    Normal,
154    /// 塔屋(PH)階。層せん断力 Qi = k·ΣWj(k は 0.5〜1.0 の指定震度)。
155    Penthouse { k: f64 },
156    /// 地下階。Qi = Q(i+1) + K·Wi、K = 0.1·(1 − H/40)·Z(H は地盤面からの深さ[m]、20m 超は 20m)。
157    Basement { depth_m: f64 },
158}
159
160/// 階(床)の定義。法規上の「層」は [`Layer`] である。
161///
162/// フィールドは**誰が決めるか**で 2 系統に分かれる。
163///
164/// - **利用者が決める**: [`Self::name`]・[`Self::elevation`]・[`Self::level_kind`]・
165///   [`Self::weight_override`]。新規作成時の入力、または ST-Bridge の `StbStory`
166///   から入り、準備計算では書き換えない。
167/// - **準備計算が埋める**: [`Self::node_ids`]・[`Self::seismic_weight`]・
168///   [`Self::structure`]。節点と部材が確定してはじめて決まる派生値であり、
169///   階生成のたびに算定し直す。
170///
171/// [`Model::stories`] は [`Self::elevation`] の**昇順**に並び、**先頭は基部の床**
172/// ([`Model::base_elevation`] と同レベル)である。階への帰属区間が直下階のレベルで
173/// 決まるため、この並びが崩れると帰属が壊れる。
174#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
175pub struct Story {
176    pub id: StoryId,
177    /// 階名(`2F`・`PH1` など)。利用者が決める自由文字列で、階の識別に用いる。
178    pub name: String,
179    /// 階のレベル [mm]。**その階が代表する床のレベル**であり、階への帰属区間の
180    /// 上端でもある([`Model::story_of_elevation`])。
181    pub elevation: f64,
182    /// この階に属する節点(準備計算が [`Model::story_of_elevation`] で埋める)。
183    pub node_ids: Vec<NodeId>,
184    /// 設計に用いる地震用重量 [N]。準備計算の階生成が自動算定値を書き込むが、
185    /// [`Self::weight_override`] が `Some` の場合はその手入力値が入る
186    /// (解析・設計側はこのフィールドだけを読めばよい)。
187    pub seismic_weight: Option<f64>,
188    /// 地震用重量の手入力値 [N]。`Some` のときは準備計算で階を再生成しても
189    /// 保持され、[`Self::seismic_weight`] へ優先して反映される。`None` は
190    /// 自動算定値をそのまま用いる。旧スキーマは手入力なし扱い。
191    #[serde(default)]
192    pub weight_override: Option<f64>,
193    /// 主要構造種別(略算周期の鉄骨造比 α 算定用)。断面形状からの自動判定値。
194    /// 旧スキーマは RC 扱い。
195    #[serde(default)]
196    pub structure: StoryStructure,
197    /// 階の種別(一般/PH/地下)。旧スキーマは一般階扱い。
198    #[serde(default)]
199    pub level_kind: StoryLevelKind,
200}
201
202/// 層(隣り合う 2 つの階の間)。法規上の「i 階」はこれを指す。
203///
204/// [`Model::layers`] が [`Model::stories`] から組み立てる**導出値**であり、
205/// モデルには保持しない(保持すると [`Story::node_ids`] と同種の同期ずれを
206/// 1 つ増やすことになる)。層を数える処理は必ずこれを介し、
207/// [`Model::stories`] を層として直接走査してはならない。
208///
209/// 層と床の対応は実務の慣行に従う。
210///
211/// | 量 | 由来 |
212/// |---|---|
213/// | 名前 | 下端床(法令の「i 階」は下の床の呼び名) |
214/// | 階高 | 上端床の標高 − 下端床の標高 |
215/// | 重量・所属節点・階種別 | **上端床** |
216///
217/// 重量が上端床なのは、層の質量が上端の床に集中するためである。
218#[derive(Clone, Debug, PartialEq)]
219pub struct Layer {
220    /// 下から 0 始まりの層の番号。層を識別する添字。
221    pub index: usize,
222    /// 層の名前(下端床の階名)。
223    pub name: String,
224    /// 下端の階(床)。
225    pub bottom: StoryId,
226    /// 上端の階(床)。層の重量・所属節点・階種別はこの階が持つ。
227    pub top: StoryId,
228    /// 層の高さ(階高)[mm]。
229    pub height: f64,
230    /// 下端床の標高 [mm]。
231    pub bottom_elevation: f64,
232    /// 上端床の標高 [mm]。
233    pub top_elevation: f64,
234    /// 層の種別(一般/PH/地下)。
235    pub level_kind: StoryLevelKind,
236    /// 設計に用いる地震用重量 [N](未算定なら `None`)。
237    pub weight: Option<f64>,
238    /// 層に属する節点(=上端床の所属節点)。
239    pub node_ids: Vec<NodeId>,
240    /// 主要構造種別(略算周期の鉄骨造比 α 算定用)。
241    pub structure: StoryStructure,
242}
243
244impl Model {
245    /// 層([`Layer`])の一覧を下から順に返す。**層を数える処理の唯一の入口**。
246    ///
247    /// 階が床レベル列であるという不変条件(モジュールドキュメント参照)から、
248    /// 層は隣り合う階の対そのものであり、層数は `stories.len() - 1` である。
249    /// 階が 1 つ以下のモデルでは空を返す。
250    pub fn layers(&self) -> Vec<Layer> {
251        self.stories
252            .windows(2)
253            .enumerate()
254            .map(|(i, w)| {
255                let (bottom, top) = (&w[0], &w[1]);
256                Layer {
257                    index: i,
258                    name: bottom.name.clone(),
259                    bottom: bottom.id,
260                    top: top.id,
261                    height: top.elevation - bottom.elevation,
262                    bottom_elevation: bottom.elevation,
263                    top_elevation: top.elevation,
264                    level_kind: top.level_kind,
265                    weight: top.seismic_weight,
266                    node_ids: top.node_ids.clone(),
267                    structure: top.structure,
268                }
269            })
270            .collect()
271    }
272
273    /// 層の数(`stories.len() - 1`、階が 1 つ以下なら 0)。
274    ///
275    /// [`Self::layers`] を組み立てずに個数だけ要るときに使う。
276    pub fn layer_count(&self) -> usize {
277        self.stories.len().saturating_sub(1)
278    }
279
280    /// 建物の基部レベル [mm](`elevation` の基準 0)。**幾何としての基部**。
281    ///
282    /// 全構造節点(`generated_masters` =階生成が作る剛床代表節点を除く)の最小 Z
283    /// 座標を基部とする。剛床代表節点は慣性力重心に置かれる仮想節点であり、実際の
284    /// 構造高さには寄与しないため除外する。節点がない場合は 0 を返す。
285    ///
286    /// 不変条件が成立していれば `stories[0].elevation` と一致する。にもかかわらず
287    /// 節点から求めるのは、**階生成が不変条件を成立させる側**だからである。階生成は
288    /// まだ床基準になっていない階列(あるいは階が 1 つもないモデル)から床レベル列を
289    /// 組み立てるため、そのブートストラップには階に依らない基部が要る。
290    /// 帰属区間([`Self::story_spans`])は不変条件を前提とするのでこれを呼ばない。
291    pub fn base_elevation(&self) -> f64 {
292        let excluded: std::collections::HashSet<NodeId> =
293            self.generated_masters.iter().copied().collect();
294        let base = self
295            .nodes
296            .iter()
297            .filter(|n| !excluded.contains(&n.id))
298            .map(|n| n.coord[2])
299            .fold(f64::INFINITY, f64::min);
300        if base.is_finite() {
301            base
302        } else {
303            0.0
304        }
305    }
306
307    /// 各階への帰属区間 `(下端, 上端]` [mm]([`Self::stories`] と同順・同長)。
308    ///
309    /// 下端は直下階のレベル、上端は当該階のレベルである。**下端は含まず上端を含む**
310    /// ため、床レベルちょうどの節点はその階に属し、中間高さの節点は直上の階に属する。
311    ///
312    /// **最下階(基部の床)だけは下端を含む点区間** `[基部, 基部]` とする。
313    /// 不変条件により最下階の標高は基部レベルそのものであり、`(下端, 上端]` の規則を
314    /// そのまま当てはめると空区間になって柱脚・基礎梁の節点がどの階にも属さなくなる
315    /// ためである。
316    ///
317    /// 区間の算出はここに集約する。
318    ///
319    /// 最下階の下端は `stories[0].elevation` と [`Self::base_elevation`] の**小さい方**
320    /// とする。不変条件が成立していれば両者は一致するので通常は前者そのものだが、
321    /// 不変条件がまだ成立していないモデル(階生成を通していない旧形式のファイル、
322    /// 基部の階を持たない取り込みデータ)では基部側が下端になり、基部〜最下階の
323    /// 節点が最下階へ収まる。これがないと、そうしたモデルで最下階の伏図が空になり、
324    /// 節点が丸ごとどの階にも属さなくなる。
325    pub fn story_spans(&self) -> Vec<(f64, f64)> {
326        let first_bottom = self
327            .stories
328            .first()
329            .map(|s| s.elevation.min(self.base_elevation()));
330        self.stories
331            .iter()
332            .enumerate()
333            .map(|(i, s)| {
334                let bottom = match i {
335                    0 => first_bottom.unwrap_or(s.elevation),
336                    _ => self.stories[i - 1].elevation,
337                };
338                (bottom, s.elevation)
339            })
340            .collect()
341    }
342
343    /// レベル `z` [mm] が属する階を、[`Self::story_spans`] の区間列から引く。
344    ///
345    /// 階への帰属は**区間**である。中間高さの節点や段差床の節点も、区間に入れば
346    /// 当該階に属する。剛床への帰属とは規則が異なる([`Self::on_diaphragm_level`])。
347    /// どの区間にも入らない場合は `None`(基部レベル未満、または最上階より上)。
348    ///
349    /// 区間列は標高の昇順で連続しているため二分探索で引く(伏図の描画が毎フレーム
350    /// 全節点に対して呼ぶため、線形探索では階数に比例して重くなる)。
351    pub fn story_at(&self, spans: &[(f64, f64)], z: f64) -> Option<StoryId> {
352        // 上端が z 以上になる最初の区間を探す。区間は上端の昇順に並ぶ。
353        let i = spans.partition_point(|&(_, top)| top < z);
354        let &(bottom, top) = spans.get(i)?;
355        // 最下階だけは下端を含む点区間([基部, 基部])。
356        let above_bottom = if i == 0 { z >= bottom } else { z > bottom };
357        if !above_bottom || z > top {
358            return None;
359        }
360        self.stories.get(i).map(|s| s.id)
361    }
362
363    /// 各節点の所属階([`Self::nodes`] と同順・同長)。
364    ///
365    /// 階への帰属規則(区間)の単一情報源。準備計算の階生成も UI の表示も
366    /// これを用いる。
367    pub fn node_stories(&self) -> Vec<Option<StoryId>> {
368        let spans = self.story_spans();
369        self.nodes
370            .iter()
371            .map(|n| self.story_at(&spans, n.coord[2]))
372            .collect()
373    }
374
375    /// レベル `z` [mm] が階 `story` の床面上にあるか(剛床のスレーブ判定)。
376    ///
377    /// 判定は階のレベルからの差が [`DIAPHRAGM_LEVEL_TOL_MM`] 以内かどうかで、
378    /// 階への帰属(区間)とは規則が異なる。
379    pub fn on_diaphragm_level(&self, story: StoryId, z: f64) -> bool {
380        self.stories
381            .get(story.index())
382            .is_some_and(|s| (z - s.elevation).abs() <= DIAPHRAGM_LEVEL_TOL_MM)
383    }
384
385    /// 階 `story` に属する剛床([`Constraint::RigidDiaphragm`])を定義順に返す。
386    ///
387    /// 剛床は階の一部ではなく拘束として保持されるため、「この階の剛床」が要る
388    /// ところは常にこのヘルパーを情報源とする。
389    pub fn diaphragms_of(&self, story: StoryId) -> impl Iterator<Item = DiaphragmRef<'_>> {
390        self.constraints.iter().filter_map(move |c| match c {
391            Constraint::RigidDiaphragm {
392                story: s,
393                master,
394                slaves,
395                weight,
396                ci_override,
397            } if *s == story => Some(DiaphragmRef {
398                story: *s,
399                master: *master,
400                slaves,
401                weight: *weight,
402                ci_override: *ci_override,
403            }),
404            _ => None,
405        })
406    }
407
408    /// 節点 `id` がいずれかの剛床のマスターまたはスレーブか。
409    pub fn node_on_rigid_diaphragm(&self, id: NodeId) -> bool {
410        self.constraints.iter().any(|c| match c {
411            Constraint::RigidDiaphragm { master, slaves, .. } => {
412                *master == id || slaves.contains(&id)
413            }
414            _ => false,
415        })
416    }
417}