Skip to main content

oxiphysics_materials/construction/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::terzaghi_bearing_factors;
6#[allow(unused_imports)]
7use super::functions::*;
8use std::f64::consts::PI;
9
10/// Concrete cover to reinforcement per ACI 318.
11pub struct CoverRequirement;
12impl CoverRequirement {
13    /// Minimum clear cover (mm) for given exposure class and bar size (mm diameter).
14    pub fn minimum_cover(exposure: &ExposureClass, bar_dia: f64) -> f64 {
15        match exposure {
16            ExposureClass::Interior => {
17                if bar_dia <= 16.0 {
18                    20.0
19                } else {
20                    40.0
21                }
22            }
23            ExposureClass::Moderate => 50.0,
24            ExposureClass::Severe => 65.0,
25            ExposureClass::Submerged => 75.0,
26        }
27    }
28    /// Effective depth given total section depth h (mm) and cover + bar dia/2.
29    pub fn effective_depth(h: f64, cover: f64, bar_dia: f64) -> f64 {
30        h - cover - bar_dia / 2.0
31    }
32}
33/// Stirrup (shear reinforcement) design for reinforced concrete beams (ACI 318).
34#[derive(Debug, Clone)]
35pub struct StirrupDesign {
36    /// Beam width bw (mm).
37    pub bw: f64,
38    /// Effective depth d (mm).
39    pub d: f64,
40    /// Concrete f'c (MPa).
41    pub fc: f64,
42    /// Steel yield strength fy (MPa).
43    pub fy: f64,
44    /// Stirrup bar area per leg Av (mm²/leg).
45    pub av: f64,
46    /// Number of legs per stirrup.
47    pub legs: u32,
48    /// Stirrup spacing s (mm).
49    pub spacing: f64,
50}
51impl StirrupDesign {
52    /// Create a stirrup design.
53    pub fn new(bw: f64, d: f64, fc: f64, fy: f64, av: f64, legs: u32, spacing: f64) -> Self {
54        StirrupDesign {
55            bw,
56            d,
57            fc,
58            fy,
59            av,
60            legs,
61            spacing,
62        }
63    }
64    /// Concrete shear contribution Vc (N) per ACI 318.
65    pub fn vc(&self) -> f64 {
66        0.17 * self.fc.sqrt() * self.bw * self.d
67    }
68    /// Steel shear contribution Vs (N) per ACI 318.
69    pub fn vs(&self) -> f64 {
70        let total_av = self.av * self.legs as f64;
71        total_av * self.fy * self.d / self.spacing
72    }
73    /// Nominal shear capacity Vn = Vc + Vs (N).
74    pub fn vn(&self) -> f64 {
75        self.vc() + self.vs()
76    }
77    /// Design shear capacity φVn (φ = 0.75).
78    pub fn phi_vn(&self) -> f64 {
79        0.75 * self.vn()
80    }
81    /// Maximum stirrup spacing per ACI 318 (d/2 or 600 mm, whichever smaller).
82    pub fn max_spacing(&self) -> f64 {
83        (self.d / 2.0).min(600.0)
84    }
85    /// Minimum stirrup area per ACI 318 (mm²/mm): Av_min/s = max(0.062*sqrt(fc)*bw/fy, 0.35*bw/fy).
86    pub fn min_av_per_s(&self) -> f64 {
87        let t1 = 0.062 * self.fc.sqrt() * self.bw / self.fy;
88        let t2 = 0.35 * self.bw / self.fy;
89        t1.max(t2)
90    }
91    /// Check if stirrup spacing is adequate for Vu.
92    pub fn is_adequate(&self, vu: f64) -> bool {
93        self.phi_vn() >= vu
94    }
95}
96/// Equal-leg angle section properties.
97#[derive(Debug, Clone)]
98pub struct AngleSection {
99    /// Leg length L (mm).
100    pub l: f64,
101    /// Leg thickness t (mm).
102    pub t: f64,
103    /// Yield strength Fy (MPa).
104    pub fy: f64,
105}
106impl AngleSection {
107    /// Create an equal-leg angle section.
108    pub fn equal_leg(l: f64, t: f64, fy: f64) -> Self {
109        AngleSection { l, t, fy }
110    }
111    /// Cross-sectional area (mm²).
112    pub fn area(&self) -> f64 {
113        2.0 * self.l * self.t - self.t.powi(2)
114    }
115    /// Centroid location (mm) from back of angle.
116    pub fn centroid(&self) -> f64 {
117        let a = self.area();
118        if a < 1e-10 {
119            return 0.0;
120        }
121        let a1 = self.l * self.t;
122        let x1 = self.t / 2.0;
123        let a2 = (self.l - self.t) * self.t;
124        let x2 = self.t + (self.l - self.t) / 2.0;
125        (a1 * x1 + a2 * x2) / a
126    }
127    /// Ixx about geometric axis (mm⁴) — approximate.
128    pub fn ixx(&self) -> f64 {
129        let t = self.t;
130        let l = self.l;
131        let i_horiz = l * t.powi(3) / 12.0 + l * t * (t / 2.0).powi(2);
132        let i_vert = t * (l - t).powi(3) / 12.0 + t * (l - t) * ((l - t) / 2.0 + t).powi(2);
133        i_horiz + i_vert
134    }
135    /// Radius of gyration rz (mm) about minimum axis (approx).
136    pub fn rz(&self) -> f64 {
137        (self.ixx() / self.area()).sqrt()
138    }
139    /// Axial capacity Pn (N) for short column.
140    pub fn axial_capacity(&self) -> f64 {
141        self.area() * self.fy
142    }
143}
144/// Retaining wall earth pressure analysis (Rankine/Coulomb).
145pub struct RetainingWall {
146    /// Height of wall H (m).
147    pub height: f64,
148    /// Soil unit weight behind wall γ (kN/m³).
149    pub gamma: f64,
150    /// Friction angle φ (degrees).
151    pub phi_deg: f64,
152    /// Wall friction angle δ (degrees).
153    pub delta_deg: f64,
154    /// Backfill inclination β (degrees).
155    pub beta_deg: f64,
156}
157impl RetainingWall {
158    /// Create a retaining wall with given parameters.
159    pub fn new(height: f64, gamma: f64, phi_deg: f64) -> Self {
160        RetainingWall {
161            height,
162            gamma,
163            phi_deg,
164            delta_deg: phi_deg * 2.0 / 3.0,
165            beta_deg: 0.0,
166        }
167    }
168    /// Rankine active earth pressure coefficient Ka.
169    pub fn rankine_ka(&self) -> f64 {
170        let phi = self.phi_deg.to_radians();
171        ((PI / 4.0 - phi / 2.0).tan()).powi(2)
172    }
173    /// Rankine passive earth pressure coefficient Kp.
174    pub fn rankine_kp(&self) -> f64 {
175        let phi = self.phi_deg.to_radians();
176        ((PI / 4.0 + phi / 2.0).tan()).powi(2)
177    }
178    /// Total active thrust Pa per unit width (kN/m) using Rankine.
179    pub fn active_thrust_rankine(&self) -> f64 {
180        0.5 * self.gamma * self.height.powi(2) * self.rankine_ka()
181    }
182    /// Total passive resistance Pp per unit width (kN/m) using Rankine.
183    pub fn passive_resistance_rankine(&self) -> f64 {
184        0.5 * self.gamma * self.height.powi(2) * self.rankine_kp()
185    }
186    /// Coulomb active pressure coefficient Ka.
187    pub fn coulomb_ka(&self) -> f64 {
188        let phi = self.phi_deg.to_radians();
189        let delta = self.delta_deg.to_radians();
190        let beta = self.beta_deg.to_radians();
191        let alpha = PI / 2.0;
192        let num = (phi - beta).sin().powi(2);
193        let term = 1.0
194            + ((phi + delta).sin() * (phi - beta).sin()
195                / ((alpha + delta).sin() * (alpha - beta).sin()))
196            .sqrt();
197        num / ((alpha).sin().powi(2) * (alpha - delta).sin() * term.powi(2))
198    }
199    /// Check overturning stability factor (should be > 2.0).
200    pub fn overturning_factor(&self, footing_width: f64) -> f64 {
201        let pa = self.active_thrust_rankine();
202        let overturning_moment = pa * self.height / 3.0;
203        let restoring_moment = self.gamma * self.height * footing_width * footing_width / 2.0;
204        if overturning_moment < 1e-10 {
205            return f64::INFINITY;
206        }
207        restoring_moment / overturning_moment
208    }
209}
210/// Masonry prism test data for determining f'm (compressive strength).
211#[derive(Debug, Clone)]
212pub struct MasonryPrism {
213    /// Unit compressive strength (MPa).
214    pub unit_strength: f64,
215    /// Mortar type (S = 12.4 MPa, N = 5.2 MPa, M = 17.2 MPa minimum).
216    pub mortar_type: String,
217    /// Prism height-to-thickness ratio (h/t), typically 2–5.
218    pub h_t_ratio: f64,
219    /// Measured prism strength fp (MPa).
220    pub fp_measured: f64,
221}
222impl MasonryPrism {
223    /// Create a masonry prism.
224    pub fn new(unit_strength: f64, mortar_type: &str, h_t_ratio: f64, fp_measured: f64) -> Self {
225        MasonryPrism {
226            unit_strength,
227            mortar_type: mortar_type.to_string(),
228            h_t_ratio,
229            fp_measured,
230        }
231    }
232    /// Correction factor for h/t ratio (TMS 602): accounts for slenderness.
233    pub fn ht_correction_factor(&self) -> f64 {
234        if self.h_t_ratio >= 5.0 {
235            1.0
236        } else if self.h_t_ratio >= 2.0 {
237            0.75 + 0.05 * (self.h_t_ratio - 2.0)
238        } else {
239            0.75
240        }
241    }
242    /// Corrected prism strength f'm (MPa).
243    pub fn fm_corrected(&self) -> f64 {
244        self.fp_measured * self.ht_correction_factor()
245    }
246    /// Modulus of elasticity Em per TMS 402: Em = 900 * f'm.
247    pub fn em(&self) -> f64 {
248        900.0 * self.fm_corrected()
249    }
250    /// Allowable compressive stress Fa (MPa) for unreinforced masonry (TMS 402).
251    pub fn allowable_compressive_stress(&self) -> f64 {
252        0.25 * self.fm_corrected()
253    }
254}
255/// Concrete mix design with water-cement ratio, admixtures, and aggregate grading.
256#[derive(Debug, Clone)]
257pub struct ConcreteMixDesign {
258    /// Water-cement ratio (w/c) by mass.
259    pub wc_ratio: f64,
260    /// Cement content (kg/m³).
261    pub cement: f64,
262    /// Water content (kg/m³).
263    pub water: f64,
264    /// Fine aggregate content (kg/m³).
265    pub fine_agg: f64,
266    /// Coarse aggregate content (kg/m³).
267    pub coarse_agg: f64,
268    /// Superplasticiser dosage (% bwc).
269    pub superplasticiser: f64,
270    /// Air entrainment (%).
271    pub air_content: f64,
272    /// Slump (mm).
273    pub slump: f64,
274}
275impl ConcreteMixDesign {
276    /// Create a standard Normal Portland Cement mix.
277    pub fn new(
278        wc_ratio: f64,
279        cement: f64,
280        fine_agg: f64,
281        coarse_agg: f64,
282        air_content: f64,
283    ) -> Self {
284        let water = wc_ratio * cement;
285        ConcreteMixDesign {
286            wc_ratio,
287            cement,
288            water,
289            fine_agg,
290            coarse_agg,
291            superplasticiser: 0.0,
292            air_content,
293            slump: 75.0,
294        }
295    }
296    /// Create a typical C30 normal-weight mix (approximate ACI 211 proportioning).
297    pub fn c30_normal_weight() -> Self {
298        ConcreteMixDesign {
299            wc_ratio: 0.50,
300            cement: 350.0,
301            water: 175.0,
302            fine_agg: 720.0,
303            coarse_agg: 1080.0,
304            superplasticiser: 0.0,
305            air_content: 2.0,
306            slump: 75.0,
307        }
308    }
309    /// Create a high-performance mix with superplasticiser.
310    pub fn high_performance() -> Self {
311        ConcreteMixDesign {
312            wc_ratio: 0.30,
313            cement: 500.0,
314            water: 150.0,
315            fine_agg: 700.0,
316            coarse_agg: 1050.0,
317            superplasticiser: 1.5,
318            air_content: 1.5,
319            slump: 180.0,
320        }
321    }
322    /// Fresh concrete unit weight (kg/m³), ignoring air.
323    pub fn fresh_unit_weight(&self) -> f64 {
324        self.cement + self.water + self.fine_agg + self.coarse_agg
325    }
326    /// Estimated 28-day compressive strength (MPa) via Abrams' law.
327    /// fc = A / B^(w/c), with A=96 MPa, B=8.6 for OPC.
328    pub fn abrams_strength(&self) -> f64 {
329        let a = 96.0_f64;
330        let b = 8.6_f64;
331        a / b.powf(self.wc_ratio)
332    }
333    /// Volume of paste (m³/m³ concrete).
334    pub fn paste_volume(&self) -> f64 {
335        let rho_c = 3150.0;
336        let rho_w = 1000.0;
337        self.cement / rho_c + self.water / rho_w
338    }
339    /// Aggregate-cement ratio by mass.
340    pub fn aggregate_cement_ratio(&self) -> f64 {
341        (self.fine_agg + self.coarse_agg) / self.cement
342    }
343    /// Check Duff Abrams water-cement ratio limit for durability (≤ 0.50 for exposure class C2).
344    pub fn meets_durability_wc_limit(&self, max_wc: f64) -> bool {
345        self.wc_ratio <= max_wc
346    }
347    /// Fineness modulus of combined aggregate (simplified weighted average).
348    /// `fm_fine` = fineness modulus of fine agg, `fm_coarse` = coarse.
349    pub fn combined_fineness_modulus(&self, fm_fine: f64, fm_coarse: f64) -> f64 {
350        let total = self.fine_agg + self.coarse_agg;
351        if total < 1e-6 {
352            return 0.0;
353        }
354        (self.fine_agg * fm_fine + self.coarse_agg * fm_coarse) / total
355    }
356}
357/// Primary consolidation settlement analysis (Terzaghi 1D consolidation).
358#[derive(Debug, Clone)]
359pub struct TerzaghiSettlement {
360    /// Compression index Cc (dimensionless).
361    pub cc: f64,
362    /// Recompression index Cr (dimensionless).
363    pub cr: f64,
364    /// Initial void ratio e0.
365    pub e0: f64,
366    /// Layer thickness H (m).
367    pub h: f64,
368    /// Initial vertical effective stress σ'v0 (kPa).
369    pub sigma_v0: f64,
370    /// Preconsolidation pressure σ'p (kPa).
371    pub sigma_p: f64,
372    /// Coefficient of consolidation cv (m²/year).
373    pub cv: f64,
374}
375impl TerzaghiSettlement {
376    /// Create a normally consolidated clay layer.
377    pub fn normally_consolidated(cc: f64, e0: f64, h: f64, sigma_v0: f64, cv: f64) -> Self {
378        TerzaghiSettlement {
379            cc,
380            cr: cc / 5.0,
381            e0,
382            h,
383            sigma_v0,
384            sigma_p: sigma_v0,
385            cv,
386        }
387    }
388    /// Create an over-consolidated clay layer.
389    pub fn over_consolidated(
390        cc: f64,
391        cr: f64,
392        e0: f64,
393        h: f64,
394        sigma_v0: f64,
395        sigma_p: f64,
396        cv: f64,
397    ) -> Self {
398        TerzaghiSettlement {
399            cc,
400            cr,
401            e0,
402            h,
403            sigma_v0,
404            sigma_p,
405            cv,
406        }
407    }
408    /// Primary settlement Sc (m) for stress increment Δσ (kPa).
409    pub fn primary_settlement(&self, delta_sigma: f64) -> f64 {
410        let sigma_f = self.sigma_v0 + delta_sigma;
411        if self.sigma_v0 >= self.sigma_p {
412            self.h * self.cc / (1.0 + self.e0) * (sigma_f / self.sigma_v0).log10()
413        } else if sigma_f <= self.sigma_p {
414            self.h * self.cr / (1.0 + self.e0) * (sigma_f / self.sigma_v0).log10()
415        } else {
416            let s1 = self.h * self.cr / (1.0 + self.e0) * (self.sigma_p / self.sigma_v0).log10();
417            let s2 = self.h * self.cc / (1.0 + self.e0) * (sigma_f / self.sigma_p).log10();
418            s1 + s2
419        }
420    }
421    /// Time factor Tv for a given consolidation degree Uv.
422    /// Uses Terzaghi's time factor: Tv ≈ π/4 * Uv² for Uv ≤ 0.6.
423    pub fn time_factor(&self, uv: f64) -> f64 {
424        if uv <= 0.6 {
425            PI / 4.0 * uv.powi(2)
426        } else {
427            -(1.0 - uv).ln() * 1.781 - 0.933
428        }
429    }
430    /// Time t (years) to reach consolidation degree Uv (0–1) for double drainage.
431    pub fn consolidation_time(&self, uv: f64) -> f64 {
432        let tv = self.time_factor(uv);
433        let hdr = self.h / 2.0;
434        tv * hdr.powi(2) / self.cv
435    }
436    /// Overconsolidation ratio OCR = σ'p / σ'v0.
437    pub fn ocr(&self) -> f64 {
438        self.sigma_p / self.sigma_v0
439    }
440}
441/// Sieve analysis results for aggregate grading.
442#[derive(Debug, Clone)]
443pub struct AggregateGrading {
444    /// Sieve sizes in mm (ascending order).
445    pub sieve_sizes: Vec<f64>,
446    /// Percent passing each sieve (0–100).
447    pub percent_passing: Vec<f64>,
448}
449impl AggregateGrading {
450    /// Create from parallel sieve / passing arrays.
451    pub fn new(sieve_sizes: Vec<f64>, percent_passing: Vec<f64>) -> Self {
452        AggregateGrading {
453            sieve_sizes,
454            percent_passing,
455        }
456    }
457    /// Create a typical ASTM C33 coarse aggregate grading (19 mm max size).
458    pub fn astm_c33_coarse() -> Self {
459        AggregateGrading {
460            sieve_sizes: vec![25.0, 19.0, 12.5, 9.5, 4.75, 2.36],
461            percent_passing: vec![100.0, 90.0, 55.0, 35.0, 5.0, 0.0],
462        }
463    }
464    /// Create a typical ASTM C33 fine aggregate grading.
465    pub fn astm_c33_fine() -> Self {
466        AggregateGrading {
467            sieve_sizes: vec![9.5, 4.75, 2.36, 1.18, 0.60, 0.30, 0.15],
468            percent_passing: vec![100.0, 95.0, 80.0, 65.0, 45.0, 20.0, 5.0],
469        }
470    }
471    /// Fineness modulus: sum of cumulative % retained on standard sieves / 100.
472    pub fn fineness_modulus(&self) -> f64 {
473        let sum_retained: f64 = self.percent_passing.iter().map(|p| 100.0 - p).sum();
474        sum_retained / 100.0
475    }
476    /// Maximum aggregate size (mm) — largest sieve with 100% passing.
477    pub fn maximum_size(&self) -> f64 {
478        for (i, &p) in self.percent_passing.iter().enumerate() {
479            if p >= 100.0 {
480                return self.sieve_sizes[i];
481            }
482        }
483        *self.sieve_sizes.last().unwrap_or(&0.0)
484    }
485    /// Nominal maximum aggregate size (mm) — first sieve where passing < 100%.
486    pub fn nominal_max_size(&self) -> f64 {
487        for (i, &p) in self.percent_passing.iter().enumerate() {
488            if p < 100.0 {
489                if i > 0 {
490                    return self.sieve_sizes[i - 1];
491                }
492                return self.sieve_sizes[0];
493            }
494        }
495        *self.sieve_sizes.last().unwrap_or(&0.0)
496    }
497}
498/// Structural timber material properties.
499pub struct TimberMaterial {
500    /// Species grade designation.
501    pub species_grade: String,
502    /// Modulus of elasticity (MPa).
503    pub moe: f64,
504    /// Modulus of rupture (MPa).
505    pub mor: f64,
506    /// Longitudinal Young's modulus (MPa).
507    pub e_longitudinal: f64,
508    /// Radial Young's modulus (MPa).
509    pub e_radial: f64,
510    /// Tangential Young's modulus (MPa).
511    pub e_tangential: f64,
512    /// Density (kg/m³).
513    pub density: f64,
514}
515impl TimberMaterial {
516    /// Create a Douglas Fir Select Structural timber section.
517    pub fn douglas_fir_ss() -> Self {
518        TimberMaterial {
519            species_grade: "Douglas Fir - Select Structural".to_string(),
520            moe: 12_400.0,
521            mor: 62.0,
522            e_longitudinal: 12_400.0,
523            e_radial: 930.0,
524            e_tangential: 620.0,
525            density: 500.0,
526        }
527    }
528    /// Adjusted bending design value Fb' (MPa) with CD (load duration) and CM (moisture).
529    pub fn adjusted_fb(&self, cd: f64, cm: f64) -> f64 {
530        self.mor * cd * cm
531    }
532    /// Adjusted modulus E' (MPa) with CM (moisture) factor.
533    pub fn adjusted_e(&self, cm: f64) -> f64 {
534        self.moe * cm
535    }
536    /// Shear modulus G_LR ≈ E_longitudinal / 16 (approximate).
537    pub fn shear_modulus_lr(&self) -> f64 {
538        self.e_longitudinal / 16.0
539    }
540}
541/// Laminated veneer lumber (LVL) section properties.
542#[derive(Debug, Clone)]
543pub struct LaminatedVeneerLumber {
544    /// Width b (mm).
545    pub b: f64,
546    /// Depth d (mm).
547    pub d: f64,
548    /// Reference bending design value Fb (MPa).
549    pub fb: f64,
550    /// Reference compression parallel Fc (MPa).
551    pub fc: f64,
552    /// Reference tension parallel Ft (MPa).
553    pub ft: f64,
554    /// Reference shear Fv (MPa).
555    pub fv: f64,
556    /// Modulus of elasticity E (MPa).
557    pub e: f64,
558}
559impl LaminatedVeneerLumber {
560    /// Create a standard 1.9E Microllam LVL section (Weyerhaeuser product).
561    pub fn microllam_1_9e(b: f64, d: f64) -> Self {
562        LaminatedVeneerLumber {
563            b,
564            d,
565            fb: 19.3,
566            fc: 19.3,
567            ft: 11.7,
568            fv: 2.07,
569            e: 13_100.0,
570        }
571    }
572    /// Cross-sectional area (mm²).
573    pub fn area(&self) -> f64 {
574        self.b * self.d
575    }
576    /// Moment of inertia (mm⁴).
577    pub fn ix(&self) -> f64 {
578        self.b * self.d.powi(3) / 12.0
579    }
580    /// Section modulus (mm³).
581    pub fn sx(&self) -> f64 {
582        self.b * self.d.powi(2) / 6.0
583    }
584    /// Allowable bending moment (N·mm) using full Fb.
585    pub fn allowable_moment(&self) -> f64 {
586        self.fb * self.sx()
587    }
588    /// Allowable axial compression (N).
589    pub fn allowable_compression(&self) -> f64 {
590        self.fc * self.area()
591    }
592    /// Euler critical buckling load (N) for column, effective length Le (mm).
593    pub fn euler_buckling_load(&self, le: f64) -> f64 {
594        PI.powi(2) * self.e * self.ix() / le.powi(2)
595    }
596    /// Depth-to-width ratio (slenderness check, should be ≤ 5 for typical use).
597    pub fn d_to_b_ratio(&self) -> f64 {
598        self.d / self.b
599    }
600}
601/// Combined geosynthetic reinforcement design for MSE walls.
602#[derive(Debug, Clone)]
603pub struct GeosyntheticReinforcement {
604    /// Geosynthetic type: "geogrid" or "geotextile".
605    pub geosyn_type: String,
606    /// Allowable tensile force per unit width Ta (kN/m).
607    pub ta: f64,
608    /// Vertical spacing of layers sv (m).
609    pub sv: f64,
610    /// Friction angle of fill soil (degrees).
611    pub phi_fill: f64,
612    /// Unit weight of fill γ (kN/m³).
613    pub gamma_fill: f64,
614    /// Coverage ratio Rc.
615    pub rc: f64,
616}
617impl GeosyntheticReinforcement {
618    /// Create a geosynthetic reinforcement design.
619    pub fn new(
620        geosyn_type: &str,
621        ta: f64,
622        sv: f64,
623        phi_fill: f64,
624        gamma_fill: f64,
625        rc: f64,
626    ) -> Self {
627        GeosyntheticReinforcement {
628            geosyn_type: geosyn_type.to_string(),
629            ta,
630            sv,
631            phi_fill,
632            gamma_fill,
633            rc,
634        }
635    }
636    /// Maximum horizontal stress at depth z (kPa).
637    pub fn horizontal_stress(&self, z: f64) -> f64 {
638        let ka = ((PI / 4.0 - self.phi_fill.to_radians() / 2.0).tan()).powi(2);
639        ka * self.gamma_fill * z
640    }
641    /// Required strength at depth z per unit width (kN/m).
642    pub fn required_strength(&self, z: f64) -> f64 {
643        self.horizontal_stress(z) * self.sv
644    }
645    /// Factor of safety in tension.
646    pub fn tension_fos(&self, z: f64) -> f64 {
647        let t_req = self.required_strength(z);
648        if t_req < 1e-10 {
649            return f64::INFINITY;
650        }
651        self.ta / t_req
652    }
653    /// Pullout length required (m) per FHWA.
654    pub fn pullout_length(&self, z: f64, fos: f64) -> f64 {
655        let t_max = self.required_strength(z);
656        let sigma_v = self.gamma_fill * z;
657        let f_star = 0.67 * self.phi_fill.to_radians().tan();
658        let denom = 2.0 * self.rc * f_star * sigma_v;
659        if denom < 1e-10 {
660            return 1.0;
661        }
662        (t_max * fos / denom).max(1.0)
663    }
664}
665/// Steel rebar bond and development length (ACI 318).
666pub struct SteelRebarBond {
667    /// Bar diameter db (mm).
668    pub db: f64,
669    /// Bar yield strength fy (MPa).
670    pub fy: f64,
671    /// Concrete compressive strength fc (MPa).
672    pub fc: f64,
673    /// Cover to center of bar (mm).
674    pub cover: f64,
675}
676impl SteelRebarBond {
677    /// Create a new rebar bond object.
678    pub fn new(db: f64, fy: f64, fc: f64, cover: f64) -> Self {
679        SteelRebarBond { db, fy, fc, cover }
680    }
681    /// Basic development length ld (mm) per ACI 318-19 (simplified).
682    pub fn development_length(&self) -> f64 {
683        let psi_t = 1.0;
684        let psi_e = 1.0;
685        let lambda = 1.0;
686        (self.fy * psi_t * psi_e) / (1.1 * lambda * self.fc.sqrt()) * self.db
687    }
688    /// Standard 90° hook development length ldh (mm).
689    pub fn hook_development_length(&self) -> f64 {
690        let ldh_basic = 0.24 * self.fy * self.db / (self.fc.sqrt());
691        ldh_basic.max(8.0 * self.db).max(150.0)
692    }
693    /// Bond stress u = fy * As / (π * db * ld) assuming uniform distribution.
694    pub fn average_bond_stress(&self) -> f64 {
695        let ld = self.development_length();
696        self.fy / (PI * ld / self.db)
697    }
698}
699/// Chemical admixture dosage and effect.
700#[derive(Debug, Clone)]
701pub struct Admixture {
702    /// Type of admixture.
703    pub admixture_type: AdmixtureType,
704    /// Dosage (% by mass of cement).
705    pub dosage: f64,
706    /// Manufacturer-stated water reduction (%).
707    pub water_reduction_pct: f64,
708    /// Setting time modification (minutes, positive = retard, negative = accelerate).
709    pub setting_time_delta: f64,
710}
711impl Admixture {
712    /// Create a new admixture.
713    pub fn new(
714        admixture_type: AdmixtureType,
715        dosage: f64,
716        water_reduction_pct: f64,
717        setting_time_delta: f64,
718    ) -> Self {
719        Admixture {
720            admixture_type,
721            dosage,
722            water_reduction_pct,
723            setting_time_delta,
724        }
725    }
726    /// Effective water content after admixture (kg/m³).
727    pub fn adjusted_water(&self, base_water: f64) -> f64 {
728        base_water * (1.0 - self.water_reduction_pct / 100.0)
729    }
730    /// Effective w/c ratio after water reduction.
731    pub fn adjusted_wc_ratio(&self, base_wc: f64) -> f64 {
732        base_wc * (1.0 - self.water_reduction_pct / 100.0)
733    }
734    /// Is this admixture a set accelerator?
735    pub fn is_accelerator(&self) -> bool {
736        self.admixture_type == AdmixtureType::Accelerator
737    }
738}
739/// Cross-laminated timber (CLT) panel properties.
740#[derive(Debug, Clone)]
741pub struct CrossLaminatedTimber {
742    /// Panel width (mm).
743    pub width: f64,
744    /// Panel total thickness (mm).
745    pub thickness: f64,
746    /// Number of layers (odd number).
747    pub n_layers: u32,
748    /// Layer thickness (mm), all equal assumed.
749    pub layer_thickness: f64,
750    /// Parallel-to-grain MOE E0 (MPa).
751    pub e0: f64,
752    /// Perpendicular-to-grain MOE E90 (MPa).
753    pub e90: f64,
754    /// Bending strength Fb (MPa) for parallel layers.
755    pub fb: f64,
756    /// Rolling shear modulus Grt (MPa).
757    pub g_rolling: f64,
758}
759impl CrossLaminatedTimber {
760    /// Create a 5-layer CLT panel with standard properties.
761    pub fn five_layer(width: f64, total_thickness: f64) -> Self {
762        let n_layers = 5u32;
763        let layer_t = total_thickness / n_layers as f64;
764        CrossLaminatedTimber {
765            width,
766            thickness: total_thickness,
767            n_layers,
768            layer_thickness: layer_t,
769            e0: 11_000.0,
770            e90: 370.0,
771            fb: 24.0,
772            g_rolling: 65.0,
773        }
774    }
775    /// Effective bending stiffness EIeff (N·mm²) per unit width using gamma method.
776    pub fn effective_bending_stiffness(&self) -> f64 {
777        let n_par = self.n_layers.div_ceil(2);
778        let h = self.layer_thickness;
779        let mut ei = 0.0;
780        for i in 0..n_par {
781            let yi = (self.thickness / 2.0) - h / 2.0 - i as f64 * 2.0 * h;
782            ei += self.e0 * self.width * h.powi(3) / 12.0 + self.e0 * self.width * h * yi.powi(2);
783        }
784        ei
785    }
786    /// Effective axial stiffness EAeff (N/mm).
787    pub fn effective_axial_stiffness(&self) -> f64 {
788        let n_par = self.n_layers.div_ceil(2);
789        self.e0 * self.width * self.layer_thickness * n_par as f64
790    }
791    /// Rolling shear stress capacity check τ_max (MPa) per unit width.
792    pub fn rolling_shear_capacity(&self) -> f64 {
793        self.g_rolling * self.layer_thickness / self.width
794    }
795    /// Panel area (mm²/m width).
796    pub fn area_per_m(&self) -> f64 {
797        self.thickness * 1000.0
798    }
799}
800/// Masonry shear wall capacity (TMS 402 / MSJC).
801#[derive(Debug, Clone)]
802pub struct MasonryShearWall {
803    /// Wall length (mm).
804    pub length: f64,
805    /// Wall thickness (mm).
806    pub thickness: f64,
807    /// Wall height (mm).
808    pub height: f64,
809    /// f'm — masonry prism strength (MPa).
810    pub fm: f64,
811    /// Area of vertical steel (mm²/m length).
812    pub av: f64,
813    /// Steel yield strength fy (MPa).
814    pub fy: f64,
815    /// Normal axial stress from vertical loads (MPa).
816    pub sigma_v: f64,
817}
818impl MasonryShearWall {
819    /// Create a masonry shear wall.
820    pub fn new(
821        length: f64,
822        thickness: f64,
823        height: f64,
824        fm: f64,
825        av: f64,
826        fy: f64,
827        sigma_v: f64,
828    ) -> Self {
829        MasonryShearWall {
830            length,
831            thickness,
832            height,
833            fm,
834            av,
835            fy,
836            sigma_v,
837        }
838    }
839    /// Net shear area An (mm²).
840    pub fn net_area(&self) -> f64 {
841        self.length * self.thickness
842    }
843    /// In-plane shear capacity Vn (N) per TMS 402 (simplified).
844    pub fn in_plane_shear_capacity(&self) -> f64 {
845        let m_v_d = (self.height / self.length).min(1.0);
846        let an = self.net_area();
847        let p = self.sigma_v * an;
848        let vnm = (4.0 - 1.75 * m_v_d) * an * self.fm.sqrt() + 0.25 * p;
849        let vns = 0.5 * self.av * self.fy * self.length / 1000.0;
850        (vnm + vns).min(6.0 * an * self.fm.sqrt())
851    }
852    /// Out-of-plane flexural capacity Mn (N·mm) per unit height.
853    pub fn out_of_plane_moment_capacity(&self) -> f64 {
854        let d = self.thickness / 2.0;
855        self.av * self.fy * d / 1000.0
856    }
857    /// Aspect ratio h/l (shear wall slenderness).
858    pub fn aspect_ratio(&self) -> f64 {
859        self.height / self.length
860    }
861}
862/// Driven pile foundation capacity.
863#[derive(Debug, Clone)]
864pub struct PileFoundation {
865    /// Pile diameter or width (m).
866    pub diameter: f64,
867    /// Pile length L (m).
868    pub length: f64,
869    /// Pile type: "concrete", "steel", "timber".
870    pub pile_type: String,
871    /// Unit skin friction qs (kPa) along pile shaft.
872    pub qs: f64,
873    /// Unit end bearing qb (kPa) at pile tip.
874    pub qb: f64,
875    /// Soil cohesion cu (kPa).
876    pub cu: f64,
877    /// Adhesion factor α.
878    pub alpha: f64,
879}
880impl PileFoundation {
881    /// Create a concrete driven pile.
882    pub fn concrete_pile(diameter: f64, length: f64, qs: f64, qb: f64) -> Self {
883        PileFoundation {
884            diameter,
885            length,
886            pile_type: "concrete".to_string(),
887            qs,
888            qb,
889            cu: qs,
890            alpha: 0.5,
891        }
892    }
893    /// Pile perimeter (m).
894    pub fn perimeter(&self) -> f64 {
895        PI * self.diameter
896    }
897    /// Pile tip area (m²).
898    pub fn tip_area(&self) -> f64 {
899        PI * (self.diameter / 2.0).powi(2)
900    }
901    /// Ultimate skin friction (kN).
902    pub fn skin_friction(&self) -> f64 {
903        self.qs * self.perimeter() * self.length
904    }
905    /// Ultimate end bearing (kN).
906    pub fn end_bearing(&self) -> f64 {
907        self.qb * self.tip_area()
908    }
909    /// Ultimate pile capacity Qu (kN).
910    pub fn ultimate_capacity(&self) -> f64 {
911        self.skin_friction() + self.end_bearing()
912    }
913    /// Allowable pile capacity Qa (kN) with FOS = 2.5.
914    pub fn allowable_capacity(&self) -> f64 {
915        self.ultimate_capacity() / 2.5
916    }
917    /// α-method skin friction for cohesive soils (kN).
918    pub fn alpha_skin_friction(&self) -> f64 {
919        self.alpha * self.cu * self.perimeter() * self.length
920    }
921    /// Settlement under working load (mm) — elastic compression of pile.
922    pub fn elastic_compression(&self, load_kn: f64) -> f64 {
923        let e_pile = if self.pile_type == "concrete" {
924            25_000.0
925        } else {
926            200_000.0
927        };
928        let area_mm2 = self.tip_area() * 1e6;
929        let load_n = load_kn * 1000.0;
930        let length_mm = self.length * 1000.0;
931        load_n * length_mm / (e_pile * area_mm2)
932    }
933}
934/// Concrete material properties per ACI/EN standard.
935pub struct ConcreteMaterial {
936    /// Characteristic compressive strength f'c (MPa).
937    pub fc: f64,
938    /// Tensile strength ft (MPa), typically ≈ 0.1 * fc.
939    pub ft: f64,
940    /// Modulus of elasticity Ec (MPa).
941    pub ec: f64,
942    /// Poisson's ratio ν.
943    pub poisson: f64,
944    /// Drying shrinkage strain ε_sh (dimensionless).
945    pub shrinkage_strain: f64,
946    /// Creep coefficient φ (ratio of creep strain to elastic strain).
947    pub creep_coefficient: f64,
948    /// Density (kg/m³).
949    pub density: f64,
950}
951impl ConcreteMaterial {
952    /// Create a normal-weight concrete with characteristic strength `fc` (MPa).
953    ///
954    /// Ec is estimated by ACI 318: Ec = 4700 * sqrt(fc) (MPa).
955    pub fn new(fc: f64) -> Self {
956        let ec = 4700.0 * fc.sqrt();
957        let ft = 0.1 * fc;
958        ConcreteMaterial {
959            fc,
960            ft,
961            ec,
962            poisson: 0.2,
963            shrinkage_strain: 3e-4,
964            creep_coefficient: 2.0,
965            density: 2400.0,
966        }
967    }
968    /// Shear modulus G = Ec / (2*(1+ν)).
969    pub fn shear_modulus(&self) -> f64 {
970        self.ec / (2.0 * (1.0 + self.poisson))
971    }
972    /// Splitting tensile strength (ACI): fct = 0.56 * sqrt(fc) MPa.
973    pub fn splitting_tensile_strength(&self) -> f64 {
974        0.56 * self.fc.sqrt()
975    }
976    /// Ultimate compressive strain (ACI): 0.003.
977    pub fn ultimate_compressive_strain(&self) -> f64 {
978        0.003
979    }
980    /// Modulus of rupture (flexural): fr = 0.62 * sqrt(fc) MPa.
981    pub fn modulus_of_rupture(&self) -> f64 {
982        0.62 * self.fc.sqrt()
983    }
984}
985/// Reinforced concrete section for moment/shear capacity (ACI 318).
986pub struct ReinforcedConcrete {
987    /// Concrete material.
988    pub concrete: ConcreteMaterial,
989    /// Width of the compression block (mm).
990    pub b: f64,
991    /// Effective depth to tension steel (mm).
992    pub d: f64,
993    /// Area of tension steel (mm²).
994    pub as_t: f64,
995    /// Yield strength of steel fy (MPa).
996    pub fy: f64,
997    /// Area of compression steel (mm²), if any.
998    pub as_comp: f64,
999    /// Depth to compression steel (mm).
1000    pub d_prime: f64,
1001}
1002impl ReinforcedConcrete {
1003    /// Create a new reinforced concrete section.
1004    #[allow(clippy::too_many_arguments)]
1005    pub fn new(
1006        concrete: ConcreteMaterial,
1007        b: f64,
1008        d: f64,
1009        as_t: f64,
1010        fy: f64,
1011        as_comp: f64,
1012        d_prime: f64,
1013    ) -> Self {
1014        ReinforcedConcrete {
1015            concrete,
1016            b,
1017            d,
1018            as_t,
1019            fy,
1020            as_comp,
1021            d_prime,
1022        }
1023    }
1024    /// Nominal moment capacity Mn (N·mm) using ACI rectangular stress block.
1025    pub fn moment_capacity(&self) -> f64 {
1026        let fc = self.concrete.fc;
1027        let beta1 = if fc <= 28.0 {
1028            0.85
1029        } else {
1030            (0.85 - 0.05 * (fc - 28.0) / 7.0).max(0.65)
1031        };
1032        let a = self.as_t * self.fy / (0.85 * fc * self.b);
1033        let c = a / beta1;
1034        let eps_cu = 0.003;
1035        let eps_prime = eps_cu * (c - self.d_prime) / c;
1036        let fs_prime = if eps_prime >= self.fy / 200_000.0 {
1037            self.fy
1038        } else {
1039            eps_prime * 200_000.0
1040        };
1041
1042        self.as_t * self.fy * (self.d - a / 2.0) + self.as_comp * fs_prime * (self.d - self.d_prime)
1043    }
1044    /// Design moment capacity φMn (φ = 0.9 for tension-controlled sections).
1045    pub fn design_moment_capacity(&self) -> f64 {
1046        0.9 * self.moment_capacity()
1047    }
1048    /// Nominal shear capacity Vn (N) per ACI 318: Vn = Vc + Vs.
1049    /// Concrete shear: Vc = 0.17 * sqrt(fc) * b * d.
1050    pub fn shear_capacity(&self) -> f64 {
1051        0.17 * self.concrete.fc.sqrt() * self.b * self.d
1052    }
1053    /// Reinforcement ratio ρ = As / (b * d).
1054    pub fn reinforcement_ratio(&self) -> f64 {
1055        self.as_t / (self.b * self.d)
1056    }
1057    /// Minimum steel ratio ρ_min per ACI 318.
1058    pub fn rho_min(&self) -> f64 {
1059        let term1 = 0.25 * self.concrete.fc.sqrt() / self.fy;
1060        let term2 = 1.4 / self.fy;
1061        term1.max(term2)
1062    }
1063}
1064/// Hollow Structural Section (HSS / RHS) properties.
1065#[derive(Debug, Clone)]
1066pub struct HssSection {
1067    /// Outer width B (mm).
1068    pub b: f64,
1069    /// Outer height H (mm).
1070    pub h: f64,
1071    /// Wall thickness t (mm).
1072    pub t: f64,
1073    /// Yield strength Fy (MPa).
1074    pub fy: f64,
1075    /// Modulus of elasticity E (MPa).
1076    pub e: f64,
1077}
1078impl HssSection {
1079    /// Create a rectangular HSS section.
1080    pub fn rectangular(b: f64, h: f64, t: f64, fy: f64) -> Self {
1081        HssSection {
1082            b,
1083            h,
1084            t,
1085            fy,
1086            e: 200_000.0,
1087        }
1088    }
1089    /// Create a square HSS section.
1090    pub fn square(b: f64, t: f64, fy: f64) -> Self {
1091        HssSection {
1092            b,
1093            h: b,
1094            t,
1095            fy,
1096            e: 200_000.0,
1097        }
1098    }
1099    /// Cross-sectional area (mm²).
1100    pub fn area(&self) -> f64 {
1101        self.b * self.h - (self.b - 2.0 * self.t) * (self.h - 2.0 * self.t)
1102    }
1103    /// Moment of inertia about strong axis Ix (mm⁴).
1104    pub fn ix(&self) -> f64 {
1105        (self.b * self.h.powi(3) - (self.b - 2.0 * self.t) * (self.h - 2.0 * self.t).powi(3)) / 12.0
1106    }
1107    /// Moment of inertia about weak axis Iy (mm⁴).
1108    pub fn iy(&self) -> f64 {
1109        (self.h * self.b.powi(3) - (self.h - 2.0 * self.t) * (self.b - 2.0 * self.t).powi(3)) / 12.0
1110    }
1111    /// Elastic section modulus Sx (mm³).
1112    pub fn sx(&self) -> f64 {
1113        self.ix() / (self.h / 2.0)
1114    }
1115    /// Plastic section modulus Zx (mm³) (approximate for rectangular HSS).
1116    pub fn zx(&self) -> f64 {
1117        let outer = self.b * self.h.powi(2) / 4.0;
1118        let inner = (self.b - 2.0 * self.t) * (self.h - 2.0 * self.t).powi(2) / 4.0;
1119        outer - inner
1120    }
1121    /// Nominal moment capacity Mp = Zx * Fy (N·mm).
1122    pub fn plastic_moment(&self) -> f64 {
1123        self.zx() * self.fy
1124    }
1125    /// Torsional constant J (mm⁴) for closed section.
1126    pub fn torsional_constant(&self) -> f64 {
1127        let h_mid = self.h - self.t;
1128        let b_mid = self.b - self.t;
1129        2.0 * self.t * b_mid * h_mid * (b_mid * h_mid) / (b_mid + h_mid)
1130    }
1131    /// Warping constant Cw ≈ 0 for closed HSS (negligible).
1132    pub fn warping_constant(&self) -> f64 {
1133        0.0
1134    }
1135    /// Slenderness ratio for local buckling (web): h/t.
1136    pub fn web_slenderness(&self) -> f64 {
1137        (self.h - 2.0 * self.t) / self.t
1138    }
1139    /// Slenderness ratio for local buckling (flange): b/t.
1140    pub fn flange_slenderness(&self) -> f64 {
1141        (self.b - 2.0 * self.t) / self.t
1142    }
1143    /// AISC compact limit for HSS flanges: λp = 1.12 * sqrt(E/Fy).
1144    pub fn compact_flange_limit(&self) -> f64 {
1145        1.12 * (self.e / self.fy).sqrt()
1146    }
1147    /// Check if flange is compact.
1148    pub fn flange_is_compact(&self) -> bool {
1149        self.flange_slenderness() <= self.compact_flange_limit()
1150    }
1151}
1152/// Eurocode EN 1990 load combinations (fundamental combination, ULS).
1153#[derive(Debug, Clone)]
1154pub struct EurocodeLoad {
1155    /// Permanent action Gk (kN or kN/m²).
1156    pub gk: f64,
1157    /// Variable leading action Qk1 (kN or kN/m²).
1158    pub qk1: f64,
1159    /// Variable accompanying action Qk2 (kN or kN/m²).
1160    pub qk2: f64,
1161    /// Wind action Wk (kN or kN/m²).
1162    pub wk: f64,
1163    /// Snow action Sk (kN or kN/m²).
1164    pub sk: f64,
1165    /// Seismic action Ed (kN or kN/m²).
1166    pub ed: f64,
1167}
1168impl EurocodeLoad {
1169    /// Create a Eurocode load set.
1170    pub fn new(gk: f64, qk1: f64, qk2: f64, wk: f64, sk: f64, ed: f64) -> Self {
1171        EurocodeLoad {
1172            gk,
1173            qk1,
1174            qk2,
1175            wk,
1176            sk,
1177            ed,
1178        }
1179    }
1180    /// STR/GEO ULS fundamental combination (Eq. 6.10): γG*Gk + γQ1*Qk1 + Σγ_Qi*ψ0i*Qki.
1181    /// γG = 1.35, γQ = 1.50, ψ0 = 0.7 for imposed, 0.6 for wind/snow.
1182    pub fn uls_combo_610(&self) -> f64 {
1183        1.35 * self.gk + 1.50 * self.qk1 + 1.50 * 0.7 * self.qk2
1184    }
1185    /// ULS Eq. 6.10a (γG*Gk + 1.5*ψ0*Qk1): generally less critical.
1186    pub fn uls_combo_610a(&self) -> f64 {
1187        1.35 * self.gk + 1.50 * 0.7 * self.qk1
1188    }
1189    /// ULS Eq. 6.10b (ξ*γG*Gk + 1.5*Qk1): ξ = 0.85 reduction on permanent action.
1190    pub fn uls_combo_610b(&self) -> f64 {
1191        0.85 * 1.35 * self.gk + 1.50 * self.qk1
1192    }
1193    /// ULS accidental combination with seismic: Gk + Ed + ψ2*Qk.
1194    /// ψ2 = 0.3 for residential.
1195    pub fn uls_seismic(&self) -> f64 {
1196        self.gk + self.ed + 0.3 * self.qk1
1197    }
1198    /// Characteristic SLS combination: Gk + Qk1 + ψ0*Qk2.
1199    pub fn sls_characteristic(&self) -> f64 {
1200        self.gk + self.qk1 + 0.7 * self.qk2
1201    }
1202    /// Frequent SLS combination: Gk + ψ1*Qk1 + ψ2*Qk2. ψ1=0.5, ψ2=0.3.
1203    pub fn sls_frequent(&self) -> f64 {
1204        self.gk + 0.5 * self.qk1 + 0.3 * self.qk2
1205    }
1206    /// Quasi-permanent SLS combination: Gk + ψ2*Qk1. ψ2=0.3.
1207    pub fn sls_quasi_permanent(&self) -> f64 {
1208        self.gk + 0.3 * self.qk1
1209    }
1210    /// Governing ULS combination.
1211    pub fn governing_uls(&self) -> f64 {
1212        [
1213            self.uls_combo_610(),
1214            self.uls_combo_610a(),
1215            self.uls_combo_610b(),
1216        ]
1217        .iter()
1218        .cloned()
1219        .fold(f64::NEG_INFINITY, f64::max)
1220    }
1221    /// Add wind to ULS: Gk + Wk combination (uplift check).
1222    pub fn uls_wind_uplift(&self) -> f64 {
1223        0.9 * self.gk + 1.50 * self.wk
1224    }
1225    /// Snow load ULS: 1.35*Gk + 1.5*Sk.
1226    pub fn uls_snow(&self) -> f64 {
1227        1.35 * self.gk + 1.50 * self.sk
1228    }
1229}
1230/// C-channel (American Standard Channel) steel section properties.
1231#[derive(Debug, Clone)]
1232pub struct ChannelSection {
1233    /// Overall depth d (mm).
1234    pub d: f64,
1235    /// Flange width bf (mm).
1236    pub bf: f64,
1237    /// Flange thickness tf (mm).
1238    pub tf: f64,
1239    /// Web thickness tw (mm).
1240    pub tw: f64,
1241    /// Yield strength Fy (MPa).
1242    pub fy: f64,
1243}
1244impl ChannelSection {
1245    /// Create a C-channel section.
1246    pub fn new(d: f64, bf: f64, tf: f64, tw: f64, fy: f64) -> Self {
1247        ChannelSection { d, bf, tf, tw, fy }
1248    }
1249    /// Cross-sectional area (mm²).
1250    pub fn area(&self) -> f64 {
1251        2.0 * self.bf * self.tf + (self.d - 2.0 * self.tf) * self.tw
1252    }
1253    /// Shear center location ex (mm) from web face.
1254    pub fn shear_center_x(&self) -> f64 {
1255        let bf = self.bf;
1256        let tf = self.tf;
1257        let d = self.d;
1258        let tw = self.tw;
1259        let hw = d - 2.0 * tf;
1260        let ix = self.ix();
1261        let sx = if d > 0.0 { ix / (d / 2.0) } else { 1.0 };
1262        bf.powi(2) * tf / (2.0 * sx / d) / (1.0 + (hw * tw) / (6.0 * bf * tf))
1263    }
1264    /// Moment of inertia Ix (mm⁴).
1265    pub fn ix(&self) -> f64 {
1266        let hw = self.d - 2.0 * self.tf;
1267        self.bf * self.d.powi(3) / 12.0 - (self.bf - self.tw) * hw.powi(3) / 12.0
1268    }
1269    /// Elastic section modulus Sx (mm³).
1270    pub fn sx(&self) -> f64 {
1271        self.ix() / (self.d / 2.0)
1272    }
1273    /// Nominal moment capacity Mn (N·mm).
1274    pub fn moment_capacity(&self) -> f64 {
1275        self.sx() * self.fy
1276    }
1277    /// Shear capacity (simplified) Vn (N).
1278    pub fn shear_capacity(&self) -> f64 {
1279        let hw = self.d - 2.0 * self.tf;
1280        0.6 * self.fy * hw * self.tw
1281    }
1282}
1283/// Type of chemical admixture.
1284#[derive(Debug, Clone, PartialEq)]
1285pub enum AdmixtureType {
1286    /// Water reducer / plasticiser.
1287    WaterReducer,
1288    /// Superplasticiser (high-range water reducer).
1289    Superplasticiser,
1290    /// Accelerator (increases early strength).
1291    Accelerator,
1292    /// Retarder (extends workability time).
1293    Retarder,
1294    /// Air entrainer.
1295    AirEntrainer,
1296    /// Shrinkage-reducing admixture.
1297    ShrinkageReducer,
1298    /// Corrosion inhibitor.
1299    CorrosionInhibitor,
1300}
1301/// Moisture content effects on timber mechanical properties (NDS Table 4A).
1302#[derive(Debug, Clone)]
1303pub struct MoistureCorrectionTimber {
1304    /// Moisture content MC (%) in service.
1305    pub mc_service: f64,
1306    /// Fiber saturation point MC_fsp (%), typically 28–30%.
1307    pub mc_fsp: f64,
1308    /// Green MOE (MPa) at MC = MC_fsp.
1309    pub e_green: f64,
1310    /// Green Fb (MPa).
1311    pub fb_green: f64,
1312}
1313impl MoistureCorrectionTimber {
1314    /// Create a moisture correction model for Douglas Fir.
1315    pub fn douglas_fir() -> Self {
1316        MoistureCorrectionTimber {
1317            mc_service: 15.0,
1318            mc_fsp: 28.0,
1319            e_green: 11_000.0,
1320            fb_green: 40.0,
1321        }
1322    }
1323    /// Compute CM factor for modulus of elasticity (NDS Table 4A).
1324    pub fn cm_factor_e(&self) -> f64 {
1325        if self.mc_service >= self.mc_fsp {
1326            return 1.0;
1327        }
1328        if self.mc_service <= 19.0 {
1329            1.0
1330        } else {
1331            1.0 - 0.1 * (self.mc_service - 19.0) / (self.mc_fsp - 19.0)
1332        }
1333    }
1334    /// Compute CM factor for bending strength Fb (NDS Table 4A).
1335    pub fn cm_factor_fb(&self) -> f64 {
1336        if self.mc_service >= self.mc_fsp {
1337            return 0.85;
1338        }
1339        if self.mc_service <= 19.0 {
1340            1.0
1341        } else {
1342            0.85 + 0.15 * (self.mc_fsp - self.mc_service) / (self.mc_fsp - 19.0)
1343        }
1344    }
1345    /// Adjusted MOE (MPa) at service MC.
1346    pub fn adjusted_e(&self) -> f64 {
1347        self.e_green * self.cm_factor_e()
1348    }
1349    /// Adjusted Fb (MPa) at service MC.
1350    pub fn adjusted_fb(&self) -> f64 {
1351        self.fb_green * self.cm_factor_fb()
1352    }
1353    /// Shrinkage coefficient: % shrinkage per % MC change (tangential direction).
1354    pub fn tangential_shrinkage_per_pct_mc(&self) -> f64 {
1355        0.29
1356    }
1357    /// Dimensional change for MC change from green to service.
1358    pub fn dimensional_change(&self, dimension_mm: f64) -> f64 {
1359        let delta_mc = (self.mc_fsp - self.mc_service).max(0.0);
1360        dimension_mm * self.tangential_shrinkage_per_pct_mc() * delta_mc / 100.0
1361    }
1362}
1363/// Geotextile filter design for drainage and erosion control.
1364#[derive(Debug, Clone)]
1365pub struct GeotextileFilter {
1366    /// Apparent opening size AOS (O95, mm).
1367    pub aos: f64,
1368    /// Permittivity ψ (s⁻¹).
1369    pub permittivity: f64,
1370    /// Transmissivity θ (m²/s).
1371    pub transmissivity: f64,
1372    /// Tensile strength (kN/m).
1373    pub tensile_strength: f64,
1374    /// Elongation at break (%).
1375    pub elongation: f64,
1376    /// Soil D85 particle size (mm).
1377    pub soil_d85: f64,
1378    /// Soil D15 particle size (mm).
1379    pub soil_d15: f64,
1380    /// In-plane hydraulic conductivity kp (m/s).
1381    pub kp: f64,
1382    /// Normal hydraulic conductivity kn (m/s).
1383    pub kn: f64,
1384}
1385impl GeotextileFilter {
1386    /// Create a typical woven geotextile for road subbase drainage.
1387    pub fn woven_road_drainage() -> Self {
1388        GeotextileFilter {
1389            aos: 0.212,
1390            permittivity: 0.5,
1391            transmissivity: 1e-4,
1392            tensile_strength: 40.0,
1393            elongation: 15.0,
1394            soil_d85: 0.3,
1395            soil_d15: 0.05,
1396            kp: 1e-3,
1397            kn: 1e-4,
1398        }
1399    }
1400    /// Check retention criterion: O95 ≤ B * D85.
1401    /// B = 1.0–2.0 depending on soil uniformity.
1402    pub fn retention_criterion_met(&self, b: f64) -> bool {
1403        self.aos <= b * self.soil_d85
1404    }
1405    /// Check permeability criterion: kn ≥ ksoil.
1406    pub fn permeability_criterion_met(&self, k_soil: f64) -> bool {
1407        self.kn >= k_soil
1408    }
1409    /// Gradient ratio test: index of clogging potential (target ≤ 3).
1410    pub fn gradient_ratio(&self) -> f64 {
1411        if self.soil_d15 < 1e-10 {
1412            return f64::INFINITY;
1413        }
1414        self.aos / self.soil_d15
1415    }
1416    /// Filter ratio (coarse side): AOS / D85 should be ≤ 2.0 for woven.
1417    pub fn filter_ratio(&self) -> f64 {
1418        if self.soil_d85 < 1e-10 {
1419            return f64::INFINITY;
1420        }
1421        self.aos / self.soil_d85
1422    }
1423    /// Required width for overlapping in a trench drain (m).
1424    pub fn overlap_width(&self, trench_depth_m: f64) -> f64 {
1425        (0.3_f64).max(trench_depth_m * 0.5)
1426    }
1427}
1428/// Geogrid properties for soil reinforcement.
1429#[derive(Debug, Clone)]
1430pub struct Geogrid {
1431    /// Ultimate tensile strength in machine direction (kN/m).
1432    pub tult_md: f64,
1433    /// Ultimate tensile strength in cross-machine direction (kN/m).
1434    pub tult_cmd: f64,
1435    /// Junction efficiency (%).
1436    pub junction_efficiency: f64,
1437    /// Aperture size (mm).
1438    pub aperture_size: f64,
1439    /// Long-term design strength (kN/m) after creep reduction.
1440    pub ltds: f64,
1441    /// Coverage ratio (ratio of solid area to total area).
1442    pub coverage_ratio: f64,
1443}
1444impl Geogrid {
1445    /// Create a typical biaxial polypropylene geogrid (BX-1100).
1446    pub fn bx1100() -> Self {
1447        Geogrid {
1448            tult_md: 15.0,
1449            tult_cmd: 20.0,
1450            junction_efficiency: 93.0,
1451            aperture_size: 33.0,
1452            ltds: 8.0,
1453            coverage_ratio: 0.35,
1454        }
1455    }
1456    /// Create a uniaxial HDPE geogrid (UX-1500HS).
1457    pub fn ux1500hs() -> Self {
1458        Geogrid {
1459            tult_md: 150.0,
1460            tult_cmd: 25.0,
1461            junction_efficiency: 91.0,
1462            aperture_size: 16.0,
1463            ltds: 80.0,
1464            coverage_ratio: 0.70,
1465        }
1466    }
1467    /// Reduction factor for installation damage RF_ID (typically 1.1–1.4).
1468    /// Long-term allowable strength = LTDS / RF_ID / RF_creep.
1469    pub fn allowable_strength(&self, rf_id: f64, rf_cr: f64) -> f64 {
1470        self.ltds / (rf_id * rf_cr)
1471    }
1472    /// Interaction coefficient for soil-geogrid friction Ci.
1473    /// `phi_soil` = soil friction angle (degrees).
1474    pub fn interaction_coefficient(&self, phi_soil: f64) -> f64 {
1475        let base_ci = 0.8;
1476        let phi = phi_soil.to_radians();
1477        base_ci * phi.tan() / phi.tan()
1478    }
1479    /// Passive resistance contribution τ_p (kPa) per geogrid layer.
1480    pub fn passive_resistance(&self, sigma_v: f64) -> f64 {
1481        self.ltds * sigma_v / 100.0
1482    }
1483}
1484/// Structural load combinations and load factors (ASCE 7 LRFD).
1485#[derive(Debug, Clone)]
1486pub struct StructuralLoad {
1487    /// Dead load D (kN or kN/m²).
1488    pub dead: f64,
1489    /// Live load L (kN or kN/m²).
1490    pub live: f64,
1491    /// Wind load W (kN or kN/m²).
1492    pub wind: f64,
1493    /// Seismic load E (kN or kN/m²).
1494    pub seismic: f64,
1495    /// Snow load S (kN or kN/m²).
1496    pub snow: f64,
1497}
1498impl StructuralLoad {
1499    /// Create a new structural load set.
1500    pub fn new(dead: f64, live: f64, wind: f64, seismic: f64, snow: f64) -> Self {
1501        StructuralLoad {
1502            dead,
1503            live,
1504            wind,
1505            seismic,
1506            snow,
1507        }
1508    }
1509    /// LRFD load combination 1: 1.4D.
1510    pub fn lrfd_combo1(&self) -> f64 {
1511        1.4 * self.dead
1512    }
1513    /// LRFD load combination 2: 1.2D + 1.6L + 0.5S.
1514    pub fn lrfd_combo2(&self) -> f64 {
1515        1.2 * self.dead + 1.6 * self.live + 0.5 * self.snow
1516    }
1517    /// LRFD load combination 3: 1.2D + 1.0W + 1.0L + 0.5S.
1518    pub fn lrfd_combo3(&self) -> f64 {
1519        1.2 * self.dead + 1.0 * self.wind + 1.0 * self.live + 0.5 * self.snow
1520    }
1521    /// LRFD load combination 4: 0.9D + 1.0W (overturning check).
1522    pub fn lrfd_combo4(&self) -> f64 {
1523        0.9 * self.dead + 1.0 * self.wind
1524    }
1525    /// LRFD seismic combination: 1.2D + 1.0E + 1.0L + 0.2S.
1526    pub fn lrfd_seismic(&self) -> f64 {
1527        1.2 * self.dead + 1.0 * self.seismic + 1.0 * self.live + 0.2 * self.snow
1528    }
1529    /// ASD service load combination: D + L.
1530    pub fn asd_combo_dl(&self) -> f64 {
1531        self.dead + self.live
1532    }
1533    /// Governing (maximum) LRFD combination.
1534    pub fn governing_lrfd(&self) -> f64 {
1535        [
1536            self.lrfd_combo1(),
1537            self.lrfd_combo2(),
1538            self.lrfd_combo3(),
1539            self.lrfd_combo4(),
1540            self.lrfd_seismic(),
1541        ]
1542        .iter()
1543        .cloned()
1544        .fold(f64::NEG_INFINITY, f64::max)
1545    }
1546}
1547/// Asphalt mixture properties (Marshall mix design).
1548pub struct AsphaltMixture {
1549    /// Bitumen content (% by mass of mix).
1550    pub bitumen_content: f64,
1551    /// Voids in mineral aggregate VMA (%).
1552    pub vma: f64,
1553    /// Voids filled with asphalt VFA (%).
1554    pub vfa: f64,
1555    /// Air voids (%).
1556    pub air_voids: f64,
1557    /// Dynamic stability (rutting resistance) in passes/mm.
1558    pub dynamic_stability: f64,
1559    /// Marshall stability (N).
1560    pub stability: f64,
1561    /// Marshall flow (mm).
1562    pub flow: f64,
1563}
1564impl AsphaltMixture {
1565    /// Create a standard Superpave-12.5 mix design.
1566    pub fn superpave_12_5() -> Self {
1567        AsphaltMixture {
1568            bitumen_content: 5.0,
1569            vma: 14.0,
1570            vfa: 72.0,
1571            air_voids: 4.0,
1572            dynamic_stability: 1000.0,
1573            stability: 8000.0,
1574            flow: 3.0,
1575        }
1576    }
1577    /// Computed bulk density (Gmb) approximation: (100 - AV%) / 100 * Gmm.
1578    /// `gmm` = theoretical maximum density.
1579    pub fn bulk_density(&self, gmm: f64) -> f64 {
1580        (100.0 - self.air_voids) / 100.0 * gmm
1581    }
1582    /// Determine if design meets Superpave 4% air voids criterion.
1583    pub fn meets_air_voids_criterion(&self) -> bool {
1584        (self.air_voids - 4.0).abs() < 0.5
1585    }
1586}
1587/// Wide-flange (I-beam) or channel steel section properties.
1588#[derive(Debug, Clone)]
1589pub struct SteelSection {
1590    /// Cross-sectional area A (mm²).
1591    pub area: f64,
1592    /// Moment of inertia about strong axis Ix (mm⁴).
1593    pub ix: f64,
1594    /// Moment of inertia about weak axis Iy (mm⁴).
1595    pub iy: f64,
1596    /// Plastic section modulus about strong axis Zx (mm³).
1597    pub zx: f64,
1598    /// Plastic section modulus about weak axis Zy (mm³).
1599    pub zy: f64,
1600    /// Yield strength Fy (MPa).
1601    pub fy: f64,
1602    /// Modulus of elasticity E (MPa).
1603    pub e: f64,
1604}
1605impl SteelSection {
1606    /// Create an I-section from flange/web dimensions.
1607    ///
1608    /// `bf` = flange width, `tf` = flange thickness, `d` = total depth,
1609    /// `tw` = web thickness, all in mm.
1610    pub fn i_section(bf: f64, tf: f64, d: f64, tw: f64, fy: f64) -> Self {
1611        let hw = d - 2.0 * tf;
1612        let area = 2.0 * bf * tf + hw * tw;
1613        let ix = bf * d.powi(3) / 12.0 - (bf - tw) * hw.powi(3) / 12.0;
1614        let iy = 2.0 * tf * bf.powi(3) / 12.0 + hw * tw.powi(3) / 12.0;
1615        let zx = bf * tf * (d / 2.0 - tf / 2.0) * 2.0 + tw * hw.powi(2) / 4.0;
1616        let zy = bf.powi(2) * tf / 2.0 + tw.powi(2) * hw / 4.0;
1617        SteelSection {
1618            area,
1619            ix,
1620            iy,
1621            zx,
1622            zy,
1623            fy,
1624            e: 200_000.0,
1625        }
1626    }
1627    /// Elastic section modulus Sx = Ix / (d/2).
1628    pub fn sx(&self, d: f64) -> f64 {
1629        self.ix / (d / 2.0)
1630    }
1631    /// Plastic moment capacity Mp = Zx * Fy (N·mm).
1632    pub fn plastic_moment(&self) -> f64 {
1633        self.zx * self.fy
1634    }
1635    /// Axial load capacity Pn = A * Fy (N) for short columns (no buckling).
1636    pub fn axial_capacity(&self) -> f64 {
1637        self.area * self.fy
1638    }
1639    /// Radius of gyration about strong axis rx = sqrt(Ix/A).
1640    pub fn rx(&self) -> f64 {
1641        (self.ix / self.area).sqrt()
1642    }
1643    /// Radius of gyration about weak axis ry = sqrt(Iy/A).
1644    pub fn ry(&self) -> f64 {
1645        (self.iy / self.area).sqrt()
1646    }
1647    /// Critical buckling stress (Euler) for slenderness ratio KL/r.
1648    pub fn elastic_buckling_stress(&self, kl_over_r: f64) -> f64 {
1649        PI.powi(2) * self.e / kl_over_r.powi(2)
1650    }
1651}
1652/// Foundation soil properties for bearing capacity analysis.
1653pub struct FoundationSoil {
1654    /// Undrained shear strength cu (kPa) for cohesive soils.
1655    pub cu: f64,
1656    /// Effective internal friction angle φ' (degrees).
1657    pub phi_deg: f64,
1658    /// Cohesion c' (kPa) for c-φ soil.
1659    pub c_prime: f64,
1660    /// Soil unit weight γ (kN/m³).
1661    pub gamma: f64,
1662    /// SPT N-value (blows per 300mm).
1663    pub spt_n: u32,
1664    /// Constrained modulus D (MPa) for settlement.
1665    pub constrained_modulus: f64,
1666}
1667impl FoundationSoil {
1668    /// Create a typical medium stiff clay.
1669    pub fn medium_clay() -> Self {
1670        FoundationSoil {
1671            cu: 50.0,
1672            phi_deg: 0.0,
1673            c_prime: 50.0,
1674            gamma: 18.0,
1675            spt_n: 10,
1676            constrained_modulus: 5.0,
1677        }
1678    }
1679    /// Ultimate bearing capacity by Terzaghi (strip footing, general shear).
1680    ///
1681    /// `b` = footing width (m), `df` = depth of foundation (m).
1682    pub fn ultimate_bearing_capacity(&self, b: f64, df: f64) -> f64 {
1683        let phi = self.phi_deg.to_radians();
1684        let (nc, nq, ng) = terzaghi_bearing_factors(phi);
1685        self.c_prime * nc + self.gamma * df * nq + 0.5 * self.gamma * b * ng
1686    }
1687    /// Allowable bearing capacity with factor of safety FOS = 3.
1688    pub fn allowable_bearing_capacity(&self, b: f64, df: f64) -> f64 {
1689        self.ultimate_bearing_capacity(b, df) / 3.0
1690    }
1691    /// Immediate settlement by Boussinesq (elastic, uniform load).
1692    ///
1693    /// `q` = net foundation pressure (kPa), `b` = footing width (m),
1694    /// `es` = Young's modulus of soil (MPa), `nu` = Poisson's ratio.
1695    pub fn immediate_settlement(&self, q: f64, b: f64, es_mpa: f64, nu: f64) -> f64 {
1696        let is = 0.82;
1697        q * b * (1.0 - nu * nu) * is / (es_mpa * 1000.0)
1698    }
1699}
1700/// Pre-stressed concrete section analysis (pre-tensioned and post-tensioned).
1701#[derive(Debug, Clone)]
1702pub struct PrestressedConcrete {
1703    /// Gross cross-section area (mm²).
1704    pub ag: f64,
1705    /// Moment of inertia of gross section (mm⁴).
1706    pub ig: f64,
1707    /// Distance from centroid to bottom fiber (mm).
1708    pub yb: f64,
1709    /// Distance from centroid to top fiber (mm).
1710    pub yt: f64,
1711    /// Prestressing steel area (mm²).
1712    pub aps: f64,
1713    /// Ultimate strength of prestressing steel fpu (MPa).
1714    pub fpu: f64,
1715    /// Initial prestress (MPa) after jacking.
1716    pub fpi: f64,
1717    /// Eccentricity of prestress at midspan (mm).
1718    pub eccentricity: f64,
1719    /// Concrete f'c (MPa).
1720    pub fc: f64,
1721    /// Span length (mm).
1722    pub span: f64,
1723}
1724impl PrestressedConcrete {
1725    /// Create a new pre-stressed concrete section.
1726    #[allow(clippy::too_many_arguments)]
1727    pub fn new(
1728        ag: f64,
1729        ig: f64,
1730        yb: f64,
1731        yt: f64,
1732        aps: f64,
1733        fpu: f64,
1734        fpi: f64,
1735        eccentricity: f64,
1736        fc: f64,
1737        span: f64,
1738    ) -> Self {
1739        PrestressedConcrete {
1740            ag,
1741            ig,
1742            yb,
1743            yt,
1744            aps,
1745            fpu,
1746            fpi,
1747            eccentricity,
1748            fc,
1749            span,
1750        }
1751    }
1752    /// Initial prestress force Pi (N).
1753    pub fn initial_prestress_force(&self) -> f64 {
1754        self.aps * self.fpi
1755    }
1756    /// Effective prestress after losses (using 20% total loss estimate).
1757    pub fn effective_prestress_force(&self, loss_fraction: f64) -> f64 {
1758        self.initial_prestress_force() * (1.0 - loss_fraction)
1759    }
1760    /// Kern distance (upper and lower kern points) for no-tension design.
1761    pub fn upper_kern(&self) -> f64 {
1762        self.ig / (self.ag * self.yb)
1763    }
1764    /// Lower kern distance.
1765    pub fn lower_kern(&self) -> f64 {
1766        self.ig / (self.ag * self.yt)
1767    }
1768    /// Bottom fiber stress at midspan under Pe + M (N/mm²).
1769    /// `pe` = effective prestress force (N), `m` = applied moment (N·mm).
1770    pub fn bottom_fiber_stress(&self, pe: f64, m: f64) -> f64 {
1771        let p_term = -pe / self.ag;
1772        let e_term = -pe * self.eccentricity * self.yb / self.ig;
1773        let m_term = m * self.yb / self.ig;
1774        p_term + e_term + m_term
1775    }
1776    /// Top fiber stress at midspan under Pe + M.
1777    pub fn top_fiber_stress(&self, pe: f64, m: f64) -> f64 {
1778        let p_term = -pe / self.ag;
1779        let e_term = pe * self.eccentricity * self.yt / self.ig;
1780        let m_term = -m * self.yt / self.ig;
1781        p_term + e_term + m_term
1782    }
1783    /// Cracking moment (N·mm): moment at which bottom tensile stress = fr.
1784    pub fn cracking_moment(&self, pe: f64) -> f64 {
1785        let fr = 0.62 * self.fc.sqrt();
1786        let sb = self.ig / self.yb;
1787        (pe / self.ag + pe * self.eccentricity / sb + fr) * sb
1788    }
1789    /// Elastic shortening loss (MPa) for pre-tensioned members (ACI 318 simplified).
1790    pub fn elastic_shortening_loss(&self) -> f64 {
1791        let es_steel = 197_000.0;
1792        let ec = 4700.0 * self.fc.sqrt();
1793        let n = es_steel / ec;
1794        let pe = self.initial_prestress_force();
1795        let fc_cgs = pe / self.ag + pe * self.eccentricity.powi(2) / self.ig;
1796        n * fc_cgs
1797    }
1798    /// Shrinkage loss (MPa) — simplified ACI estimate 70 MPa for pre-tensioned.
1799    pub fn shrinkage_loss(&self) -> f64 {
1800        70.0
1801    }
1802    /// Creep loss (MPa) — simplified: Ccr * n * fc_cgs.
1803    pub fn creep_loss(&self) -> f64 {
1804        let ccr = 2.0;
1805        let n = 197_000.0 / (4700.0 * self.fc.sqrt());
1806        let pe = self.effective_prestress_force(0.0);
1807        let fc_cgs = pe / self.ag + pe * self.eccentricity.powi(2) / self.ig;
1808        ccr * n * fc_cgs
1809    }
1810    /// Total prestress losses (MPa).
1811    pub fn total_losses(&self) -> f64 {
1812        self.elastic_shortening_loss() + self.shrinkage_loss() + self.creep_loss()
1813    }
1814    /// Nominal flexural strength Mn (N·mm) per ACI 318 (fps from strand stress).
1815    pub fn nominal_flexural_strength(&self) -> f64 {
1816        let rho_p = self.aps / (self.ag * 0.8);
1817        let fps = self.fpu * (1.0 - 0.5 * rho_p * self.fpu / self.fc);
1818        let dp = self.yb;
1819        let a = fps * self.aps / (0.85 * self.fc * (self.ag / self.yb));
1820        fps * self.aps * (dp - a / 2.0)
1821    }
1822}
1823/// Concrete cover requirement based on exposure class.
1824#[derive(Debug, Clone, PartialEq)]
1825pub enum ExposureClass {
1826    /// Interior not exposed to weather.
1827    Interior,
1828    /// Exposed to weather — moderate.
1829    Moderate,
1830    /// Exposed to deicers or aggressive chemicals.
1831    Severe,
1832    /// Submerged or buried.
1833    Submerged,
1834}
1835/// Masonry unit (brick or concrete block) and wall properties.
1836pub struct MasonryUnit {
1837    /// Net compressive strength of unit f'm (MPa).
1838    pub fm: f64,
1839    /// Mortar type (S, N, or M).
1840    pub mortar_type: String,
1841    /// Bond pattern (running bond or stack bond).
1842    pub bond_pattern: String,
1843    /// Effective modulus of elasticity Em (MPa) per TMS 402.
1844    pub em: f64,
1845    /// Density (kg/m³).
1846    pub density: f64,
1847}
1848impl MasonryUnit {
1849    /// Create a standard clay brick masonry unit.
1850    pub fn clay_brick(fm: f64) -> Self {
1851        let em = 700.0 * fm;
1852        MasonryUnit {
1853            fm,
1854            mortar_type: "S".to_string(),
1855            bond_pattern: "Running".to_string(),
1856            em,
1857            density: 1900.0,
1858        }
1859    }
1860    /// Shear modulus Gv ≈ 0.4 * Em.
1861    pub fn shear_modulus(&self) -> f64 {
1862        0.4 * self.em
1863    }
1864    /// Modulus of rupture fr ≈ 0.064 * fm (MPa) per TMS 402.
1865    pub fn modulus_of_rupture(&self) -> f64 {
1866        0.064 * self.fm
1867    }
1868}
1869/// Glued laminated timber (glulam) section properties per NDS / APA.
1870#[derive(Debug, Clone)]
1871pub struct GlulamSection {
1872    /// Width b (mm).
1873    pub b: f64,
1874    /// Total depth d (mm).
1875    pub d: f64,
1876    /// Number of laminations.
1877    pub n_lam: u32,
1878    /// Thickness of each lamination (mm).
1879    pub lam_thickness: f64,
1880    /// Reference bending design value Fb (MPa).
1881    pub fb: f64,
1882    /// Reference shear design value Fv (MPa).
1883    pub fv: f64,
1884    /// Modulus of elasticity (MPa).
1885    pub e: f64,
1886    /// Moisture service condition factor CM.
1887    pub cm: f64,
1888}
1889impl GlulamSection {
1890    /// Create a new glulam section.
1891    pub fn new(b: f64, d: f64, n_lam: u32, fb: f64, fv: f64, e: f64) -> Self {
1892        let lam_thickness = d / n_lam as f64;
1893        GlulamSection {
1894            b,
1895            d,
1896            n_lam,
1897            lam_thickness,
1898            fb,
1899            fv,
1900            e,
1901            cm: 1.0,
1902        }
1903    }
1904    /// Create a 24F-V4 Douglas Fir glulam (275 × 570 mm, 19 lams).
1905    pub fn df_24f_v4() -> Self {
1906        GlulamSection {
1907            b: 275.0,
1908            d: 570.0,
1909            n_lam: 19,
1910            lam_thickness: 30.0,
1911            fb: 16.5,
1912            fv: 2.4,
1913            e: 12_400.0,
1914            cm: 1.0,
1915        }
1916    }
1917    /// Cross-sectional area (mm²).
1918    pub fn area(&self) -> f64 {
1919        self.b * self.d
1920    }
1921    /// Moment of inertia Ix (mm⁴).
1922    pub fn ix(&self) -> f64 {
1923        self.b * self.d.powi(3) / 12.0
1924    }
1925    /// Section modulus Sx (mm³).
1926    pub fn sx(&self) -> f64 {
1927        self.b * self.d.powi(2) / 6.0
1928    }
1929    /// Volume factor Cv for bending members (NDS).
1930    /// `l` = beam span (m), reduces for long spans.
1931    pub fn volume_factor(&self, l_m: f64) -> f64 {
1932        let kl = 1.09;
1933        let b_ft = self.b / 25.4 / 12.0;
1934        let d_ft = self.d / 25.4 / 12.0;
1935        let l_ft = l_m * 3.28084;
1936        (kl * 21.0 / l_ft).powf(0.1)
1937            * (12.0 / (d_ft * 12.0)).powf(0.1)
1938            * (5.125 / (b_ft * 12.0)).powf(0.1).min(1.0)
1939    }
1940    /// Adjusted bending design value Fb' (MPa).
1941    pub fn adjusted_fb(&self, cd: f64, cv: f64) -> f64 {
1942        self.fb * cd * self.cm * cv
1943    }
1944    /// Allowable moment (N·mm).
1945    pub fn allowable_moment(&self, cd: f64, cv: f64) -> f64 {
1946        self.adjusted_fb(cd, cv) * self.sx()
1947    }
1948    /// Allowable shear (N).
1949    pub fn allowable_shear(&self) -> f64 {
1950        let fv_prime = self.fv * self.cm;
1951        fv_prime * (2.0 / 3.0) * self.b * self.d
1952    }
1953    /// Mid-span deflection under uniform load w (N/mm), simple span.
1954    pub fn midspan_deflection(&self, w_n_per_mm: f64, span_mm: f64) -> f64 {
1955        5.0 * w_n_per_mm * span_mm.powi(4) / (384.0 * self.e * self.ix())
1956    }
1957}