1use crate::model::MaterialCategory;
19use crate::section_shape::{concrete_young_modulus_gamma, E_STEEL};
20use crate::units::{
21 concrete_unit_weight_kn_m3, to_internal::mass_density_from_unit_weight_kn_m3, ConcreteClass,
22 ConcreteComposition, STEEL_UNIT_WEIGHT_KN_M3,
23};
24
25fn bucket2(t: f64, le40: f64, gt40: f64) -> f64 {
28 if t <= 40.0 {
29 le40
30 } else {
31 gt40
32 }
33}
34
35fn bucket3(t: f64, le40: f64, le75: f64, gt75: f64) -> f64 {
37 if t <= 40.0 {
38 le40
39 } else if t <= 75.0 {
40 le75
41 } else {
42 gt75
43 }
44}
45
46pub const STEEL_GRADES: &[&str] = &[
49 "SS400", "SS490", "SM400", "SM490", "SM520", "SN400", "SN490", "STK400", "STK490", "STKN400",
51 "STKN490", "STKR400", "STKR490", "SNR400", "SNR490", "SSC400", "SWH400",
52 "BCR235", "BCR295", "BCP235", "BCP325",
54 "TMCP325", "TMCP355", "TMCP385", "TMCP440",
56 "SA440", "LY100", "LY225",
58];
59
60pub fn steel_f_value(grade: &str, thickness: f64) -> Option<f64> {
70 match grade {
71 "SS400" | "SM400" | "SN400" | "STK400" | "STKN400" | "STKR400" | "SNR400" | "SSC400"
73 | "SWH400" => Some(bucket2(thickness, 235.0, 215.0)),
74 "SS490" => Some(bucket2(thickness, 275.0, 255.0)),
76 "SM490" | "SN490" | "STK490" | "STKN490" | "STKR490" | "SNR490" => {
78 Some(bucket2(thickness, 325.0, 295.0))
79 }
80 "SM520" => Some(bucket3(thickness, 355.0, 335.0, 325.0)),
82 "BCR295" => Some(295.0),
84 "BCR235" => Some(235.0),
85 "BCP235" => Some(235.0),
86 "BCP325" => Some(325.0),
87 "TMCP325" => Some(325.0),
89 "TMCP355" => Some(355.0),
90 "TMCP385" => Some(385.0),
91 "TMCP440" => Some(440.0),
92 "SA440" => Some(440.0),
94 "LY100" => Some(80.0),
96 "LY225" => Some(205.0),
97 _ => None,
98 }
99}
100
101pub fn steel_f_value_prefix(name: &str, thickness: f64) -> Option<f64> {
107 STEEL_GRADES
108 .iter()
109 .filter(|g| name.starts_with(*g))
110 .max_by_key(|g| g.len())
111 .and_then(|g| steel_f_value(g, thickness))
112}
113
114pub fn steel_material_strength_factor(name: &str) -> f64 {
124 let Some(grade) = STEEL_GRADES
125 .iter()
126 .filter(|g| name.starts_with(*g))
127 .max_by_key(|g| g.len())
128 else {
129 return 1.0;
130 };
131 match *grade {
132 "SA440" | "TMCP440" => 1.05,
134 _ => 1.1,
135 }
136}
137
138pub fn material_strength_factor_steel(mat: &crate::model::Material) -> f64 {
147 mat.strength_factor
148 .unwrap_or_else(|| steel_material_strength_factor(&mat.name))
149}
150
151pub fn material_strength_factor_rebar(mat: &crate::model::Material) -> f64 {
160 mat.strength_factor.unwrap_or(1.1)
161}
162
163pub fn rebar_f_value(name: &str) -> Option<f64> {
168 let rest = name
169 .strip_prefix("SD")
170 .or_else(|| name.strip_prefix("SR"))?;
171 let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
172 digits.parse::<f64>().ok().filter(|v| *v > 0.0)
173}
174
175pub fn rebar_grade_f_value(name: &str) -> Option<f64> {
187 let name = name.trim();
188 if let Some(v) = rebar_f_value(name) {
189 return Some(v);
190 }
191 let digits: String = name
192 .chars()
193 .rev()
194 .take_while(|c| c.is_ascii_digit())
195 .collect::<Vec<_>>()
196 .into_iter()
197 .rev()
198 .collect();
199 digits.parse::<f64>().ok().filter(|v| *v > 0.0)
200}
201
202pub fn rebar_yield_strength(rebar_mat: Option<&crate::model::Material>) -> Option<f64> {
213 rebar_mat.and_then(|m| m.fy).filter(|v| *v > 0.0)
214}
215
216pub fn shear_rebar_yield_strength(mat: Option<&crate::model::Material>) -> Option<f64> {
223 mat.and_then(|m| m.fy).filter(|v| *v > 0.0)
224}
225
226pub const SHEAR_REBAR_DEFAULT_FY: f64 = 295.0;
228
229pub fn is_high_strength_shear_grade(name: &str) -> bool {
240 let g = name.trim().to_uppercase();
241 if g.is_empty() {
242 return false;
243 }
244 !(g.starts_with("SD") || g.starts_with("SR"))
245}
246
247pub fn is_high_strength_shear_material(mat: Option<&crate::model::Material>) -> bool {
250 mat.is_some_and(|m| is_high_strength_shear_grade(&m.name))
251}
252
253pub const MAIN_REBAR_GRADES: &[&str] = &["SD295A", "SD295B", "SD345", "SD390", "SD490", "USD685"];
255
256pub const REBAR_NOMINAL_SIZES: &[f64] = &[
262 10.0, 13.0, 16.0, 19.0, 22.0, 25.0, 29.0, 32.0, 35.0, 38.0, 41.0,
263];
264
265pub const SHEAR_REBAR_GRADES: &[&str] = &[
268 "SD295A", "SD295B", "SD345", "SD390", "KH785", "UB785", "KSS785", "SHD685", "SPR785", "MK785",
269 "SBPD1275",
270];
271
272pub fn parse_concrete_fc(name: &str) -> Option<f64> {
275 let rest = name
276 .strip_prefix("Fc")
277 .or_else(|| name.strip_prefix("FC"))
278 .or_else(|| name.strip_prefix("fc"))?;
279 let digits: String = rest
280 .trim()
281 .chars()
282 .take_while(|c| c.is_ascii_digit() || *c == '.')
283 .collect();
284 digits.parse::<f64>().ok().filter(|v| *v > 0.0)
285}
286
287pub fn category_of_grade(name: &str) -> Option<MaterialCategory> {
299 let upper = name.trim().to_uppercase();
300 if upper.starts_with("FC") {
301 return Some(MaterialCategory::Concrete);
302 }
303 if upper.starts_with("SD") || upper.starts_with("SR") || upper.starts_with("KH") {
304 return Some(MaterialCategory::Rebar);
305 }
306 const STEEL_PREFIX: &[&str] = &["SS", "SN", "SM", "STK", "ST", "SA", "BC", "TMCP", "LY"];
307 if STEEL_PREFIX.iter().any(|p| upper.starts_with(p)) {
308 return Some(MaterialCategory::Steel);
309 }
310 None
311}
312
313#[derive(Clone, Debug, PartialEq)]
315pub struct MaterialPreset {
316 pub name: &'static str,
317 pub category: MaterialCategory,
318 pub young: f64,
320 pub poisson: f64,
322 pub density: f64,
324 pub fc: Option<f64>,
326 pub fy: Option<f64>,
328}
329
330const PRESET_STEEL: &[&str] = &[
332 "SS400", "SN400", "SM400", "SM490", "SN490", "BCR295", "BCP235", "TMCP325", "TMCP355",
333 "TMCP385", "TMCP440", "SA440", "LY100", "LY225",
334];
335
336const PRESET_REBAR: &[&str] = &["SD295", "SD345", "SD390"];
338
339const PRESET_CONCRETE_FC: &[f64] = &[
341 18.0, 21.0, 24.0, 27.0, 30.0, 33.0, 36.0, 40.0, 42.0, 45.0, 50.0, 55.0, 60.0,
342];
343
344const PRESET_CONCRETE_NAMES: &[&str] = &[
346 "Fc18", "Fc21", "Fc24", "Fc27", "Fc30", "Fc33", "Fc36", "Fc40", "Fc42", "Fc45", "Fc50", "Fc55",
347 "Fc60",
348];
349
350pub fn material_presets() -> Vec<MaterialPreset> {
359 let steel_density = mass_density_from_unit_weight_kn_m3(STEEL_UNIT_WEIGHT_KN_M3);
360 let mut out = Vec::new();
361 for &name in PRESET_STEEL {
362 out.push(MaterialPreset {
363 name,
364 category: MaterialCategory::Steel,
365 young: E_STEEL,
366 poisson: 0.3,
367 density: steel_density,
368 fc: None,
369 fy: steel_f_value(name, 0.0),
370 });
371 }
372 for &name in PRESET_REBAR {
373 out.push(MaterialPreset {
374 name,
375 category: MaterialCategory::Rebar,
376 young: E_STEEL,
377 poisson: 0.3,
378 density: steel_density,
379 fc: None,
380 fy: rebar_f_value(name),
381 });
382 }
383 for (&fc, &name) in PRESET_CONCRETE_FC.iter().zip(PRESET_CONCRETE_NAMES) {
384 let gamma_c =
385 concrete_unit_weight_kn_m3(fc, ConcreteClass::Normal, ConcreteComposition::Plain);
386 let gamma_rc =
387 concrete_unit_weight_kn_m3(fc, ConcreteClass::Normal, ConcreteComposition::Rc);
388 out.push(MaterialPreset {
389 name,
390 category: MaterialCategory::Concrete,
391 young: concrete_young_modulus_gamma(fc, gamma_c),
392 poisson: 0.2,
393 density: mass_density_from_unit_weight_kn_m3(gamma_rc),
394 fc: Some(fc),
395 fy: None,
396 });
397 }
398 out
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 #[test]
407 fn test_category_of_grade() {
408 for (name, expected) in [
409 ("Fc21", MaterialCategory::Concrete),
410 ("FC60", MaterialCategory::Concrete),
411 ("SN400B", MaterialCategory::Steel),
412 ("SS400", MaterialCategory::Steel),
413 ("SM490A", MaterialCategory::Steel),
414 ("STKR400", MaterialCategory::Steel),
415 ("BCR295", MaterialCategory::Steel),
416 ("TMCP325", MaterialCategory::Steel),
417 ("LY225", MaterialCategory::Steel),
418 ("SD295A", MaterialCategory::Rebar),
419 ("SD345", MaterialCategory::Rebar),
420 ("SR235", MaterialCategory::Rebar),
421 ("KH785", MaterialCategory::Rebar),
422 ] {
423 assert_eq!(
424 category_of_grade(name),
425 Some(expected),
426 "グレード名 {name} の区分"
427 );
428 }
429 }
430
431 #[test]
433 fn test_category_of_grade_unknown() {
434 for name in ["コンクリート", "普通強度", "", " ", "X999"] {
435 assert_eq!(category_of_grade(name), None, "未知の名称 {name:?}");
436 }
437 }
438
439 #[test]
442 fn test_category_of_grade_order() {
443 assert_eq!(category_of_grade("SD390"), Some(MaterialCategory::Rebar));
445 assert_eq!(category_of_grade("fc24"), Some(MaterialCategory::Concrete));
447 assert_eq!(category_of_grade("sn400b"), Some(MaterialCategory::Steel));
448 assert_eq!(
450 category_of_grade(" SD345 "),
451 Some(MaterialCategory::Rebar)
452 );
453 }
454
455 #[test]
459 fn test_presets_agree_with_category_of_grade() {
460 for p in material_presets() {
461 if let Some(guessed) = category_of_grade(p.name) {
462 assert_eq!(guessed, p.category, "プリセット {} の区分", p.name);
463 }
464 }
465 }
466
467 #[test]
469 fn test_steel_f_value_le40() {
470 for (g, f) in [
471 ("SS400", 235.0),
472 ("SN400", 235.0),
473 ("SM400", 235.0),
474 ("SM490", 325.0),
475 ("SN490", 325.0),
476 ("SM520", 355.0),
477 ("SS490", 275.0),
478 ("BCR295", 295.0),
479 ("BCP235", 235.0),
480 ("BCP325", 325.0),
481 ("TMCP325", 325.0),
482 ("TMCP355", 355.0),
483 ("TMCP385", 385.0),
484 ("TMCP440", 440.0),
485 ("SA440", 440.0),
486 ("LY100", 80.0),
487 ("LY225", 205.0),
488 ] {
489 assert_eq!(steel_f_value(g, 40.0), Some(f), "{g}");
490 }
491 }
492
493 #[test]
496 fn test_steel_f_value_gt40() {
497 assert_eq!(steel_f_value("SS400", 41.0), Some(215.0));
498 assert_eq!(steel_f_value("SM490", 41.0), Some(295.0));
499 assert_eq!(steel_f_value("SN490", 41.0), Some(295.0));
500 assert_eq!(steel_f_value("SM520", 41.0), Some(335.0));
501 assert_eq!(steel_f_value("SM520", 76.0), Some(325.0));
502 for g in [
503 "TMCP325", "TMCP355", "TMCP385", "TMCP440", "SA440", "BCR295", "LY225",
504 ] {
505 assert_eq!(steel_f_value(g, 41.0), steel_f_value(g, 40.0), "{g}");
506 }
507 }
508
509 #[test]
511 fn test_steel_f_value_prefix() {
512 assert_eq!(steel_f_value_prefix("SN490B", 40.0), Some(325.0));
513 assert_eq!(steel_f_value_prefix("SN400C", 40.0), Some(235.0));
514 assert_eq!(steel_f_value_prefix("SM490YA", 40.0), Some(325.0));
515 assert_eq!(steel_f_value_prefix("STKN400W", 40.0), Some(235.0));
516 assert_eq!(steel_f_value_prefix("STKN490B", 40.0), Some(325.0));
517 assert_eq!(steel_f_value_prefix("SNR400A", 40.0), Some(235.0));
518 assert_eq!(steel_f_value_prefix("SNR490B", 40.0), Some(325.0));
519 assert_eq!(steel_f_value_prefix("未知", 40.0), None);
520 }
521
522 #[test]
524 fn test_steel_material_strength_factor() {
525 assert_eq!(steel_material_strength_factor("SS400"), 1.1);
526 assert_eq!(steel_material_strength_factor("SN490B"), 1.1);
527 assert_eq!(steel_material_strength_factor("BCR295"), 1.1);
528 assert_eq!(steel_material_strength_factor("LY225"), 1.1);
529 assert_eq!(steel_material_strength_factor("TMCP385"), 1.1);
530 assert_eq!(steel_material_strength_factor("SA440"), 1.05);
531 assert_eq!(steel_material_strength_factor("TMCP440"), 1.05);
532 assert_eq!(steel_material_strength_factor("未知の材料"), 1.0);
533 assert_eq!(steel_material_strength_factor("SD345"), 1.0);
534 }
535
536 #[test]
538 fn test_material_strength_factor_by_context() {
539 let mk = |name: &str, factor: Option<f64>| crate::model::Material {
540 id: crate::ids::MaterialId(0),
541 name: name.to_string(),
542 category: MaterialCategory::Steel,
543 young: 205000.0,
544 poisson: 0.3,
545 density: 7.85e-9,
546 shear: None,
547 fc: None,
548 fy: None,
549 concrete_class: Default::default(),
550 strength_factor: factor,
551 };
552 assert_eq!(material_strength_factor_steel(&mk("SS400", None)), 1.1);
554 assert_eq!(material_strength_factor_steel(&mk("SA440", None)), 1.05);
555 assert_eq!(material_strength_factor_steel(&mk("カスタム", None)), 1.0);
556 assert_eq!(material_strength_factor_rebar(&mk("Fc24", None)), 1.1);
558 assert_eq!(material_strength_factor_rebar(&mk("SD345", None)), 1.1);
559 assert_eq!(
561 material_strength_factor_steel(&mk("カスタム", Some(1.2))),
562 1.2
563 );
564 assert_eq!(material_strength_factor_rebar(&mk("Fc24", Some(1.0))), 1.0);
565 }
566
567 #[test]
568 fn test_rebar_f_value() {
569 assert_eq!(rebar_f_value("SD295A"), Some(295.0));
570 assert_eq!(rebar_f_value("SD345"), Some(345.0));
571 assert_eq!(rebar_f_value("SD390"), Some(390.0));
572 assert_eq!(rebar_f_value("SR235"), Some(235.0));
573 assert_eq!(rebar_f_value("SS400"), None);
574 }
575
576 #[test]
578 fn test_rebar_grade_f_value_high_strength() {
579 assert_eq!(rebar_grade_f_value("SD345"), Some(345.0));
580 assert_eq!(rebar_grade_f_value("USD685"), Some(685.0));
581 assert_eq!(rebar_grade_f_value("KH785"), Some(785.0));
582 assert_eq!(rebar_grade_f_value("UB785"), Some(785.0));
583 assert_eq!(rebar_grade_f_value("SBPD1275"), Some(1275.0));
584 assert_eq!(rebar_grade_f_value("不明"), None);
585 }
586
587 fn rebar_mat(name: &str, fy: Option<f64>) -> crate::model::Material {
589 crate::model::Material {
590 strength_factor: None,
591 concrete_class: Default::default(),
592 id: crate::ids::MaterialId(0),
593 name: name.into(),
594 category: MaterialCategory::Rebar,
595 young: 205000.0,
596 poisson: 0.3,
597 density: 0.0,
598 shear: None,
599 fc: None,
600 fy,
601 }
602 }
603
604 #[test]
607 fn test_rebar_yield_strength_resolution_order() {
608 let mut mat = rebar_mat("SD295A", Some(295.0));
609 assert_eq!(rebar_yield_strength(Some(&mat)), Some(295.0));
610 mat.fy = None;
612 assert_eq!(rebar_yield_strength(Some(&mat)), None);
613 mat.fy = Some(0.0);
615 assert_eq!(rebar_yield_strength(Some(&mat)), None);
616 assert_eq!(rebar_yield_strength(None), None);
618 }
619
620 #[test]
622 fn test_shear_rebar_yield_strength() {
623 let mat = rebar_mat("SD295A", Some(295.0));
624 assert_eq!(shear_rebar_yield_strength(Some(&mat)), Some(295.0));
625 let high = rebar_mat("KH785", Some(785.0));
626 assert_eq!(shear_rebar_yield_strength(Some(&high)), Some(785.0));
627 assert_eq!(shear_rebar_yield_strength(None), None);
628 assert_eq!(SHEAR_REBAR_DEFAULT_FY, 295.0);
629 }
630
631 #[test]
632 fn test_parse_concrete_fc() {
633 assert_eq!(parse_concrete_fc("Fc21"), Some(21.0));
634 assert_eq!(parse_concrete_fc("FC36"), Some(36.0));
635 assert_eq!(parse_concrete_fc("fc60"), Some(60.0));
636 assert_eq!(parse_concrete_fc("SD345"), None);
637 }
638
639 #[test]
641 fn test_material_presets() {
642 let presets = material_presets();
643 assert_eq!(presets.len(), 14 + 3 + 13);
644 let find = |name: &str| {
645 presets
646 .iter()
647 .find(|p| p.name == name)
648 .unwrap_or_else(|| panic!("preset {name} not found"))
649 };
650
651 let ss400 = find("SS400");
652 assert_eq!(ss400.category, MaterialCategory::Steel);
653 assert_eq!(ss400.young, 205000.0);
654 assert_eq!(ss400.fy, Some(235.0));
655
656 let ly100 = find("LY100");
657 assert_eq!(ly100.fy, Some(80.0));
658
659 let sd345 = find("SD345");
660 assert_eq!(sd345.category, MaterialCategory::Rebar);
661 assert_eq!(sd345.fy, Some(345.0));
662
663 let fc24 = find("Fc24");
665 assert_eq!(fc24.category, MaterialCategory::Concrete);
666 assert_eq!(fc24.fc, Some(24.0));
667 let ec24 = 3.35e4 * (23.0f64 / 24.0).powi(2) * (24.0f64 / 60.0).powf(1.0 / 3.0);
668 assert!((fc24.young - ec24).abs() < 1e-9);
669 let fc42 = find("Fc42");
670 let ec42 = 3.35e4 * (23.5f64 / 24.0).powi(2) * (42.0f64 / 60.0).powf(1.0 / 3.0);
671 assert!((fc42.young - ec42).abs() < 1e-9);
672 let fc60 = find("Fc60");
673 let ec60 = 3.35e4 * (60.0f64 / 60.0).powf(1.0 / 3.0);
674 assert!((fc60.young - ec60).abs() < 1e-9);
675
676 let rho_steel = mass_density_from_unit_weight_kn_m3(77.0);
678 assert!((ss400.density - rho_steel).abs() < 1e-18);
679 let rho_rc = mass_density_from_unit_weight_kn_m3(24.0);
680 assert!((fc24.density - rho_rc).abs() < 1e-18);
681 }
682}