"""
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")
# ============================================================================
# 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 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
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",
]