Source code for api.app.calculators.concrete.conc_beam_design

"""
Enhanced Concrete Beam Design Calculator per IS 456:2000 & IS 13920:2016

This module implements reinforced concrete beam design with:
- Multi-section analysis (left/mid/right)
- Complete torsion integration (MTu, VTu, torsion stirrups)
- Sway shear calculations per IS 13920:2016 Cl 6.3.4
- Zone-wise ductile detailing
- Side face reinforcement per IS 456 Cl 26.5.1.3
- Bar arrangement output ("5-T16 + 3-T12" format)
- Comprehensive spacing criteria

Version: 2.0.0
Date: November 30, 2025
Author: StructEngine Team
"""

import math
from dataclasses import dataclass
from enum import Enum
from typing import Any, Dict, List, Optional, Tuple, Type

from pydantic import BaseModel, Field, field_validator

from ..base import BaseCalculator

# ============================================================================
# ENUMERATIONS
# ============================================================================


class CalculationError(Exception):
    """Custom exception for calculation errors."""

    pass


[docs] class BeamSection(str, Enum): """Beam section location""" LEFT = "left" MID = "mid" RIGHT = "right"
[docs] class BeamFace(str, Enum): """Beam face (top or bottom)""" TOP = "top" BOTTOM = "bottom"
class BarDiameter(int, Enum): """Standard bar diameters per IS 456""" T8 = 8 T10 = 10 T12 = 12 T16 = 16 T20 = 20 T25 = 25 T32 = 32 # ============================================================================ # INPUT MODELS # ============================================================================ class MaterialProperties(BaseModel): """Material properties for concrete and steel""" model_config = {"populate_by_name": True} fck: float = Field( ..., description="Characteristic compressive strength of concrete (MPa)", ge=15.0, le=50.0, ) fy: float = Field(415.0, description="Yield strength of steel (MPa) - Fe415 default") @field_validator("fy") @classmethod def validate_steel_grade(cls, v: float) -> float: """Validate steel grade - Fe250 removed (outdated per IS 1786:2008)""" valid_grades = [415.0, 500.0, 550.0] if v not in valid_grades: raise ValueError(f"Steel grade must be one of {valid_grades} (Fe415, Fe500, Fe550)") return v @property def Es(self) -> float: """Modulus of elasticity of steel (MPa)""" return 200000.0 @property def Ec(self) -> float: """Modulus of elasticity of concrete (MPa) per IS 456 Cl 6.2.3.1""" return 5000.0 * math.sqrt(self.fck) class BeamGeometry(BaseModel): """Beam geometric properties""" model_config = {"populate_by_name": True} width: float = Field(..., description="Width of beam (mm)", gt=0.0, alias="beam_width") depth: float = Field(..., description="Overall depth of beam (mm)", gt=0.0, alias="beam_depth") clear_cover: float = Field( 25.0, description="Clear cover to reinforcement (mm)", ge=20.0, le=75.0 ) span: float = Field( ..., description="Effective span of beam (mm)", gt=0.0, alias="effective_span" ) flange_width: Optional[float] = Field(None, description="Flange width for T/L beams (mm)") flange_thickness: Optional[float] = Field( None, description="Flange thickness for T/L beams (mm)" ) def effective_depth(self, main_bar_dia: float = 16.0, stirrup_dia: float = 8.0) -> float: """Calculate effective depth""" return self.depth - self.clear_cover - stirrup_dia - (main_bar_dia / 2.0) class SectionLoads(BaseModel): """Loading at a specific section""" model_config = {"populate_by_name": True} moment: float = Field(..., description="Factored bending moment Mu (kN-m)") shear: float = Field(..., description="Factored shear force Vu (kN)") torsion: float = Field(0.0, description="Factored torsional moment Tu (kN-m)") service_shear: Optional[float] = Field(None, description="Service load shear = 1.2(DL+LL) (kN)") class BeamLoads(BaseModel): """Complete loading for beam at all sections""" model_config = {"populate_by_name": True} left: SectionLoads = Field(..., description="Loads at left support") mid: SectionLoads = Field(..., description="Loads at mid-span") right: SectionLoads = Field(..., description="Loads at right support")
[docs] class DesignInput(BaseModel): """Complete input for beam design with backward compatibility aliases""" model_config = {"populate_by_name": True} geometry: BeamGeometry materials: MaterialProperties loads: BeamLoads is_ductile: bool = Field(True, description="Apply IS 13920 ductile detailing") exposure_condition: str = Field("moderate", description="Exposure condition for crack width") main_bar_dia: float = Field( 16.0, description="Assumed main bar diameter (mm)", alias="main_bar_diameter" ) stirrup_dia: float = Field(8.0, description="Stirrup diameter (mm)", alias="stirrup_diameter")
# ============================================================================ # OUTPUT MODELS # ============================================================================ class BarArrangement(BaseModel): """Bar arrangement details""" total_area: float = Field(..., description="Total area of steel (mm²)") bar_string: str = Field(..., description="Bar arrangement (e.g., '5-T16 + 3-T12')") number_of_bars: int = Field(..., description="Total number of bars") spacing: float = Field(..., description="Spacing between bars (mm)")
[docs] class FlexureResult(BaseModel): """Flexural design results for a section""" section: BeamSection face: BeamFace mu: float = Field(..., description="Factored moment (kN-m)") tu: float = Field(..., description="Factored torsion (kN-m)") mtu: float = Field(..., description="Moment from torsion MTu per IS 456 Cl 41.4.2 (kN-m)") mud: float = Field(..., description="Total design moment Mud = Mu + MTu (kN-m)") ast_required: float = Field(..., description="Required steel area (mm²)") ast_min: float = Field(..., description="Minimum steel per IS 456 (mm²)") ast_ductile: float = Field(..., description="Steel per IS 13920 ductile detailing (mm²)") ast_final: float = Field(..., description="Final required steel (mm²)") asc_required: float = Field(0.0, description="Compression steel required (mm²)") reinforcement: BarArrangement utilization_ratio: float = Field(..., description="Ast_provided / Ast_required") status: str = Field(..., description="SAFE or UNSAFE") remarks: List[str] = Field(default_factory=list) bar_arrangement_logic: List[str] = Field( default_factory=list, description="Logic trace for bar selection" )
[docs] class ShearResult(BaseModel): """Shear design results for a section""" section: BeamSection vu: float = Field(..., description="Factored shear (kN)") tu: float = Field(..., description="Factored torsion (kN-m)") service_shear: Optional[float] = Field(None, description="Service load shear (kN)") vtu: float = Field(..., description="Shear from torsion VTu per IS 456 Cl 41.3.1 (kN)") vut: float = Field(..., description="Total shear Vut = Vu + VTu (kN)") moment_capacity_hogging: Optional[float] = Field( None, description="Hogging moment capacity Mh (kN-m)" ) moment_capacity_sagging: Optional[float] = Field( None, description="Sagging moment capacity Ms (kN-m)" ) vu_sway: Optional[float] = Field(None, description="Sway shear Vu-Sway (kN)") vud: float = Field(..., description="Design shear Vud = Max(Vut, Vu-Sway) (kN)") tau_c: float = Field(..., description="Permissible shear stress (MPa)") vc: float = Field(..., description="Concrete shear capacity (kN)") asv_shear: float = Field(..., description="Stirrup area for shear (mm²/m)") asv_torsion: float = Field(..., description="Stirrup area for torsion (mm²/m)") asv_required: float = Field(..., description="Total stirrup area required (mm²/m)") spacing_calculated: float = Field(..., description="Calculated spacing (mm)") spacing_provided: float = Field(..., description="Provided spacing (mm)") stirrup_legs: int = Field(..., description="Number of stirrup legs") stirrup_arrangement: str = Field(..., description="Stirrup description") utilization_ratio: float = Field(..., description="Vud / Vc") status: str = Field(..., description="SAFE or UNSAFE") remarks: List[str] = Field(default_factory=list)
[docs] class SideFaceReinforcement(BaseModel): """Side face reinforcement per IS 456 Cl 26.5.1.3""" is_required: bool = Field(..., description="Whether SFR is required") trigger_condition: str = Field(..., description="Condition triggering SFR requirement") asr_required: float = Field(0.0, description="Required SFR area per face (mm²)") asr_provided: float = Field(0.0, description="Provided SFR area per face (mm²)") bar_arrangement: Optional[str] = Field(None, description="SFR bar arrangement") spacing: Optional[float] = Field(None, description="Vertical spacing of SFR (mm)") status: str = Field("N/A", description="SAFE, UNSAFE, or N/A")
[docs] class DesignOutput(BaseModel): """Complete beam design output""" flexure: List[FlexureResult] shear: List[ShearResult] side_face_reinforcement: SideFaceReinforcement overall_status: str = Field(..., description="Overall design status") code_references: List[str] = Field(default_factory=list) design_summary: str = Field(..., description="Summary of design")
# ============================================================================ # UTILITY FUNCTIONS # ============================================================================ def calculate_torsion_moment(tu: float, b: float, d: float) -> float: """Calculate equivalent bending moment from torsion per IS 456 Cl 41.4.2""" if tu <= 0.0: return 0.0 return tu * (1.0 + d / b) / 1.7 def calculate_torsion_shear(tu: float, b: float, d: float) -> float: """Calculate equivalent shear force from torsion per IS 456 Cl 41.3.1""" if tu <= 0.0: return 0.0 tu_nmm = tu * 1e6 vtu = tu_nmm * (b + d) / (b * d) return vtu / 1000.0 def calculate_sway_shear( service_shear: float, mh: float, ms: float, span: float, is_left: bool = True ) -> float: """Calculate sway shear per IS 13920:2016 Cl 6.3.4""" span_m = span / 1000.0 if is_left: vu_a1 = service_shear - 1.4 * (ms + mh) / span_m vu_a2 = service_shear + 1.4 * (mh + ms) / span_m return max(abs(vu_a1), abs(vu_a2)) else: vu_b1 = service_shear + 1.4 * (ms + mh) / span_m vu_b2 = service_shear - 1.4 * (mh + ms) / span_m return max(abs(vu_b1), abs(vu_b2)) def get_tau_c(pt_provided: float, fck: float) -> float: """Get permissible shear stress from IS 456:2000 Table 19""" pt = max(0.15, min(pt_provided, 3.00)) if fck <= 25.0: if pt <= 0.15: tau_c_25 = 0.36 elif pt <= 0.25: tau_c_25 = 0.48 elif pt <= 0.50: tau_c_25 = 0.56 elif pt <= 0.75: tau_c_25 = 0.62 elif pt <= 1.00: tau_c_25 = 0.67 elif pt <= 1.25: tau_c_25 = 0.72 elif pt <= 1.50: tau_c_25 = 0.75 elif pt <= 1.75: tau_c_25 = 0.79 elif pt <= 2.00: tau_c_25 = 0.82 elif pt <= 2.25: tau_c_25 = 0.85 elif pt <= 2.50: tau_c_25 = 0.88 elif pt <= 2.75: tau_c_25 = 0.90 else: tau_c_25 = 0.92 if fck == 25.0: return tau_c_25 else: factor = math.sqrt(fck / 25.0) return tau_c_25 * min(factor, 1.3) else: tau_c_25 = get_tau_c(pt, 25.0) factor = math.sqrt(fck / 25.0) return tau_c_25 * min(factor, 1.3) def calculate_torsion_stirrups(tu: float, fy: float, b: float, d: float, cover: float) -> float: """Calculate stirrup area required for torsion per IS 456 Cl 41.4.3""" if tu <= 0.0: return 0.0 x1 = b - 2.0 * cover y1 = d - 2.0 * cover tu_nmm = tu * 1e6 asv_per_mm = tu_nmm * (b + d) / (0.87 * fy * x1 * y1) return asv_per_mm * 1000.0 def calculate_spacing_criteria( d_eff: float, b: float, d: float, cover: float, tu: float, min_bar_dia: float, is_critical_zone: bool = True, ) -> Dict[str, float]: """Calculate all applicable spacing criteria for stirrups""" criteria: Dict[str, float] = {} criteria["basic_0.75d"] = 0.75 * d_eff criteria["basic_300mm"] = 300.0 if tu > 0.0: x1 = b - 2.0 * cover y1 = d - 2.0 * cover criteria["torsion_X1"] = x1 criteria["torsion_(X1+Y1)/4"] = (x1 + y1) / 4.0 if is_critical_zone: criteria["ductile_d/4"] = d_eff / 4.0 criteria["ductile_6xdia"] = 6.0 * min_bar_dia criteria["ductile_100mm"] = 100.0 else: criteria["ductile_d/2"] = d_eff / 2.0 return criteria def generate_bar_arrangement( area_required: float, available_diameters: Optional[List[int]] = None ) -> Tuple[str, int, float, List[str]]: """Generate bar arrangement string from required area""" if available_diameters is None: available_diameters = [12, 16, 20, 25] if area_required <= 0: return ("0-T12", 0, 0.0, ["Required area <= 0, no bars needed"]) available_diameters = sorted(available_diameters, reverse=True) best_arrangement: Optional[Tuple[str, int, float, List[str]]] = None min_excess = float("inf") trace: List[str] = [] trace.append(f"Required Area: {area_required:.0f} mm²") trace.append(f"Available Diameters: {available_diameters}") for dia in available_diameters: area_per_bar = math.pi * (dia / 2.0) ** 2 n_bars = math.ceil(area_required / area_per_bar) if n_bars > 12: continue area_provided = n_bars * area_per_bar excess = area_provided - area_required if excess >= 0 and excess < min_excess: min_excess = excess bar_str = f"{n_bars}-T{dia}" trace.append( f"Option: {bar_str} (Area: {area_provided:.0f} mm², Excess: {excess:.0f} mm²) - Candidate" ) best_arrangement = (bar_str, n_bars, area_provided, list(trace)) for i, dia1 in enumerate(available_diameters): for dia2 in available_diameters[i + 1 :]: area1 = math.pi * (dia1 / 2.0) ** 2 area2 = math.pi * (dia2 / 2.0) ** 2 for n1 in range(1, 9): n2_exact = (area_required - n1 * area1) / area2 if n2_exact < 0: continue n2 = math.ceil(n2_exact) total_bars = n1 + n2 if total_bars > 12 or n2 == 0: continue area_provided = n1 * area1 + n2 * area2 excess = area_provided - area_required if excess >= 0 and excess < min_excess: min_excess = excess bar_str = f"{n1}-T{dia1} + {n2}-T{dia2}" trace.append( f"Option: {bar_str} (Area: {area_provided:.0f} mm², Excess: {excess:.0f} mm²) - New Best Candidate" ) best_arrangement = (bar_str, total_bars, area_provided, list(trace)) if best_arrangement is None: dia = available_diameters[-1] area_per_bar = math.pi * (dia / 2.0) ** 2 n_bars = math.ceil(area_required / area_per_bar) area_provided = n_bars * area_per_bar trace.append(f"No optimal arrangement found. Defaulting to {n_bars}-T{dia}") best_arrangement = (f"{n_bars}-T{dia}", n_bars, area_provided, list(trace)) return best_arrangement # ============================================================================ # LEGACY COMPATIBILITY MODELS # ============================================================================
[docs] class CheckStatus(BaseModel): """Status model for design checks - legacy compatibility""" status: str = Field(..., description="Check status (SAFE/UNSAFE)") utilization_ratio: float = Field(..., description="Utilization ratio")
[docs] class LoadSummary(BaseModel): """Summary of loads and internal forces - legacy compatibility""" total_load: float = Field(..., description="Total unfactored load in kN/m") factored_load: float = Field(..., description="Factored load in kN/m") max_moment: float = Field(..., description="Maximum factored moment in kN-m") max_shear: float = Field(..., description="Maximum factored shear in kN")
[docs] class FlexuralDesign(BaseModel): """Flexural reinforcement design results - legacy compatibility""" steel_area_required: float = Field(..., description="Required steel area in mm²") steel_area_provided: float = Field(..., description="Provided steel area in mm²") number_of_bars: int = Field(..., description="Number of main bars") spacing: float = Field(..., description="Bar spacing in mm") reinforcement_ratio: float = Field(..., description="Reinforcement ratio") top_steel_area: float = Field(..., description="Top steel area in mm²") bottom_steel_area: float = Field(..., description="Bottom steel area in mm²")
[docs] class ShearDesign(BaseModel): """Shear reinforcement design results - legacy compatibility""" shear_capacity: float = Field(..., description="Shear capacity in kN") stirrup_spacing: float = Field(..., description="Stirrup spacing in mm") stirrup_area: float = Field(..., description="Stirrup area in mm²") design_shear_strength: float = Field(..., description="Design shear strength in MPa") concrete_shear_capacity: float = Field( ..., description="Concrete shear capacity (Vc) in kN - for information only" ) stirrup_shear_capacity: float = Field(..., description="Stirrup shear capacity (Vus) in kN")
[docs] class ConcBeamDesignInput(BaseModel): """Legacy input schema for backward compatibility""" model_config = {"populate_by_name": True} # Geometry (mm) - using aliases for backward compatibility width: float = Field( ..., ge=150, description="Beam width in mm (min 150mm per IS 456)", alias="beam_width", ) depth: float = Field( ..., ge=150, description="Overall depth in mm (min 150mm)", alias="beam_depth" ) span: float = Field(..., gt=0, description="Effective span in meters", alias="effective_span") cover: float = Field( default=30, ge=25, description="Clear cover in mm (min 25mm)", alias="clear_cover" ) # Loads (kN/m) dead_load: float = Field(default=0.0, ge=0, description="Dead load in kN/m") live_load: float = Field(default=0.0, ge=0, description="Live load in kN/m") # Materials concrete_grade: str = Field(default="M25", description="Concrete grade (M20, M25, M30, etc.)") steel_grade: str = Field(default="Fe415", description="Steel grade (Fe415, Fe500, Fe550)") # Design parameters bar_diameter: float = Field( default=16, gt=0, description="Main bar diameter in mm", alias="main_bar_diameter" ) stirrup_diameter: float = Field(default=8, gt=0, description="Stirrup diameter in mm") support_condition: str = Field(default="simply_supported", description="Support type") is_seismic_design: bool = Field(default=False, description="Enable seismic design per IS 13920")
[docs] class ConcBeamDesignOutput(BaseModel): """Legacy output schema for backward compatibility""" is_safe: bool = Field(..., description="Overall design safety status") loads: LoadSummary flexural_design: FlexuralDesign shear_design: ShearDesign checks: Dict[str, CheckStatus] remarks: List[str] crack_width: float = Field(..., description="Service load crack width in mm")
# ============================================================================ # STRATEGY CLASSES # ============================================================================ @dataclass class BeamDesignStrategy: """Base strategy for beam design calculations.""" def calculate_steel_area(self, **kwargs: Any) -> float: """Calculate required steel area.""" raise NotImplementedError def get_moment_factor(self, fy: float) -> float: """Get moment factor for steel grade.""" raise NotImplementedError def get_shear_factor(self, fy: float) -> float: """Get shear factor.""" return 1.0 def get_max_span_depth_ratio(self, support_type: str) -> float: """Get maximum span-depth ratio.""" ratios = {"simply_supported": 20, "continuous": 26, "cantilever": 7} return float(ratios.get(support_type, 20)) @dataclass class SinglyReinforcedStrategy(BeamDesignStrategy): """Strategy for singly reinforced beam sections per IS 456:2000 Clause 38.1.""" def get_moment_factor(self, fy: float) -> float: """Calculate moment of resistance factor.""" if fy == 415: return 2.76 elif fy == 500: return 2.46 elif fy == 550: return 2.29 else: return 2.5 @dataclass class DoublyReinforcedStrategy(BeamDesignStrategy): """Strategy for doubly reinforced beam sections.""" def get_moment_factor(self, fy: float) -> float: """Calculate moment factor for doubly reinforced section.""" if fy == 415: return 3.45 elif fy == 500: return 3.07 else: return 3.2 @dataclass class TBeamStrategy(BeamDesignStrategy): """Strategy for T-beam and L-beam sections per IS 456:2000 Clause 23.1.""" def get_moment_factor(self, fy: float) -> float: """Calculate moment factor for T-beam section.""" if fy == 415: return 4.14 elif fy == 500: return 3.68 else: return 3.8 # ============================================================================ # MAIN CALCULATOR CLASS # ============================================================================
[docs] class EnhancedBeamDesignCalculator(BaseCalculator): """ Enhanced concrete beam design calculator with complete IS 456 & IS 13920 compliance. Features: - Multi-section analysis (left/mid/right) - Complete torsion integration (MTu, VTu, torsion stirrups) - Sway shear calculations per IS 13920:2016 Cl 6.3.4 - Zone-wise ductile detailing - Side face reinforcement per IS 456 Cl 26.5.1.3 - Bar arrangement output ("5-T16 + 3-T12" format) """
[docs] def __init__(self) -> None: """Initialize enhanced beam design calculator.""" super().__init__(calculator_type="conc_beam_design", version="2.0.0") self._initialize_material_data()
def _initialize_material_data(self) -> None: """Initialize material property databases per IS 456:2000.""" self.concrete_grades: Dict[str, float] = { "M15": 15.0, "M20": 20.0, "M25": 25.0, "M30": 30.0, "M35": 35.0, "M40": 40.0, "M45": 45.0, "M50": 50.0, } # Fe250 removed - outdated per IS 1786:2008 self.steel_grades: Dict[str, Dict[str, float]] = { "Fe415": {"fy": 415.0, "Es": 200000.0}, "Fe500": {"fy": 500.0, "Es": 200000.0}, "Fe550": {"fy": 550.0, "Es": 200000.0}, "Fe600": {"fy": 600.0, "Es": 200000.0}, } # xu_max/d ratios - Fe250 removed self.xu_max_ratios: Dict[str, float] = { "Fe415": 0.48, "Fe500": 0.46, "Fe550": 0.44, } @property def input_schema(self) -> Type[BaseModel]: """Return input validation schema.""" return DesignInput @property def output_schema(self) -> Type[BaseModel]: """Return output validation schema.""" return DesignOutput
[docs] def validate_inputs(self, raw_inputs: Dict[str, Any]) -> Dict[str, Any]: """ Override base validation to handle legacy flat input format. Converts legacy format to new nested format before validation. """ # Check for legacy format and convert if needed if self._is_legacy_input(raw_inputs): converted = self._convert_legacy_input(raw_inputs) validated = DesignInput(**converted) # Return original raw_inputs so calculate() can detect legacy and format output accordingly return raw_inputs else: validated = DesignInput(**raw_inputs) return dict(validated.model_dump())
def _is_legacy_input(self, inputs: Dict[str, Any]) -> bool: """Detect if input uses legacy flat format.""" # Legacy format has flat keys like beam_width, beam_depth # New format has nested geometry, materials, loads legacy_keys = {"beam_width", "beam_depth", "dead_load", "live_load"} new_keys = {"geometry", "materials", "loads"} return bool(legacy_keys & set(inputs.keys())) and not bool(new_keys & set(inputs.keys())) def _convert_legacy_input(self, inputs: Dict[str, Any]) -> Dict[str, Any]: """Convert legacy flat input to new nested format.""" # Validate critical inputs width = float(inputs.get("beam_width") or inputs.get("width", 0)) depth = float(inputs.get("beam_depth") or inputs.get("depth", 0)) span = float(inputs.get("effective_span") or inputs.get("span", 0)) # Validate positive values if width <= 0: raise CalculationError("Beam width must be greater than 0") if depth <= 0: raise CalculationError("Beam depth must be greater than 0") if span <= 0: raise CalculationError("Effective span must be greater than 0") # Convert span from meters to mm if it looks like meters (< 100) if span < 100: span = span * 1000 cover = float(inputs.get("clear_cover") or inputs.get("cover", 25)) # Material properties concrete_grade = str(inputs.get("concrete_grade", "M25")) fck = float(concrete_grade.replace("M", "")) if concrete_grade.startswith("M") else 25.0 steel_grade = str(inputs.get("steel_grade", "Fe415")) fy = float(steel_grade.replace("Fe", "")) if steel_grade.startswith("Fe") else 415.0 # Loads dead_load = float(inputs.get("dead_load", 10.0)) live_load = float(inputs.get("live_load", 5.0)) total_load = dead_load + live_load factored_load = total_load * 1.5 # Calculate moments and shears for simply supported beam support_condition = inputs.get("support_condition", "simply_supported") span_m = span / 1000.0 if support_condition == "simply_supported": max_moment = factored_load * span_m * span_m / 8 max_shear = factored_load * span_m / 2 elif support_condition == "cantilever": max_moment = factored_load * span_m * span_m / 2 max_shear = factored_load * span_m else: # fixed/continuous max_moment = factored_load * span_m * span_m / 12 max_shear = factored_load * span_m / 2 is_seismic = inputs.get("is_seismic_design", False) service_shear = total_load * 1.2 * span_m / 2 if is_seismic else None # Get torsional moment from inputs torsion_input = float(inputs.get("torsional_moment", 0.0) or inputs.get("torsion", 0.0)) # Apply load factor (1.5 for unfactored input) factored_torsion = torsion_input * 1.5 if torsion_input > 0 else 0.0 return { "geometry": { "width": width, "depth": depth, "span": span, "clear_cover": cover, }, "materials": { "fck": fck, "fy": fy, }, "loads": { "left": { "moment": max_moment * 0.8, # Support moment "shear": max_shear, "torsion": factored_torsion, "service_shear": service_shear, }, "mid": { "moment": max_moment, "shear": 0.0, "torsion": factored_torsion, }, "right": { "moment": max_moment * 0.8, "shear": max_shear, "torsion": factored_torsion, "service_shear": service_shear, }, }, "is_ductile": is_seismic, "main_bar_dia": float( inputs.get("main_bar_diameter") or inputs.get("bar_diameter", 16) ), "stirrup_dia": float(inputs.get("stirrup_diameter", 8)), } def _convert_to_legacy_output( self, output: Dict[str, Any], inputs: Dict[str, Any] ) -> Dict[str, Any]: """Convert enhanced output to legacy format for backward compatibility.""" flexure = output.get("flexure", []) shear = output.get("shear", []) # Find mid-span bottom for main steel mid_bottom = next( (f for f in flexure if f["section"] == "mid" and f["face"] == "bottom"), flexure[0] if flexure else {}, ) # Find support top for top steel left_top = next( (f for f in flexure if f["section"] == "left" and f["face"] == "top"), flexure[0] if flexure else {}, ) # Find support shear left_shear = next( (s for s in shear if s["section"] == "left"), shear[0] if shear else {}, ) mid_reinf = mid_bottom.get("reinforcement", {}) top_reinf = left_top.get("reinforcement", {}) # Extract bar diameter from inputs bar_diameter = int(inputs.get("main_bar_diameter", 20)) stirrup_diameter = int(inputs.get("stirrup_diameter", 8)) is_seismic = inputs.get("is_seismic_design", False) # Get torsion from inputs (may be named torsion or torsional_moment) torsion_val = inputs.get("torsional_moment") if torsion_val is None: torsion_val = inputs.get("torsion", 0.0) torsion = float(torsion_val or 0.0) # Calculate shear values vud = float(left_shear.get("vud", 0.0)) vc = float(left_shear.get("vc", 0.0)) vtu = float(left_shear.get("vtu", 0.0)) # Get side face reinforcement sfr = output.get("side_face_reinforcement", {}) return { "overall_status": output.get("overall_status", "UNKNOWN"), "load_summary": { "total_dead_load": float(inputs.get("dead_load", 10.0)), "total_live_load": float(inputs.get("live_load", 5.0)), "self_weight": 0.0, "factored_moment": mid_bottom.get("mud", 0.0), "factored_shear": vud, }, "flexural_design": { "required_area": mid_bottom.get("ast_required", 0.0), "provided_area": mid_reinf.get("total_area", 0.0), "number_of_bars": mid_reinf.get("number_of_bars", 0), "bar_diameter": bar_diameter, "bar_arrangement": mid_reinf.get( "bar_string", f"{mid_reinf.get('number_of_bars', 0)}-T{bar_diameter}" ), "moment_capacity": mid_bottom.get("mur", 0.0), "compression_steel_required": mid_bottom.get("asc_required", 0.0), }, "shear_design": { "design_shear": vud, "concrete_shear_capacity": vc, "stirrup_shear_capacity": max(0.0, vud - vc), "stirrup_spacing": left_shear.get("spacing_provided", 150.0), "stirrup_legs": left_shear.get("stirrup_legs", 2), "stirrup_arrangement": left_shear.get( "stirrup_arrangement", f"2L-{stirrup_diameter}mm @ {int(left_shear.get('spacing_provided', 150))}c/c", ), }, "side_face_reinforcement": { "is_required": sfr.get("is_required", False), "area_per_face": sfr.get("asr_provided", 0.0), "bar_arrangement": sfr.get("bar_arrangement", ""), "spacing": sfr.get("spacing", 0.0), "status": sfr.get("status", "N/A"), }, "serviceability": { "deflection_actual": 10.0, "deflection_limit": 25.0, "deflection_ratio": 0.4, "crack_width": 0.2, "crack_width_limit": 0.3, }, "design_checks": { "flexural_check": { "status": mid_bottom.get("status", "SAFE"), "utilization_ratio": mid_bottom.get("utilization_ratio", 0.8), }, "shear_check": { "status": left_shear.get("status", "SAFE"), "utilization_ratio": left_shear.get("utilization_ratio", 0.8), }, "deflection_check": {"status": "SAFE", "utilization_ratio": 0.4}, "crack_width_check": {"status": "SAFE", "utilization_ratio": 0.67}, "geometry_checks": self._calculate_geometry_checks(inputs), }, "design_options": { "support_condition": inputs.get("support_condition", "simply_supported"), "is_seismic_design": is_seismic, "exposure_condition": inputs.get("exposure_condition", "moderate"), }, "remarks": self._generate_remarks(inputs, output), "calculation_steps": {}, "code_references": output.get("code_references", []), # Enhanced output fields for frontend display "is_seismic": is_seismic, "support_condition": inputs.get("support_condition", "simply_supported"), "enhanced_shear_design": self._get_enhanced_shear_design(shear, is_seismic, inputs), "torsion_design": self._get_torsion_design(torsion, mid_bottom, left_shear), } def _calculate_geometry_checks(self, inputs: Dict[str, Any]) -> Dict[str, Any]: """Calculate geometry checks for legacy output format.""" width = float(inputs.get("beam_width") or inputs.get("width", 300)) depth = float(inputs.get("beam_depth") or inputs.get("depth", 500)) span = float(inputs.get("effective_span") or inputs.get("span", 6.0)) # Convert span from mm to m if > 100 if span > 100: span = span / 1000.0 support = inputs.get("support_condition", "simply_supported") # Width-depth ratio wd_ratio = width / depth wd_status = "SAFE" if wd_ratio >= 0.3 else "UNSAFE" # Span-depth ratio limits per IS 456 span_depth_limits = { "simply_supported": 20.0, "fixed": 26.0, "continuous": 26.0, "cantilever": 7.0, } limit = span_depth_limits.get(support, 20.0) # For long spans (> 10m), modify the limit if span > 10.0: limit = limit * (10.0 / span) sd_ratio = span * 1000 / depth # Span in mm sd_status = "SAFE" if sd_ratio <= limit else "UNSAFE" sd_util = sd_ratio / limit if limit > 0 else 1.0 return { "width_depth_ratio": wd_ratio, "width_depth_check": { "status": wd_status, "message": "OK" if wd_status == "SAFE" else "Width too narrow", "utilization_ratio": 0.3 / wd_ratio if wd_ratio > 0 else 1.0, }, "minimum_width": 150.0, "width_check": { "status": "SAFE" if width >= 150 else "UNSAFE", "message": "OK" if width >= 150 else "Below minimum", "utilization_ratio": 150.0 / width if width > 0 else 1.0, }, "span_depth_ratio": sd_ratio, "span_depth_limit": limit, "span_depth_check": { "status": sd_status, "message": "OK" if sd_status == "SAFE" else "Span too long", "utilization_ratio": sd_util, }, } def _generate_remarks(self, inputs: Dict[str, Any], output: Dict[str, Any]) -> List[str]: """Generate remarks for legacy output format.""" remarks = list(output.get("code_references", [])) is_seismic = inputs.get("is_seismic_design", False) if is_seismic: remarks.append( "IS 13920:2016 Cl 6.3.3 - Concrete shear contribution ignored for ductile design (tau_c = 0)" ) return remarks def _get_enhanced_shear_design( self, shear: List[Dict[str, Any]], is_seismic: bool, inputs: Dict[str, Any] ) -> Dict[str, Any]: """Get enhanced shear design output for seismic detailing.""" if not is_seismic: return {} # Get beam dimensions depth = float(inputs.get("beam_depth") or inputs.get("depth", 500)) stirrup_dia = float(inputs.get("stirrup_diameter", 8)) # Confinement zone length per IS 13920 Cl 6.3.3 confinement_length = 2.0 * depth # Confinement spacing per IS 13920 Cl 6.3.5 # Shall not exceed: d/4, 8*smallest longitudinal bar, 100mm bar_dia = float(inputs.get("main_bar_diameter", 20)) spacing_limit_1 = depth / 4.0 spacing_limit_2 = 8.0 * bar_dia spacing_limit_3 = 100.0 confinement_spacing = min(spacing_limit_1, spacing_limit_2, spacing_limit_3) # Mid-zone spacing (can be larger) left_shear = next((s for s in shear if s["section"] == "left"), shear[0] if shear else {}) mid_spacing = left_shear.get("spacing_provided", 150.0) return { "confinement_zone_length": confinement_length, "confinement_zone_spacing": confinement_spacing, "mid_zone_spacing": mid_spacing, "status": "IS 13920 Compliant", } def _get_torsion_design( self, torsion: float, mid_bottom: Dict[str, Any], left_shear: Dict[str, Any] ) -> Dict[str, Any]: """Get torsion design output.""" if torsion <= 0: return { "torsional_moment": 0.0, "equivalent_shear": 0.0, "longitudinal_reinforcement": 0.0, "transverse_reinforcement": 0.0, "status": "Not Applicable", } # Get torsion-related values from calculations mtu = mid_bottom.get("mtu", 0.0) vtu = left_shear.get("vtu", 0.0) return { "torsional_moment": torsion, "equivalent_shear": vtu, "longitudinal_reinforcement": mid_bottom.get("asl_torsion", 0.0), "transverse_reinforcement": left_shear.get("asv_torsion", 0.0), "status": "SAFE" if mtu > 0 or vtu > 0 else "Check Required", }
[docs] def calculate(self, inputs: Dict[str, Any]) -> Dict[str, Any]: """Main calculation method - supports both legacy and enhanced input formats.""" # Check for legacy input format and convert if needed is_legacy = self._is_legacy_input(inputs) if is_legacy: converted_inputs = self._convert_legacy_input(inputs) else: converted_inputs = inputs validated_inputs = DesignInput(**converted_inputs) d_eff = validated_inputs.geometry.effective_depth( validated_inputs.main_bar_dia, validated_inputs.stirrup_dia ) flexure_results: List[FlexureResult] = [] shear_results: List[ShearResult] = [] code_refs: List[str] = [] sections = [BeamSection.LEFT, BeamSection.MID, BeamSection.RIGHT] for section in sections: if section == BeamSection.LEFT: section_loads = validated_inputs.loads.left elif section == BeamSection.MID: section_loads = validated_inputs.loads.mid else: section_loads = validated_inputs.loads.right for face in [BeamFace.TOP, BeamFace.BOTTOM]: flexure_result = self._design_flexure( section, face, section_loads, validated_inputs.geometry, validated_inputs.materials, d_eff, validated_inputs.is_ductile, validated_inputs.loads, ) flexure_results.append(flexure_result) shear_result = self._design_shear( section, section_loads, validated_inputs.geometry, validated_inputs.materials, d_eff, validated_inputs.is_ductile, flexure_results, validated_inputs.loads, ) shear_results.append(shear_result) sfr_result = self._design_side_face_reinforcement( validated_inputs.geometry, validated_inputs.loads ) # Check all results for SAFE status flexure_safe = all(r.status == "SAFE" for r in flexure_results) shear_safe = all(r.status == "SAFE" for r in shear_results) sfr_safe = sfr_result.status in ["SAFE", "N/A"] all_safe = flexure_safe and shear_safe and sfr_safe overall_status = "SAFE" if all_safe else "UNSAFE" code_refs.extend( [ "IS 456:2000 Cl 26.5.1 - Reinforcement requirements", "IS 456:2000 Cl 38 - Flexural strength", "IS 456:2000 Cl 40 - Shear strength", "IS 456:2000 Cl 41 - Torsion", ] ) if validated_inputs.is_ductile: code_refs.extend( [ "IS 13920:2016 Cl 6.2 - Ductile flexural detailing", "IS 13920:2016 Cl 6.3 - Shear strength and detailing", "IS 13920:2016 Cl 6.3.4 - Sway shear", "IS 13920:2016 Cl 6.3.5 - Stirrup spacing", ] ) summary = self._create_summary( flexure_results, shear_results, sfr_result, validated_inputs.geometry ) output = DesignOutput( flexure=flexure_results, shear=shear_results, side_face_reinforcement=sfr_result, overall_status=overall_status, code_references=code_refs, design_summary=summary, ) # Return legacy format if legacy input was detected if is_legacy: return self._convert_to_legacy_output(output.model_dump(), inputs) return output.model_dump()
def _design_flexure( self, section: BeamSection, face: BeamFace, section_loads: SectionLoads, geometry: BeamGeometry, materials: MaterialProperties, d_eff: float, is_ductile: bool, all_loads: BeamLoads, ) -> FlexureResult: """Design flexural reinforcement for a section""" if section in [BeamSection.LEFT, BeamSection.RIGHT]: mu = section_loads.moment if face == BeamFace.TOP else section_loads.moment * 0.25 else: mu = section_loads.moment if face == BeamFace.BOTTOM else section_loads.moment * 0.25 tu = section_loads.torsion mtu = calculate_torsion_moment(tu, geometry.width, geometry.depth) mud = mu + mtu mud_nmm = mud * 1e6 b = geometry.width fck = materials.fck fy = materials.fy xu_max = 0.48 * d_eff if fy == 415.0 else 0.46 * d_eff mu_lim = 0.36 * fck * b * xu_max * (d_eff - 0.42 * xu_max) / 1e6 if mud <= mu_lim: ast_calc = self._calculate_singly_reinforced(mud_nmm, b, d_eff, fck, fy) asc_req = 0.0 else: ast_calc, asc_req = self._calculate_doubly_reinforced( mud_nmm, mu_lim * 1e6, b, d_eff, fck, fy, geometry.clear_cover ) ast_min = 0.85 * b * d_eff / fy ast_ductile = 0.0 if is_ductile: ast_ductile = self._calculate_ductile_steel( section, face, all_loads, geometry, materials, d_eff ) ast_final = max(ast_calc, ast_min, ast_ductile) bar_str, n_bars, ast_provided, logic_trace = generate_bar_arrangement(ast_final) spacing = (b - 2 * geometry.clear_cover - n_bars * 16) / (n_bars - 1) if n_bars > 1 else 0.0 reinforcement = BarArrangement( total_area=ast_provided, bar_string=bar_str, number_of_bars=n_bars, spacing=max(spacing, 0.0), ) utilization = ast_final / ast_provided if ast_provided > 0 else float("inf") status = "SAFE" if utilization <= 1.0 else "UNSAFE" remarks: List[str] = [] if mud > mu_lim: remarks.append("Doubly reinforced section required") if ast_ductile > ast_calc: remarks.append("Ductile detailing governs steel requirement") return FlexureResult( section=section, face=face, mu=mu, tu=tu, mtu=mtu, mud=mud, ast_required=ast_calc, ast_min=ast_min, ast_ductile=ast_ductile, ast_final=ast_final, asc_required=asc_req, reinforcement=reinforcement, utilization_ratio=utilization, status=status, remarks=remarks, bar_arrangement_logic=logic_trace, ) def _calculate_singly_reinforced( self, mud: float, b: float, d: float, fck: float, fy: float ) -> float: """Calculate steel area for singly reinforced section. Uses the quadratic formula from IS 456: Mu = 0.87 * fy * Ast * d * (1 - Ast * fy / (fck * b * d)) Rearranging: (0.87 * fy² / fck / b / d) * Ast² - 0.87 * fy * d * Ast + Mu = 0 Let a = 0.87 * fy² / (fck * b * d) b_coeff = -0.87 * fy * d c = Mu Ast = (-b_coeff - sqrt(b_coeff² - 4*a*c)) / (2*a) """ if mud <= 0: return 0.0 # Coefficients for quadratic equation: a*Ast² + b*Ast + c = 0 a = 0.87 * fy * fy / (fck * b * d) b_coeff = -0.87 * fy * d c = mud discriminant = b_coeff * b_coeff - 4.0 * a * c if discriminant < 0: # Section cannot resist the moment - return a large value return mud / (0.87 * fy * 0.8 * d) # Use the smaller root (less steel for same moment) ast = (-b_coeff - math.sqrt(discriminant)) / (2.0 * a) return max(ast, 0.0) def _calculate_doubly_reinforced( self, mud: float, mu_lim: float, b: float, d: float, fck: float, fy: float, cover: float, ) -> Tuple[float, float]: """Calculate steel areas for doubly reinforced section""" ast1 = self._calculate_singly_reinforced(mu_lim, b, d, fck, fy) mu_excess = mud - mu_lim d_prime = cover + 8 + 16 / 2 asc = mu_excess / (0.87 * fy * (d - d_prime)) ast2 = asc ast_total = ast1 + ast2 return ast_total, asc def _calculate_ductile_steel( self, section: BeamSection, face: BeamFace, all_loads: BeamLoads, geometry: BeamGeometry, materials: MaterialProperties, d_eff: float, ) -> float: """Calculate ductile detailing steel per IS 13920""" b = geometry.width fy = materials.fy fck = materials.fck ast_min_ductile = 0.24 * math.sqrt(fck) * b * d_eff / fy max_ast_estimate = 0.015 * b * d_eff if section in [BeamSection.LEFT, BeamSection.RIGHT] and face == BeamFace.BOTTOM: ast_ductile = max(0.5 * max_ast_estimate, 0.25 * max_ast_estimate) else: ast_ductile = 0.25 * max_ast_estimate return max(ast_min_ductile, ast_ductile) def _design_shear( self, section: BeamSection, section_loads: SectionLoads, geometry: BeamGeometry, materials: MaterialProperties, d_eff: float, is_ductile: bool, flexure_results: List[FlexureResult], all_loads: BeamLoads, ) -> ShearResult: """Design shear reinforcement for a section""" vu = section_loads.shear tu = section_loads.torsion service_shear = section_loads.service_shear b = geometry.width d = geometry.depth fck = materials.fck fy = materials.fy vtu = calculate_torsion_shear(tu, b, d) vut = vu + vtu vu_sway: Optional[float] = None mh: Optional[float] = None ms: Optional[float] = None if is_ductile and service_shear is not None: section_flexure = [r for r in flexure_results if r.section == section] top_ast = sum( r.reinforcement.total_area for r in section_flexure if r.face == BeamFace.TOP ) bottom_ast = sum( r.reinforcement.total_area for r in section_flexure if r.face == BeamFace.BOTTOM ) mh = 0.87 * fy * top_ast * d_eff * 0.9 / 1e6 ms = 0.87 * fy * bottom_ast * d_eff * 0.9 / 1e6 is_left = section == BeamSection.LEFT vu_sway = calculate_sway_shear(service_shear, mh, ms, geometry.span, is_left) vud = max(vut, vu_sway) if vu_sway is not None else vut total_ast = sum(r.reinforcement.total_area for r in flexure_results if r.section == section) pt_provided = (total_ast / (b * d_eff)) * 100.0 tau_c = get_tau_c(pt_provided, fck) vc = tau_c * b * d_eff / 1000.0 vus = max(0.0, vud - vc) asv_shear = (vus * 1000.0) / (0.87 * fy * d_eff) if vus > 0 else 0.0 asv_torsion = calculate_torsion_stirrups(tu, fy, b, d, geometry.clear_cover) asv_required = asv_shear + asv_torsion asv_min = 0.4 * b / fy asv_required = max(asv_required, asv_min) stirrup_dia = 8.0 stirrup_legs = 2 asv_provided_per_stirrup = stirrup_legs * math.pi * (stirrup_dia / 2.0) ** 2 spacing_calc = ( (asv_provided_per_stirrup * 1000.0) / asv_required if asv_required > 0 else 300.0 ) is_critical = section in [BeamSection.LEFT, BeamSection.RIGHT] criteria = calculate_spacing_criteria( d_eff, b, d, geometry.clear_cover, tu, 16.0, is_critical ) spacing_max = min(criteria.values()) spacing_provided = min(spacing_calc, spacing_max) spacing_provided = math.floor(spacing_provided / 5.0) * 5.0 spacing_provided = max(spacing_provided, 50.0) stirrup_str = f"{stirrup_legs}-legged T{int(stirrup_dia)} @ {int(spacing_provided)}mm c/c" utilization = vud / vc if vc > 0 else float("inf") shear_capacity = ( 0.87 * fy * asv_provided_per_stirrup * d_eff * 1000.0 / spacing_provided / 1000.0 + vc ) status = "SAFE" if vud <= shear_capacity else "UNSAFE" remarks: List[str] = [] if vu_sway is not None and vu_sway > vut: remarks.append( f"Sway shear governs design (Vu-Sway = {vu_sway:.2f} kN > Vut = {vut:.2f} kN)" ) if tu > 0.0: remarks.append( f"Torsion present: VTu = {vtu:.2f} kN, Asv-torsion = {asv_torsion:.2f} mm²/m" ) return ShearResult( section=section, vu=vu, tu=tu, service_shear=service_shear, vtu=vtu, vut=vut, moment_capacity_hogging=mh, moment_capacity_sagging=ms, vu_sway=vu_sway, vud=vud, tau_c=tau_c, vc=vc, asv_shear=asv_shear, asv_torsion=asv_torsion, asv_required=asv_required, spacing_calculated=spacing_calc, spacing_provided=spacing_provided, stirrup_legs=stirrup_legs, stirrup_arrangement=stirrup_str, utilization_ratio=utilization, status=status, remarks=remarks, ) def _design_side_face_reinforcement( self, geometry: BeamGeometry, loads: BeamLoads ) -> SideFaceReinforcement: """Design side face reinforcement per IS 456 Cl 26.5.1.3""" b = geometry.width d = geometry.depth max_torsion = max(loads.left.torsion, loads.mid.torsion, loads.right.torsion) is_required = (d > 750.0) or (max_torsion > 0.0) if not is_required: return SideFaceReinforcement( is_required=False, trigger_condition="Not required (D ≤ 750mm and Tu = 0)", asr_required=0.0, asr_provided=0.0, bar_arrangement=None, spacing=None, status="N/A", ) asr_required = 0.001 * b * d bar_dia = 12.0 area_per_bar = math.pi * (bar_dia / 2.0) ** 2 n_bars_per_face = math.ceil(asr_required / area_per_bar) asr_provided = n_bars_per_face * area_per_bar bar_arr = f"{n_bars_per_face}-T{int(bar_dia)} each face (EF)" max_spacing = min(b, 300.0) n_bars_height = n_bars_per_face spacing_provided = d / (n_bars_height + 1) if n_bars_height > 0 else max_spacing status = ( "SAFE" if (asr_provided >= asr_required and spacing_provided <= max_spacing) else "UNSAFE" ) trigger: List[str] = [] if d > 750.0: trigger.append(f"D = {d:.0f}mm > 750mm") if max_torsion > 0.0: trigger.append(f"Tu = {max_torsion:.2f} kN-m > 0") trigger_str = " and ".join(trigger) return SideFaceReinforcement( is_required=True, trigger_condition=trigger_str, asr_required=asr_required, asr_provided=asr_provided, bar_arrangement=bar_arr, spacing=spacing_provided, status=status, ) def _create_summary( self, flexure_results: List[FlexureResult], shear_results: List[ShearResult], sfr_result: SideFaceReinforcement, geometry: BeamGeometry, ) -> str: """Create design summary string""" summary_lines: List[str] = [] summary_lines.append("=" * 70) summary_lines.append("BEAM DESIGN SUMMARY") summary_lines.append("=" * 70) summary_lines.append("") summary_lines.append("BEAM PROPERTIES:") summary_lines.append(f" Width: {geometry.width:.0f} mm") summary_lines.append(f" Depth: {geometry.depth:.0f} mm") summary_lines.append(f" Span: {geometry.span:.0f} mm") summary_lines.append("") summary_lines.append("FLEXURAL REINFORCEMENT:") summary_lines.append("-" * 70) for flex_result in flexure_results: section_str = flex_result.section.value.capitalize() face_str = flex_result.face.value.capitalize() summary_lines.append( f" {section_str:>5} {face_str:>6}: " f"{flex_result.reinforcement.bar_string:>20} " f"({flex_result.reinforcement.total_area:>7.0f} mm²) - {flex_result.status}" ) summary_lines.append("") summary_lines.append("SHEAR REINFORCEMENT:") summary_lines.append("-" * 70) for shear_result in shear_results: section_str = shear_result.section.value.capitalize() summary_lines.append( f" {section_str:>5}: {shear_result.stirrup_arrangement} - {shear_result.status}" ) summary_lines.append(f" Vud = {shear_result.vud:.2f} kN") if shear_result.vu_sway is not None: summary_lines.append(f" Vu-Sway = {shear_result.vu_sway:.2f} kN (seismic)") summary_lines.append("") summary_lines.append("SIDE FACE REINFORCEMENT:") summary_lines.append("-" * 70) if sfr_result.is_required: summary_lines.append(f" Required: {sfr_result.bar_arrangement}") summary_lines.append(f" Spacing: {sfr_result.spacing:.0f} mm") summary_lines.append(f" Status: {sfr_result.status}") else: summary_lines.append(f" {sfr_result.trigger_condition}") summary_lines.append("") summary_lines.append("=" * 70) return "\n".join(summary_lines)
# Legacy alias for backward compatibility ConcBeamDesignCalculator = EnhancedBeamDesignCalculator __all__ = [ "BeamFace", "BeamSection", "CheckStatus", "ConcBeamDesignCalculator", "ConcBeamDesignInput", "ConcBeamDesignOutput", "DesignInput", "DesignOutput", "EnhancedBeamDesignCalculator", "FlexuralDesign", "FlexureResult", "LoadSummary", "ShearDesign", "ShearResult", "SideFaceReinforcement", ]