1#[derive(Clone, Copy)]
25pub struct RcCapacityInput {
26 pub b: f64,
28 pub d: f64,
30 pub at: f64,
32 pub d_eff: f64,
34 pub sigma_y: f64,
36 pub fc: f64,
38 pub pw: f64,
40 pub sigma_wy: f64,
42 pub clear_span: f64,
44 pub sigma_0: f64,
48}
49
50pub fn rc_mu_simple(inp: &RcCapacityInput) -> f64 {
60 if inp.at <= 0.0 || inp.d_eff <= 0.0 || inp.sigma_y <= 0.0 {
61 return 0.0;
62 }
63 0.9 * inp.at * inp.sigma_y * inp.d_eff
64}
65
66pub fn rc_qmu_simple(inp: &RcCapacityInput) -> f64 {
70 if inp.clear_span <= 0.0 {
71 return 0.0;
72 }
73 2.0 * rc_mu_simple(inp) / inp.clear_span
74}
75
76pub fn rc_column_mu_simple(inp: &RcCapacityInput, ag: f64, n_axial: f64) -> f64 {
99 if inp.b <= 0.0 || inp.d <= 0.0 || inp.at <= 0.0 || inp.sigma_y <= 0.0 || inp.fc <= 0.0 {
100 return 0.0;
101 }
102 let (b, d, at, sy, fc) = (inp.b, inp.d, inp.at, inp.sigma_y, inp.fc);
103 let ag = ag.max(at);
104 let n_max = b * d * fc + ag * sy;
105 let n_min = -ag * sy;
106 let n = n_axial.clamp(n_min, n_max);
107 let n_bal = 0.4 * b * d * fc;
108
109 let mu = if n > n_bal {
110 let m_bal = 0.8 * at * sy * d + 0.12 * b * d * d * fc;
111 m_bal * (n_max - n) / (n_max - n_bal)
112 } else if n >= 0.0 {
113 0.8 * at * sy * d + 0.5 * n * d * (1.0 - n / (b * d * fc))
114 } else {
115 0.8 * at * sy * d + 0.4 * n * d
116 };
117 mu.max(0.0)
118}
119
120pub fn rc_qsu_simple(inp: &RcCapacityInput) -> f64 {
137 if inp.b <= 0.0 || inp.d_eff <= 0.0 || inp.at <= 0.0 || inp.fc <= 0.0 || inp.clear_span <= 0.0 {
138 return 0.0;
139 }
140 let pt = 100.0 * inp.at / (inp.b * inp.d_eff);
141 let j = 7.0 * inp.d_eff / 8.0;
142 let shear_span_ratio = (inp.clear_span / (2.0 * inp.d_eff)).clamp(1.0, 3.0);
143 let pw = inp.pw.clamp(0.0, 0.012);
144 let concrete_term = 0.068 * pt.powf(0.23) * (inp.fc + 18.0) / (shear_span_ratio + 0.12);
145 let hoop_term = 0.85 * (pw * inp.sigma_wy).max(0.0).sqrt();
146 let sigma_0 = inp.sigma_0.clamp(0.0, 0.4 * inp.fc);
148 let axial_term = 0.1 * sigma_0;
149 (concrete_term + hoop_term + axial_term) * inp.b * j
150}
151
152pub fn rc_alpha_y_sugano(pt: f64, a_over_d: f64, d_over_full: f64, n: f64) -> f64 {
167 let ad = a_over_d.clamp(1.0, 5.0);
168 let base = if ad >= 2.0 {
169 0.043 + 1.635 * n * pt + 0.043 * ad
170 } else {
171 -0.0836 + 0.159 * ad
172 };
173 (base * d_over_full * d_over_full).max(0.0)
174}
175
176pub const RC_CRACK_COEF: f64 = 0.56;
180
181pub fn rc_crack_moment(fc: f64, ze: f64) -> f64 {
190 if fc <= 0.0 || ze <= 0.0 {
191 return 0.0;
192 }
193 RC_CRACK_COEF * fc.sqrt() * ze
194}
195
196#[allow(clippy::too_many_arguments)]
206pub fn rc_capacity_input_from_rect(
207 b: f64,
208 d: f64,
209 main: &crate::section_shape::BarSet,
210 rebar: &crate::section_shape::RcRebar,
211 mat: &crate::model::Material,
212 rebar_mat: Option<&crate::model::Material>,
213 shear_mat: Option<&crate::model::Material>,
214 clear_span: f64,
215) -> Option<RcCapacityInput> {
216 let fc = mat.fc?;
217 let at = crate::section_shape::bar_set_area(main) / 2.0;
218 let d_eff =
219 crate::rc_rebar_geom::tension_effective_depth(d, rebar.cover, rebar.shear.dia, main);
220 let pw = crate::rc_rebar_geom::pw_ratio(&rebar.shear, b);
221 Some(RcCapacityInput {
222 b,
223 d,
224 at,
225 d_eff,
226 sigma_y: crate::material_grade::rebar_yield_strength(rebar_mat)
227 .or(mat.fy)
228 .unwrap_or(345.0),
229 fc,
230 pw,
231 sigma_wy: crate::material_grade::shear_rebar_yield_strength(shear_mat)
232 .unwrap_or(crate::material_grade::SHEAR_REBAR_DEFAULT_FY),
233 clear_span,
234 sigma_0: 0.0,
235 })
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241 use crate::ids::MaterialId;
242 use crate::model::{Material, MaterialCategory};
243 use crate::section_shape::{BarSet, RcRebar, ShearBar};
244
245 fn sample_input() -> RcCapacityInput {
248 RcCapacityInput {
249 b: 400.0,
250 d: 600.0,
251 at: 1935.0,
252 d_eff: 530.0,
253 sigma_y: 345.0,
254 fc: 24.0,
255 pw: 0.002,
256 sigma_wy: 295.0,
257 clear_span: 3000.0,
258 sigma_0: 0.0,
259 }
260 }
261
262 #[test]
263 fn rc_capacity_input_from_rect_matches_handcalc_without_strength_factor() {
264 let rebar = RcRebar {
265 main_x: BarSet {
266 count: 8,
267 dia: 22.0,
268 layers: 1,
269 },
270 main_y: BarSet {
271 count: 4,
272 dia: 22.0,
273 layers: 1,
274 },
275 cover: 40.0,
276 shear: ShearBar {
277 dia: 10.0,
278 pitch: 150.0,
279 legs: 2,
280 },
281 };
282 let mat = Material {
283 strength_factor: None,
284 concrete_class: Default::default(),
285 id: MaterialId(0),
286 name: "FC24".into(),
287 category: MaterialCategory::Concrete,
288 young: 23000.0,
289 poisson: 0.2,
290 density: 2.4e-9,
291 shear: None,
292 fc: Some(24.0),
293 fy: None,
294 };
295 let input = rc_capacity_input_from_rect(
296 400.0,
297 600.0,
298 &rebar.main_x,
299 &rebar,
300 &mat,
301 None,
302 None,
303 3000.0,
304 )
305 .expect("fc set");
306 let at_expected = crate::section_shape::bar_set_area(&rebar.main_x) / 2.0;
307 let d_eff_expected =
308 crate::rc_rebar_geom::tension_effective_depth(600.0, 40.0, 10.0, &rebar.main_x);
309 assert!((input.at - at_expected).abs() < 1e-9);
310 assert!((input.d_eff - d_eff_expected).abs() < 1e-9);
311 assert_eq!(input.sigma_y, 345.0);
312 assert_eq!(input.sigma_wy, 295.0);
313 }
314
315 #[test]
316 fn test_rc_mu_simple_matches_handcalc() {
317 let inp = sample_input();
318 let mu_handcalc = 0.9 * 1935.0 * 345.0 * 530.0;
320 let mu = rc_mu_simple(&inp);
321 assert!(
322 (mu - mu_handcalc).abs() < 1e-6,
323 "Mu={} vs handcalc={}",
324 mu,
325 mu_handcalc
326 );
327 }
328
329 #[test]
330 fn test_rc_column_mu_simple_branches() {
331 let inp = sample_input();
332 let (b, d, at, sy, fc) = (400.0_f64, 600.0, 1935.0, 345.0, 24.0);
333 let ag = 2.0 * at; let n_bal = 0.4 * b * d * fc; let n_max = b * d * fc + ag * sy;
336
337 let mu0 = rc_column_mu_simple(&inp, ag, 0.0);
339 assert!((mu0 - 0.8 * at * sy * d).abs() < 1e-6);
340
341 let n1 = 0.2 * b * d * fc;
343 let mu1 = rc_column_mu_simple(&inp, ag, n1);
344 let expect1 = 0.8 * at * sy * d + 0.5 * n1 * d * (1.0 - n1 / (b * d * fc));
345 assert!((mu1 - expect1).abs() < 1e-6);
346 assert!(mu1 > mu0);
347
348 let mu_at_nmax = rc_column_mu_simple(&inp, ag, n_max);
350 assert!(mu_at_nmax.abs() < 1e-6);
351 let n2 = 0.7 * n_max + 0.3 * n_bal;
352 let mu2 = rc_column_mu_simple(&inp, ag, n2);
353 let m_bal = 0.8 * at * sy * d + 0.12 * b * d * d * fc;
354 let expect2 = m_bal * (n_max - n2) / (n_max - n_bal);
355 assert!((mu2 - expect2).abs() < 1e-6);
356
357 let n3 = -0.5 * ag * sy;
359 let mu3 = rc_column_mu_simple(&inp, ag, n3);
360 assert!((mu3 - (0.8 * at * sy * d + 0.4 * n3 * d)).abs() < 1e-6);
361 assert!(mu3 < mu0);
362 let lo = rc_column_mu_simple(&inp, ag, n_bal - 1e-6);
364 let hi = rc_column_mu_simple(&inp, ag, n_bal + 1e-6);
365 assert!(
366 (lo - hi).abs() / lo < 1e-6,
367 "branch continuity: {lo} vs {hi}"
368 );
369 }
370
371 #[test]
372 fn test_rc_qmu_simple_matches_handcalc() {
373 let inp = sample_input();
374 let mu_handcalc = 0.9 * 1935.0 * 345.0 * 530.0;
375 let qmu_handcalc = 2.0 * mu_handcalc / 3000.0;
376 let qmu = rc_qmu_simple(&inp);
377 assert!(
378 (qmu - qmu_handcalc).abs() < 1e-6,
379 "Qmu={} vs handcalc={}",
380 qmu,
381 qmu_handcalc
382 );
383 }
384
385 #[test]
386 fn test_rc_alpha_y_sugano_matches_handcalc() {
387 let ay = rc_alpha_y_sugano(0.008, 3.0, 0.9, 15.0);
389 let base = 0.043 + 1.635 * 15.0 * 0.008 + 0.043 * 3.0;
390 assert!((ay - base * 0.9 * 0.9).abs() < 1e-9, "αy={ay}");
391 assert!(ay > 0.15 && ay < 0.5, "αy={ay}");
393
394 let ay2 = rc_alpha_y_sugano(0.008, 1.5, 0.9, 15.0);
396 let base2 = -0.0836 + 0.159 * 1.5;
397 assert!((ay2 - base2 * 0.81).abs() < 1e-9);
398
399 let lo = rc_alpha_y_sugano(0.008, 0.5, 0.9, 15.0);
401 let at1 = rc_alpha_y_sugano(0.008, 1.0, 0.9, 15.0);
402 assert!((lo - at1).abs() < 1e-12);
403 let hi = rc_alpha_y_sugano(0.008, 8.0, 0.9, 15.0);
404 let at5 = rc_alpha_y_sugano(0.008, 5.0, 0.9, 15.0);
405 assert!((hi - at5).abs() < 1e-12);
406 }
407
408 #[test]
409 fn test_rc_qsu_simple_matches_handcalc() {
410 let inp = sample_input();
411 let pt: f64 = 100.0 * 1935.0 / (400.0 * 530.0);
415 let j = 7.0 * 530.0 / 8.0;
416 let shear_span_ratio: f64 = 3000.0 / (2.0 * 530.0);
417 let concrete_term = 0.068 * pt.powf(0.23) * (24.0 + 18.0) / (shear_span_ratio + 0.12);
418 let hoop_term = 0.85 * (0.002_f64 * 295.0).sqrt();
419 let qsu_handcalc = (concrete_term + hoop_term) * 400.0 * j;
420
421 let qsu = rc_qsu_simple(&inp);
422 assert!(
423 (qsu - qsu_handcalc).abs() < 1e-6,
424 "Qsu={} vs handcalc={}",
425 qsu,
426 qsu_handcalc
427 );
428 let qmu = rc_qmu_simple(&inp);
430 assert!(qsu / qmu > 1.0, "Qsu/Qmu={}", qsu / qmu);
431 }
432
433 #[test]
434 fn test_rc_qsu_simple_clamps_shear_span_ratio_low() {
435 let mut inp = sample_input();
437 inp.clear_span = 200.0; let qsu = rc_qsu_simple(&inp);
439
440 let pt: f64 = 100.0 * 1935.0 / (400.0 * 530.0);
441 let j = 7.0 * 530.0 / 8.0;
442 let concrete_term = 0.068 * pt.powf(0.23) * (24.0 + 18.0) / (1.0 + 0.12); let hoop_term = 0.85 * (0.002_f64 * 295.0).sqrt();
444 let qsu_handcalc = (concrete_term + hoop_term) * 400.0 * j;
445 assert!(
446 (qsu - qsu_handcalc).abs() < 1e-6,
447 "Qsu={} vs handcalc(clamped)={}",
448 qsu,
449 qsu_handcalc
450 );
451 }
452
453 #[test]
454 fn test_rc_qsu_simple_clamps_shear_span_ratio_high() {
455 let mut inp = sample_input();
457 inp.clear_span = 6000.0; let qsu = rc_qsu_simple(&inp);
459
460 let pt: f64 = 100.0 * 1935.0 / (400.0 * 530.0);
461 let j = 7.0 * 530.0 / 8.0;
462 let concrete_term = 0.068 * pt.powf(0.23) * (24.0 + 18.0) / (3.0 + 0.12); let hoop_term = 0.85 * (0.002_f64 * 295.0).sqrt();
464 let qsu_handcalc = (concrete_term + hoop_term) * 400.0 * j;
465 assert!(
466 (qsu - qsu_handcalc).abs() < 1e-6,
467 "Qsu={} vs handcalc(clamped)={}",
468 qsu,
469 qsu_handcalc
470 );
471 }
472
473 #[test]
474 fn test_rc_qsu_simple_clamps_pw_upper_bound() {
475 let mut inp_over = sample_input();
477 inp_over.pw = 0.05;
478 let mut inp_clamped = sample_input();
479 inp_clamped.pw = 0.012;
480
481 let qsu_over = rc_qsu_simple(&inp_over);
482 let qsu_clamped = rc_qsu_simple(&inp_clamped);
483 assert!(
484 (qsu_over - qsu_clamped).abs() < 1e-9,
485 "qsu_over={} vs qsu_clamped={}",
486 qsu_over,
487 qsu_clamped
488 );
489 assert!(qsu_clamped > rc_qsu_simple(&sample_input()));
491 }
492
493 #[test]
494 fn test_rc_mu_simple_invalid_inputs_are_zero() {
495 let base = sample_input();
496
497 let mut at_zero = sample_input();
498 at_zero.at = 0.0;
499 assert_eq!(rc_mu_simple(&at_zero), 0.0);
500
501 let mut d_eff_zero = sample_input();
502 d_eff_zero.d_eff = 0.0;
503 assert_eq!(rc_mu_simple(&d_eff_zero), 0.0);
504
505 let mut sigma_y_zero = sample_input();
506 sigma_y_zero.sigma_y = 0.0;
507 assert_eq!(rc_mu_simple(&sigma_y_zero), 0.0);
508
509 assert!(rc_mu_simple(&base) > 0.0);
511 }
512
513 #[test]
514 fn test_rc_qmu_simple_zero_clear_span_is_zero() {
515 let mut inp = sample_input();
516 inp.clear_span = 0.0;
517 assert_eq!(rc_qmu_simple(&inp), 0.0);
518
519 let mut inp_neg = sample_input();
520 inp_neg.clear_span = -100.0;
521 assert_eq!(rc_qmu_simple(&inp_neg), 0.0);
522 }
523
524 #[test]
525 fn test_rc_qsu_simple_invalid_inputs_are_zero() {
526 let mut b_zero = sample_input();
527 b_zero.b = 0.0;
528 assert_eq!(rc_qsu_simple(&b_zero), 0.0);
529
530 let mut d_eff_zero = sample_input();
531 d_eff_zero.d_eff = 0.0;
532 assert_eq!(rc_qsu_simple(&d_eff_zero), 0.0);
533
534 let mut at_zero = sample_input();
535 at_zero.at = 0.0;
536 assert_eq!(rc_qsu_simple(&at_zero), 0.0);
537
538 let mut fc_zero = sample_input();
539 fc_zero.fc = 0.0;
540 assert_eq!(rc_qsu_simple(&fc_zero), 0.0);
541
542 let mut span_zero = sample_input();
543 span_zero.clear_span = 0.0;
544 assert_eq!(rc_qsu_simple(&span_zero), 0.0);
545 }
546
547 #[test]
548 fn test_rc_qsu_simple_sigma_0_zero_matches_original() {
549 let inp = sample_input();
551 assert_eq!(inp.sigma_0, 0.0);
552 let qsu = rc_qsu_simple(&inp);
553 let pt: f64 = 100.0 * 1935.0 / (400.0 * 530.0);
554 let j = 7.0 * 530.0 / 8.0;
555 let shear_span_ratio: f64 = 3000.0 / (2.0 * 530.0);
556 let concrete_term = 0.068 * pt.powf(0.23) * (24.0 + 18.0) / (shear_span_ratio + 0.12);
557 let hoop_term = 0.85 * (0.002_f64 * 295.0).sqrt();
558 let qsu_handcalc = (concrete_term + hoop_term) * 400.0 * j;
559 assert!((qsu - qsu_handcalc).abs() < 1e-6);
560 }
561
562 #[test]
563 fn test_rc_qsu_simple_axial_term_adds_01_sigma0_b_j() {
564 let mut inp = sample_input();
567 let qsu_base = rc_qsu_simple(&inp);
568 inp.sigma_0 = 5.0;
569 let qsu_with_axial = rc_qsu_simple(&inp);
570 let j = 7.0 * 530.0 / 8.0;
571 let expected_delta = 0.1 * 5.0 * 400.0 * j;
572 assert!(
573 (qsu_with_axial - qsu_base - expected_delta).abs() < 1e-6,
574 "delta={} expected={}",
575 qsu_with_axial - qsu_base,
576 expected_delta
577 );
578 }
579
580 #[test]
581 fn test_rc_qsu_simple_sigma_0_clamped_to_upper_bound_04fc() {
582 let mut inp_over = sample_input();
584 inp_over.sigma_0 = 20.0;
585 let mut inp_clamped = sample_input();
586 inp_clamped.sigma_0 = 0.4 * 24.0;
587 assert!((rc_qsu_simple(&inp_over) - rc_qsu_simple(&inp_clamped)).abs() < 1e-9);
588 assert!(rc_qsu_simple(&inp_clamped) > rc_qsu_simple(&sample_input()));
590 }
591
592 #[test]
593 fn test_rc_qsu_simple_sigma_0_negative_is_clamped_to_zero() {
594 let mut inp_neg = sample_input();
596 inp_neg.sigma_0 = -10.0;
597 let qsu_neg = rc_qsu_simple(&inp_neg);
598 let qsu_zero = rc_qsu_simple(&sample_input());
599 assert!((qsu_neg - qsu_zero).abs() < 1e-9);
600 }
601}