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

"""
Refactored Column Design Calculator - IS 456:2000 + IS 13920:2016
WITH INTERACTION DIAGRAM SUPPORT (IS 456 ANNEX G)

Concrete column design with reduced complexity and complete shear design.
Implements:
- Method extraction for calculation steps
- Parameter objects for grouped inputs
- Strategy pattern for column types
- Interaction diagram method for slender columns (IS 456 Annex G)
- Functional programming approaches
- Cyclomatic complexity < 10 per method

CORRECTED DESIGN MOMENT LOGIC:
M_design = max(M_analysis, M_min_ecc) + M_slenderness

INTERACTION DIAGRAM IMPLEMENTATION (v5.0.0):
✨ NEW: Proper IS 456 Annex G interaction diagrams for slender columns
✨ NEW: P-M interaction curves for various fck and steel percentages
✨ NEW: Interpolation for intermediate values
✨ IMPROVED: Steel calculation using biaxial bending interaction

UPDATED FEATURES (Dec 5, 2025 - Version 5.0.0):
- ✨ NEW: IS 456 Annex G Interaction Diagrams
- ✨ NEW: Biaxial bending P-M interaction curves
- ✨ NEW: Interpolation for custom f_ck values
- Proper handling of N-M and N-M interaction
- D/B ratio classification check (IS 13920:2016)
- Special confining reinforcement Ash1, Ash2 (IS 13920:2016 Clause 7.6)
- Corrected ductile zone length formula
- 100mm cap on ductile link spacing
- Enhanced output schema with ductile details
- Capacity-based shear design (IS 13920:2016 Clause 7.5)
- Simplified seismic design inputs for MRF
- IS 13920:2016 full compliance option
"""

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

from pydantic import BaseModel, Field, validator

from ..base import BaseCalculator, CalculationError
from .interaction_diagrams import InteractionDiagramTables

# =============================================================================
# PARAMETER OBJECTS (Data Classes)
# =============================================================================


[docs] @dataclass class ColumnGeometry: """Parameter object for column geometry.""" width: float # B (mm) depth: float # D (mm) unsupported_length_x: float # Along major axis (mm) unsupported_length_y: float # Along minor axis (mm) clear_cover: float # (mm) main_bar_dia: float # (mm) tie_dia: float # (mm)
[docs] @dataclass class MaterialProperties: """Parameter object for material properties.""" fck: float # Concrete grade (N/mm²) fy: float # Steel grade (N/mm²) fyshear: float # Shear steel grade (N/mm²)
[docs] @dataclass class LoadParameters: """Parameter object for loads and forces.""" axial_load: float # Pu (kN) - compression positive moment_x: float # Mux (kN-m) - along major axis moment_y: float # Muy (kN-m) - along minor axis shear_x: float # Vux (kN) - along major axis shear_y: float # Vuy (kN) - along minor axis
[docs] @dataclass class EffectiveLengthFactors: """Parameter object for effective length factors.""" along_major_axis: float # ke_x (per Annex-E, IS 456) along_minor_axis: float # ke_y (per Annex-E, IS 456)
[docs] @dataclass class SeismicDesignParameters: """Parameter object for seismic design (IS 13920:2016 Clause 7.5).""" enabled: bool = False sum_beam_moment_major: float = 0.0 # ΣM_beam along major axis (kN-m) sum_beam_moment_minor: float = 0.0 # ΣM_beam along minor axis (kN-m) story_height: float = 3000.0 # Floor-to-floor height (mm)
# ============================================================================= # STRATEGY PATTERN # =============================================================================
[docs] class ColumnDesignStrategy: """Strategy interface for column design calculations."""
[docs] def calculate_slenderness_moment( self, geometry: ColumnGeometry, factored_axial_load: float, effective_length: float, depth_or_width: float, ) -> float: raise NotImplementedError
[docs] def calculate_steel_requirements( self, geometry: ColumnGeometry, materials: MaterialProperties, design_moments: Dict[str, Any], factored_axial_load: float, ) -> Dict[str, Any]: raise NotImplementedError
[docs] class ShortColumnStrategy(ColumnDesignStrategy): """Strategy for short column calculations using IS 456 Clause 39.4."""
[docs] def calculate_slenderness_moment( self, geometry: ColumnGeometry, factored_axial_load: float, effective_length: float, depth_or_width: float, ) -> float: """Short columns have no slenderness moment.""" return 0.0
[docs] def calculate_steel_requirements( self, geometry: ColumnGeometry, materials: MaterialProperties, design_moments: Dict[str, Any], factored_axial_load: float, ) -> Dict[str, Any]: """Calculate steel using IS 456:2000 Clause 39.4.""" gross_area = geometry.width * geometry.depth min_steel_area = (0.8 / 100) * gross_area max_steel_area = (4.0 / 100) * gross_area resultant_moment = design_moments["resultant_moment"] eccentricity = resultant_moment / factored_axial_load if factored_axial_load > 0 else 0 eccentricity_ratio = eccentricity / geometry.depth if geometry.depth > 0 else 0 # IS 456:2000 Clause 39.4 formula # P_u = 0.4 f_ck A_c + 0.67 f_y A_sc P_u_N = factored_axial_load * 1000.0 numerator = P_u_N - 0.4 * materials.fck * gross_area denominator = 0.67 * materials.fy - 0.4 * materials.fck if denominator <= 0: required_area = min_steel_area else: required_area = max(0, numerator / denominator) final_area = max(min_steel_area, min(required_area, max_steel_area)) return { "min_area": min_steel_area, "max_area": max_steel_area, "required_area": required_area, "provided_area": final_area, "percentage": (final_area / gross_area) * 100, "calculation_method": "IS_456_Cl_39_4", "is_short_column": True, "eccentricity_ratio": eccentricity_ratio, }
[docs] class SlenderColumnInteractionDiagramStrategy(ColumnDesignStrategy): """ Strategy for slender column calculations using IS 456 Annex G. Implements proper P-M interaction diagrams for biaxial bending. """
[docs] def calculate_slenderness_moment( self, geometry: ColumnGeometry, factored_axial_load: float, effective_length: float, depth_or_width: float, ) -> float: """Calculate slenderness moment per IS 456 Clause 39.7.1.""" if factored_axial_load <= 0: return 0.0 slenderness_moment = (factored_axial_load * effective_length**2) / ( 2000 * depth_or_width * 1000 ) return float(slenderness_moment)
[docs] def calculate_steel_requirements( self, geometry: ColumnGeometry, materials: MaterialProperties, design_moments: Dict[str, Any], factored_axial_load: float, ) -> Dict[str, Any]: """ Calculate steel using IS 456 Annex G interaction diagrams. Steps: 1. Get interaction diagram for given fck 2. Normalize P_u and M_u 3. Find point on diagram or use interpolation 4. Extract required steel percentage 5. Apply min/max limits """ gross_area = geometry.width * geometry.depth min_steel_area = (0.8 / 100) * gross_area max_steel_area = (4.0 / 100) * gross_area # Get normalized values resultant_moment = design_moments["resultant_moment"] eccentricity = resultant_moment / factored_axial_load if factored_axial_load > 0 else 0 eccentricity_ratio = eccentricity / geometry.depth if geometry.depth > 0 else 0 # Normalize P and M for diagram # P_norm = P_u / (f_ck * b * d) # M_norm = M_u / (f_ck * b * d²) characteristic_length = max(geometry.width, geometry.depth) P_norm = factored_axial_load / (materials.fck * gross_area / 1000) # kN M_norm = (resultant_moment * 1000) / ( materials.fck * gross_area * characteristic_length / 1000 ) # kN-m to N-mm # Get diagram diagram = InteractionDiagramTables.get_diagram(materials.fck) # Find required steel percentage from diagram required_percentage = self._find_steel_percentage_from_diagram(diagram, P_norm, M_norm) # Check for P-M interaction failure (diagram returns -1.0 if point exceeds all curves) pmm_interaction_failed = required_percentage < 0 if pmm_interaction_failed: # Column FAILS P-M interaction check - exceeds capacity of 4% steel required_area = max_steel_area # Use max as placeholder final_area = max_steel_area actual_percentage = -1.0 # Indicates failure else: required_area = (required_percentage / 100) * gross_area final_area = max(min_steel_area, min(required_area, max_steel_area)) actual_percentage = (final_area / gross_area) * 100 return { "min_area": min_steel_area, "max_area": max_steel_area, "required_area": required_area, "provided_area": final_area, "percentage": actual_percentage, "calculation_method": "IS_456_Annex_G_Interaction_Diagram", "is_short_column": False, "eccentricity_ratio": eccentricity_ratio, "pmm_interaction_failed": pmm_interaction_failed, }
@staticmethod def _find_steel_percentage_from_diagram( diagram: Dict[float, List[Tuple[float, float]]], P_norm: float, M_norm: float, ) -> float: """ Find required steel percentage from interaction diagram. Algorithm: 1. Start with minimum steel (0.8%) 2. Interpolate P and M values across steel percentages 3. Find steel % where the normalized P-M point lies inside the curve 4. If point exceeds all curves, return -1.0 (FAILS P-M interaction) """ steel_percentages = sorted(diagram.keys()) # Normalize M to eccentricity ratio for lookup if P_norm > 0: e_norm = M_norm / P_norm if M_norm > 0 else 0 else: return 0.8 # Check each steel percentage for i, p_ratio in enumerate(steel_percentages): curve = diagram[p_ratio] # Find if point (P_norm, M_norm) lies within this curve for j, (P_curve, M_curve) in enumerate(curve): if P_curve >= P_norm: # Interpolate M value at this P if j == 0: M_at_P = M_curve else: P_prev, M_prev = curve[j - 1] # Linear interpolation if P_curve > P_prev: M_at_P = M_prev + (M_curve - M_prev) * (P_norm - P_prev) / ( P_curve - P_prev ) else: M_at_P = M_curve # Check if design point is inside the curve if M_norm <= M_at_P: return p_ratio # If point exceeds all curves, column FAILS P-M interaction check # Return -1.0 as failure indicator (will trigger design failure) return -1.0
# ============================================================================= # INPUT/OUTPUT SCHEMAS # =============================================================================
[docs] class ColumnDesignInput(BaseModel): """Input schema for column design calculation.""" # Geometry column_width: float = Field(..., gt=0, description="Column width (B) in mm") column_depth: float = Field(..., gt=0, description="Column depth (D) in mm") unsupported_length_x: float = Field( ..., gt=0, description="Unsupported length along X (major axis) in mm" ) unsupported_length_y: float = Field( ..., gt=0, description="Unsupported length along Y (minor axis) in mm" ) # Materials concrete_grade: str = Field(..., description="Concrete grade (e.g., M25, M30)") steel_grade: str = Field(..., description="Steel grade (e.g., Fe415, Fe500)") shear_steel_grade: str = Field(default="Fe415", description="Shear reinforcement steel grade") # Loads and Forces (FACTORED values - already multiplied by load factors) axial_load: float = Field( ..., description="FACTORED axial load Pu in kN (compression positive)" ) moment_x: float = Field(default=0, description="FACTORED moment Mux along X in kN-m") moment_y: float = Field(default=0, description="FACTORED moment Muy along Y in kN-m") shear_x: float = Field(default=0, description="FACTORED shear Vux along X in kN") shear_y: float = Field(default=0, description="FACTORED shear Vuy along Y in kN") # Effective Length Factors effective_length_factor_x: float = Field( ..., ge=0.5, le=2.5, description=( "Effective length factor along X axis (ke_x). " "Typical values: 0.65-0.80 (fixed ends), 0.80-1.0 (pinned), " "1.2-2.0 (sway frames). Refer IS 456:2000 Annex E." ), ) effective_length_factor_y: float = Field( ..., ge=0.5, le=2.5, description=( "Effective length factor along Y axis (ke_y). " "Typical values: 0.65-0.80 (fixed ends), 0.80-1.0 (pinned), " "1.2-2.0 (sway frames). Refer IS 456:2000 Annex E." ), ) # Reinforcement clear_cover: float = Field(default=50, gt=0, description="Clear cover in mm") main_bar_diameter: float = Field(default=12, gt=0, description="Main bar diameter in mm") tie_diameter: float = Field(default=8, gt=0, description="Tie diameter in mm") # Seismic Design (Optional) enable_seismic_design: bool = Field( default=False, description=( "Enable capacity-based shear design per IS 13920:2016 Clause 7.5. " "Required for moment-resisting frames in seismic zones." ), ) sum_beam_moment_capacity_major: Optional[float] = Field( None, ge=0, description=( "Sum of beam moment capacities (top + bottom) along MAJOR axis in kN-m. " "Required only if enable_seismic_design = True." ), ) sum_beam_moment_capacity_minor: Optional[float] = Field( None, ge=0, description=( "Sum of beam moment capacities (top + bottom) along MINOR axis in kN-m. " "Required only if enable_seismic_design = True." ), ) story_height: Optional[float] = Field( None, ge=2000, le=6000, description=( "Floor-to-floor story height in mm. " "Typical: 3000mm (residential), 3600-4000mm (commercial). " "Required only if enable_seismic_design = True." ), )
[docs] @validator("sum_beam_moment_capacity_major") def validate_seismic_inputs_major(cls, v: Any, values: Dict[str, Any]) -> Any: """Validate that beam moments are provided if seismic design is enabled.""" if values.get("enable_seismic_design") and v is None: raise ValueError( "sum_beam_moment_capacity_major is required when enable_seismic_design = True" ) return v
[docs] @validator("story_height") def validate_story_height(cls, v: Any, values: Dict[str, Any]) -> Any: """Validate that story height is provided if seismic design is enabled.""" if values.get("enable_seismic_design") and v is None: raise ValueError("story_height is required when enable_seismic_design = True") return v
[docs] class ColumnDesignOutput(BaseModel): """Output schema for column design results.""" inputs: ColumnDesignInput # Material strengths fck: float = Field(..., description="Characteristic strength of concrete (N/mm²)") fy: float = Field(..., description="Yield strength of steel (N/mm²)") fyshear: float = Field(..., description="Yield strength of shear steel (N/mm²)") # D/B Ratio Check db_ratio: float = Field(..., description="D/B ratio for wall vs column classification") db_ratio_limit: float = Field(default=4.0, description="D/B ratio limit per IS 13920") column_classification: str = Field(..., description="Column or Wall classification") # Geometry results gross_area: float = Field(..., description="Gross area in mm²") effective_length_x: float = Field(..., description="Effective length along X in mm") effective_length_y: float = Field(..., description="Effective length along Y in mm") slenderness_ratio_x: float = Field(..., description="Slenderness ratio along X") slenderness_ratio_y: float = Field(..., description="Slenderness ratio along Y") column_type: str = Field(..., description="Column type (short/long)") # Factored loads factored_axial_load: float = Field(..., description="Factored axial load in kN") factored_moment_x: float = Field(..., description="Factored moment X in kN-m") factored_moment_y: float = Field(..., description="Factored moment Y in kN-m") factored_shear_x: float = Field(..., description="Factored shear X in kN") factored_shear_y: float = Field(..., description="Factored shear Y in kN") # Minimum eccentricity min_ecc_x: float = Field(..., description="Minimum eccentricity along X in mm") min_ecc_y: float = Field(..., description="Minimum eccentricity along Y in mm") min_moment_x: float = Field(..., description="Minimum moment along X in kN-m") min_moment_y: float = Field(..., description="Minimum moment along Y in kN-m") min_eccentricity_method: str = Field( default="One Axis at a Time", description="Minimum eccentricity check method per IS 456:2000 Clause 25.4", ) # Analysis moments analysis_moment_x: float = Field(..., description="Analysis moment along X in kN-m") analysis_moment_y: float = Field(..., description="Analysis moment along Y in kN-m") # Maximum of analysis and min eccentricity max_of_analysis_minecc_x: float = Field( ..., description="max(|M_analysis|, |M_min_ecc|) along X in kN-m" ) max_of_analysis_minecc_y: float = Field( ..., description="max(|M_analysis|, |M_min_ecc|) along Y in kN-m" ) # Slenderness effects slenderness_moment_x: float = Field(..., description="Slenderness moment along X in kN-m") slenderness_moment_y: float = Field(..., description="Slenderness moment along Y in kN-m") # Design moments design_moment_x: float = Field(..., description="Final design moment along X in kN-m") design_moment_y: float = Field(..., description="Final design moment along Y in kN-m") resultant_moment: float = Field(..., description="Resultant design moment in kN-m") # Steel requirements min_steel_percentage: float = Field(..., description="Minimum steel percentage (0.8%)") max_steel_percentage: float = Field(..., description="Maximum steel percentage (4.0%)") required_steel_area: float = Field(..., description="Required steel area in mm²") provided_steel_area: float = Field(..., description="Provided steel area in mm²") steel_percentage: float = Field(..., description="Steel percentage provided") calculation_method: str = Field(..., description="Steel calculation method used") # Reinforcement details main_bars_count: int = Field(..., description="Number of main bars") main_bars_description: str = Field(..., description="Main bars description") # Normal links normal_link_dia: float = Field(..., description="Normal link diameter in mm") normal_link_spacing: float = Field(..., description="Normal link spacing in mm") normal_link_description: str = Field(..., description="Normal link description") # Ductile links ductile_link_dia: float = Field(..., description="Ductile link diameter in mm") ductile_link_spacing: float = Field( ..., description="Ductile link spacing in mm (capped at 100mm per IS 13920)" ) ductile_link_description: str = Field(..., description="Ductile link description") ductile_zone_length: float = Field( ..., description="Length of ductile zone in mm per IS 13920 Clause 7.4" ) # Special Confining Reinforcement hoop_dimension_h: float = Field( ..., description="Longer dimension of rectangular hoop to outer face (mm)" ) core_area_Ak: float = Field(..., description="Area of confined concrete core (mm²)") Ash1_confinement: float = Field(..., description="Ash1 confinement requirement (mm²)") Ash2_shear: float = Field(..., description="Ash2 minimum shear requirement (mm²)") Ash_required: float = Field(..., description="Required Ash = max(Ash1, Ash2) (mm²)") link_area_provided: float = Field(..., description="Provided link area per leg (mm²)") confining_reinforcement_check: bool = Field( ..., description="Is confining reinforcement adequate per IS 13920:2016" ) ash_utilization_ratio: float = Field(..., description="Ash required / Ash provided ratio") # Seismic Design Results seismic_design_enabled: bool = Field(..., description="Is capacity-based shear design enabled") estimated_hst_x: Optional[float] = Field( None, description="Estimated distance between contra-flexure points X (mm)" ) estimated_hst_y: Optional[float] = Field( None, description="Estimated distance between contra-flexure points Y (mm)" ) capacity_based_shear_x: Optional[float] = Field( None, description="Capacity-based shear along X per IS 13920 (kN)" ) capacity_based_shear_y: Optional[float] = Field( None, description="Capacity-based shear along Y per IS 13920 (kN)" ) design_shear_x_value: float = Field(..., description="Final design shear along X (kN)") design_shear_y_value: float = Field(..., description="Final design shear along Y (kN)") governing_shear_case_x: str = Field( ..., description="Which shear governs along X: 'Analysis' or 'Capacity-Based (IS 13920)'" ) governing_shear_case_y: str = Field( ..., description="Which shear governs along Y: 'Analysis' or 'Capacity-Based (IS 13920)'" ) # Shear design results shear_stress_x: float = Field(..., description="Shear stress along X in N/mm²") shear_stress_y: float = Field(..., description="Shear stress along Y in N/mm²") shear_capacity_concrete_x: float = Field( ..., description="Shear capacity of concrete along X in kN" ) shear_capacity_concrete_y: float = Field( ..., description="Shear capacity of concrete along Y in kN" ) shear_capacity_enhanced_x: float = Field( ..., description="Enhanced shear capacity along X in N/mm²" ) shear_capacity_enhanced_y: float = Field( ..., description="Enhanced shear capacity along Y in N/mm²" ) # Capacity checks axial_capacity: float = Field(..., description="Axial load capacity in kN") moment_capacity_x: float = Field(..., description="Moment capacity along X in kN-m") moment_capacity_y: float = Field(..., description="Moment capacity along Y in kN-m") capacity_interaction_ratio: float = Field(..., description="P-M interaction ratio") # Design checks capacity_check: bool = Field(..., description="Is capacity adequate") slenderness_check: bool = Field(..., description="Is slenderness within limits") reinforcement_check: bool = Field(..., description="Is reinforcement adequate") shear_check_x: bool = Field(..., description="Is shear adequate along X") shear_check_y: bool = Field(..., description="Is shear adequate along Y") is_13920_compliant: bool = Field( ..., description="Is design IS 13920:2016 compliant for seismic zones" ) status: str = Field(..., description="Design status (PASS/FAIL)") remarks: List[str] = Field(default_factory=list, description="Design remarks")
# ============================================================================= # MAIN CALCULATOR CLASS - VERSION 5.0.0 # =============================================================================
[docs] class RefactoredColumnDesignCalculator(BaseCalculator): """ Refactored calculator for concrete column design per IS 456:2000 + IS 13920:2016 WITH INTERACTION DIAGRAM SUPPORT (IS 456 ANNEX G) Design Moment Calculation: M_design = max(M_analysis, M_min_ecc) + M_slenderness UPDATES (Dec 5, 2025 - Version 5.0.0): - ✨ IS 456 Annex G Interaction Diagrams for slender columns - ✨ Proper P-M interaction curves - ✨ Biaxial bending support - D/B ratio classification check - Special confining reinforcement (Ash1, Ash2) - Corrected ductile zone length - 100mm cap on ductile link spacing - Capacity-based shear design (IS 13920 Clause 7.5) - Full IS 13920:2016 seismic compliance """
[docs] def __init__(self) -> None: super().__init__("column_design_refactored", "5.0.0") self.material_strengths = self._initialize_material_data() self.load_factor = 1.5 self.partial_safety_concrete = 1.5 self.partial_safety_steel = 1.15 self.overstrength_factor = 1.4 # IS 13920:2016 Clause 7.5
@property def input_schema(self) -> Type[BaseModel]: return ColumnDesignInput @property def output_schema(self) -> Type[BaseModel]: return ColumnDesignOutput def _initialize_material_data(self) -> Dict[str, Any]: """Initialize material strength data.""" return { "concrete": { "M15": 15, "M20": 20, "M25": 25, "M30": 30, "M35": 35, "M40": 40, "M45": 45, "M50": 50, }, "steel": { "Fe250": 250, "Fe415": 415, "Fe500": 500, "Fe550": 550, }, } def _get_material_strength(self, material_type: str, grade: str) -> float: """Get material strength with default fallback.""" strength = self.material_strengths[material_type].get( grade, 25 if material_type == "concrete" else 415 ) return float(strength) def _check_db_ratio(self, geometry: ColumnGeometry) -> Dict[str, Any]: """Check D/B ratio per IS 13920:2016 for wall vs column classification.""" db_ratio = geometry.depth / geometry.width code_limit = 4.0 if db_ratio > code_limit: raise CalculationError( f"D/B ratio ({db_ratio:.2f}) exceeds limit ({code_limit}). " "This member must be designed as a structural wall per IS 13920:2016 Clause 10, " "not as a column. Wall design provisions differ significantly." ) return { "db_ratio": round(db_ratio, 3), "db_ratio_limit": code_limit, "classification": "Column" if db_ratio <= code_limit else "Wall", } def _extract_parameters(self, inputs: Dict[str, Any]) -> Tuple[Any, ...]: """Extract and group input parameters.""" geometry = ColumnGeometry( width=inputs["column_width"], depth=inputs["column_depth"], unsupported_length_x=inputs["unsupported_length_x"], unsupported_length_y=inputs["unsupported_length_y"], clear_cover=inputs["clear_cover"], main_bar_dia=inputs["main_bar_diameter"], tie_dia=inputs["tie_diameter"], ) materials = MaterialProperties( fck=self._get_material_strength("concrete", inputs["concrete_grade"]), fy=self._get_material_strength("steel", inputs["steel_grade"]), fyshear=self._get_material_strength("steel", inputs.get("shear_steel_grade", "Fe415")), ) loads = LoadParameters( axial_load=inputs["axial_load"], moment_x=inputs.get("moment_x", 0.0), moment_y=inputs.get("moment_y", 0.0), shear_x=inputs.get("shear_x", 0.0), shear_y=inputs.get("shear_y", 0.0), ) eff_lengths = EffectiveLengthFactors( along_major_axis=inputs["effective_length_factor_x"], along_minor_axis=inputs["effective_length_factor_y"], ) seismic = SeismicDesignParameters( enabled=inputs.get("enable_seismic_design", False), sum_beam_moment_major=inputs.get("sum_beam_moment_capacity_major", 0.0) or 0.0, sum_beam_moment_minor=inputs.get("sum_beam_moment_capacity_minor", 0.0) or 0.0, story_height=inputs.get("story_height", 3000.0) or 3000.0, ) return geometry, materials, loads, eff_lengths, seismic def _estimate_clear_story_height( self, story_height: float, beam_depth_estimate: float = 600.0 ) -> float: """Estimate distance between contra-flexure points (hst).""" return story_height - beam_depth_estimate def _calculate_capacity_based_shear(self, sum_beam_moment_capacity: float, hst: float) -> float: """Calculate capacity-based shear per IS 13920:2016 Clause 7.5.""" if sum_beam_moment_capacity <= 0 or hst <= 0: return 0.0 Vu_capacity = self.overstrength_factor * sum_beam_moment_capacity / (hst / 1000) return Vu_capacity def _get_design_shear( self, analysis_shear: float, capacity_based_shear: float, enable_seismic: bool, ) -> Tuple[float, str]: """Determine design shear force per IS 13920:2016 Clause 7.5.""" if not enable_seismic: return abs(analysis_shear), "Analysis" design_shear = max(abs(analysis_shear), capacity_based_shear) if capacity_based_shear > abs(analysis_shear): return design_shear, "Capacity-Based (IS 13920)" else: return design_shear, "Analysis" def _calculate_geometric_properties( self, geometry: ColumnGeometry, eff_lengths: EffectiveLengthFactors ) -> Tuple[Any, ...]: """Calculate basic geometric properties.""" gross_area = geometry.width * geometry.depth effective_length_x = eff_lengths.along_major_axis * geometry.unsupported_length_x effective_length_y = eff_lengths.along_minor_axis * geometry.unsupported_length_y slenderness_ratio_x = effective_length_x / geometry.depth slenderness_ratio_y = effective_length_y / geometry.width is_short_column = (slenderness_ratio_x <= 12) and (slenderness_ratio_y <= 12) return ( gross_area, effective_length_x, effective_length_y, slenderness_ratio_x, slenderness_ratio_y, is_short_column, ) def _apply_load_factors(self, loads: LoadParameters) -> Tuple[Any, ...]: """Pass through factored loads (user provides already-factored values).""" # NOTE: User must provide FACTORED loads directly # No multiplication applied - loads are used as-is return ( loads.axial_load, loads.moment_x, loads.moment_y, loads.shear_x, loads.shear_y, ) def _calculate_minimum_eccentricity( self, geometry: ColumnGeometry, factored_axial_load: float ) -> Dict[str, Any]: """Calculate minimum eccentricity per IS 456 Clause 25.4.""" min_ecc_x = max( (geometry.unsupported_length_x / 500) + (geometry.width / 30), 20.0, ) min_ecc_y = 0.0 # One axis at a time min_moment_x = factored_axial_load * min_ecc_x / 1000 min_moment_y = 0.0 return { "min_ecc_x": min_ecc_x, "min_ecc_y": min_ecc_y, "min_moment_x": min_moment_x, "min_moment_y": min_moment_y, } def _get_column_strategy(self, is_short_column: bool) -> ColumnDesignStrategy: """Select appropriate calculation strategy.""" if is_short_column: return ShortColumnStrategy() else: return SlenderColumnInteractionDiagramStrategy() def _calculate_slenderness_moments( self, geometry: ColumnGeometry, factored_axial_load: float, effective_length_x: float, effective_length_y: float, is_short_column: bool, ) -> Dict[str, Any]: """Calculate slenderness moments per IS 456 Clause 39.7.1.""" strategy = self._get_column_strategy(is_short_column) slenderness_moment_x = strategy.calculate_slenderness_moment( geometry, factored_axial_load, effective_length_x, geometry.depth ) slenderness_moment_y = strategy.calculate_slenderness_moment( geometry, factored_axial_load, effective_length_y, geometry.width ) return { "slenderness_moment_x": slenderness_moment_x, "slenderness_moment_y": slenderness_moment_y, } def _calculate_design_moments( self, factored_moments: Tuple[Any, ...], min_eccentricity: Dict[str, Any], slenderness_moments: Dict[str, Any], ) -> Dict[str, Any]: """Calculate final design moments per IS 456 logic.""" factored_moment_x = factored_moments[0] factored_moment_y = factored_moments[1] analysis_moment_x = abs(factored_moment_x) analysis_moment_y = abs(factored_moment_y) min_ecc_moment_x = abs(min_eccentricity["min_moment_x"]) min_ecc_moment_y = abs(min_eccentricity["min_moment_y"]) max_of_analysis_minecc_x = max(analysis_moment_x, min_ecc_moment_x) max_of_analysis_minecc_y = max(analysis_moment_y, min_ecc_moment_y) design_moment_x = max_of_analysis_minecc_x + abs( slenderness_moments["slenderness_moment_x"] ) design_moment_y = max_of_analysis_minecc_y + abs( slenderness_moments["slenderness_moment_y"] ) resultant_moment = math.sqrt(design_moment_x**2 + design_moment_y**2) return { "analysis_moment_x": analysis_moment_x, "analysis_moment_y": analysis_moment_y, "max_of_analysis_minecc_x": max_of_analysis_minecc_x, "max_of_analysis_minecc_y": max_of_analysis_minecc_y, "design_moment_x": design_moment_x, "design_moment_y": design_moment_y, "resultant_moment": resultant_moment, } def _calculate_steel_requirements( self, geometry: ColumnGeometry, materials: MaterialProperties, design_moments: Dict[str, Any], factored_axial_load: float, is_short_column: bool, ) -> Dict[str, Any]: """Calculate steel reinforcement requirements using strategy pattern.""" strategy = self._get_column_strategy(is_short_column) return strategy.calculate_steel_requirements( geometry, materials, design_moments, factored_axial_load ) def _design_reinforcement_layout( self, geometry: ColumnGeometry, steel_requirements: Dict[str, Any] ) -> Dict[str, Any]: """Design main reinforcement and link layout.""" main_bar_area = math.pi * geometry.main_bar_dia**2 / 4 main_bars_count = max(4, math.ceil(steel_requirements["provided_area"] / main_bar_area)) main_bars_count = ((main_bars_count + 3) // 4) * 4 provided_area = main_bars_count * main_bar_area normal_link_spacing = min( min(geometry.width, geometry.depth), 16 * geometry.main_bar_dia, 300, ) ductile_link_spacing = min( 6 * geometry.main_bar_dia, 100, ) ductile_zone_length = max( max(geometry.depth, geometry.width), geometry.unsupported_length_x / 6, 450, ) return { "count": main_bars_count, "provided_area": provided_area, "description": f"{main_bars_count} nos. {geometry.main_bar_dia}mm dia bars", "normal_link_dia": geometry.tie_dia, "normal_link_spacing": normal_link_spacing, "normal_link_description": f"{geometry.tie_dia}mm dia @ {normal_link_spacing:.0f}mm c/c", "ductile_link_dia": geometry.tie_dia, "ductile_link_spacing": ductile_link_spacing, "ductile_link_description": f"{geometry.tie_dia}mm dia @ {ductile_link_spacing:.0f}mm c/c", "ductile_zone_length": ductile_zone_length, } def _calculate_special_confining_reinforcement( self, geometry: ColumnGeometry, materials: MaterialProperties, ductile_link_spacing: float, ) -> Dict[str, Any]: """Calculate special confining reinforcement per IS 13920:2016 Clause 7.6.""" cover_to_link = geometry.clear_cover + geometry.tie_dia / 2 h = max(geometry.width, geometry.depth) - 2 * cover_to_link Ag = geometry.width * geometry.depth Ak = (geometry.width - 2 * cover_to_link) * (geometry.depth - 2 * cover_to_link) Ash1 = 0.18 * ductile_link_spacing * h * (materials.fck / materials.fy) * ((Ag / Ak) - 1) Ash2 = 0.05 * ductile_link_spacing * h * (materials.fck / materials.fy) Ash_required = max(Ash1, Ash2) link_area = math.pi * geometry.tie_dia**2 / 4 return { "hoop_dimension_h": round(h, 2), "core_area_Ak": round(Ak, 2), "Ash1_confinement": round(Ash1, 2), "Ash2_shear": round(Ash2, 2), "Ash_required": round(Ash_required, 2), "link_area_provided": round(link_area, 2), "check": link_area >= Ash_required, "utilization_ratio": round( Ash_required / link_area if link_area > 0 else float("inf"), 3 ), } def _calculate_capacities( self, geometry: ColumnGeometry, materials: MaterialProperties, bar_details: Dict[str, Any], ) -> Dict[str, Any]: """Calculate column axial and moment capacities per IS 456.""" gross_area = geometry.width * geometry.depth concrete_area = gross_area - bar_details["provided_area"] axial_capacity = ( 0.4 * materials.fck * concrete_area + 0.67 * materials.fy * bar_details["provided_area"] ) / 1000 moment_capacity = ( bar_details["provided_area"] * materials.fy * max(geometry.width, geometry.depth) * 0.8 ) / (4 * 1000000) return { "axial": axial_capacity, "moment_x": moment_capacity, "moment_y": moment_capacity, } def _calculate_shear_design( self, geometry: ColumnGeometry, materials: MaterialProperties, factored_loads: Tuple[Any, ...], bar_details: Dict[str, Any], seismic: SeismicDesignParameters, ) -> Dict[str, Any]: """Calculate shear design per IS 456 Clause 40 + IS 13920 Clause 7.5.""" factored_axial = factored_loads[0] factored_shear_x = factored_loads[3] factored_shear_y = factored_loads[4] # Capacity-based shear capacity_based_shear_x = None capacity_based_shear_y = None estimated_hst_x = None estimated_hst_y = None if seismic.enabled: estimated_hst_x = self._estimate_clear_story_height(seismic.story_height) estimated_hst_y = estimated_hst_x capacity_based_shear_x = self._calculate_capacity_based_shear( seismic.sum_beam_moment_major, estimated_hst_x ) capacity_based_shear_y = self._calculate_capacity_based_shear( seismic.sum_beam_moment_minor, estimated_hst_y ) # Design shear design_shear_x, governing_case_x = self._get_design_shear( factored_shear_x, capacity_based_shear_x or 0.0, seismic.enabled, ) design_shear_y, governing_case_y = self._get_design_shear( factored_shear_y, capacity_based_shear_y or 0.0, seismic.enabled, ) # Shear stress shear_stress_x = design_shear_x / (0.8 * geometry.width * geometry.depth / 1000) shear_stress_y = design_shear_y / (0.8 * geometry.width * geometry.depth / 1000) pt = (bar_details["provided_area"] / (geometry.width * geometry.depth)) * 100 * 0.5 beta = 0.8 + (pt / 100) tau_c = 0.85 * math.sqrt(materials.fck / 25) * beta enhancement_factor = min( 1.5, 1 + (3 * factored_axial) / (geometry.width * geometry.depth * materials.fck / 1000), ) tau_c_enhanced_x = tau_c * enhancement_factor tau_c_enhanced_y = tau_c * enhancement_factor shear_capacity_concrete_x = tau_c * 0.8 * geometry.width * geometry.depth / 1000 shear_capacity_concrete_y = tau_c * 0.8 * geometry.width * geometry.depth / 1000 return { "seismic_enabled": seismic.enabled, "estimated_hst_x": estimated_hst_x, "estimated_hst_y": estimated_hst_y, "capacity_based_shear_x": capacity_based_shear_x, "capacity_based_shear_y": capacity_based_shear_y, "design_shear_x": design_shear_x, "design_shear_y": design_shear_y, "governing_case_x": governing_case_x, "governing_case_y": governing_case_y, "shear_stress_x": shear_stress_x, "shear_stress_y": shear_stress_y, "tau_c": tau_c, "enhancement_factor": enhancement_factor, "tau_c_enhanced_x": tau_c_enhanced_x, "tau_c_enhanced_y": tau_c_enhanced_y, "shear_capacity_concrete_x": shear_capacity_concrete_x, "shear_capacity_concrete_y": shear_capacity_concrete_y, } def _perform_design_checks( self, capacities: Dict[str, Any], design_moments: Dict[str, Any], factored_loads: Tuple[Any, ...], slenderness_ratio_x: float, slenderness_ratio_y: float, steel_percentage: float, shear_design: Dict[str, Any], confining_check: bool, ) -> Dict[str, Any]: """Perform all design checks.""" factored_axial = factored_loads[0] axial_ratio = factored_axial / capacities["axial"] if capacities["axial"] > 0 else 0 moment_ratio = ( design_moments["resultant_moment"] / (capacities["moment_x"] * 1.5) if capacities["moment_x"] > 0 else 0 ) capacity_interaction_ratio = axial_ratio + moment_ratio shear_check_x = shear_design["shear_stress_x"] <= shear_design["tau_c_enhanced_x"] shear_check_y = shear_design["shear_stress_y"] <= shear_design["tau_c_enhanced_y"] checks = { "capacity": capacity_interaction_ratio <= 1.0, "slenderness_x": slenderness_ratio_x <= 60, "slenderness_y": slenderness_ratio_y <= 60, "reinforcement": 0.8 <= steel_percentage <= 4.0, "shear_x": shear_check_x, "shear_y": shear_check_y, "confining_reinforcement": confining_check, } return { "individual": checks, "capacity_interaction_ratio": capacity_interaction_ratio, "overall": all(checks.values()), } def _generate_remarks( self, checks: Dict[str, Any], slenderness_ratio_x: float, slenderness_ratio_y: float, steel_percentage: float, ash_utilization: float, seismic_enabled: bool, calculation_method: str, pmm_interaction_failed: bool = False, ) -> List[str]: """Generate design remarks.""" remarks = [] # Check for PMM interaction failure first (critical) if pmm_interaction_failed: remarks.append( "❌ DESIGN FAILS: Column exceeds P-M interaction capacity. " "Even with 4.0% reinforcement, the section is inadequate. " "ACTION REQUIRED: Increase column size or concrete grade." ) # Add method note if "Interaction_Diagram" in calculation_method: remarks.append("✨ Calculation Method: IS 456 Annex G - Interaction Diagrams (v5.0.0)") elif "Cl_39_4" in calculation_method: remarks.append("✓ Calculation Method: IS 456 Clause 39.4 (short columns)") if not checks["individual"]["capacity"]: remarks.append( f"✗ Capacity inadequate - P-M interaction ratio {checks['capacity_interaction_ratio']:.3f} > 1.0. " "Increase column size, concrete grade, or steel reinforcement." ) if not checks["individual"]["slenderness_x"]: remarks.append( f"✗ Slenderness ratio along X ({slenderness_ratio_x:.1f}) exceeds limit 60." ) if not checks["individual"]["slenderness_y"]: remarks.append( f"✗ Slenderness ratio along Y ({slenderness_ratio_y:.1f}) exceeds limit 60." ) if not checks["individual"]["reinforcement"]: remarks.append(f"✗ Steel percentage {steel_percentage:.2f}% outside limits (0.8-4.0%).") if not checks["individual"]["shear_x"]: remarks.append("✗ Shear capacity inadequate along X direction.") if not checks["individual"]["shear_y"]: remarks.append("✗ Shear capacity inadequate along Y direction.") if not checks["individual"]["confining_reinforcement"]: remarks.append( f"✗ Special confining reinforcement inadequate (ratio: {ash_utilization:.2f}). " "Increase tie diameter or reduce spacing per IS 13920:2016 Clause 7.6." ) if checks["overall"]: if seismic_enabled: remarks.append( "✓ All checks satisfied - Design is SAFE per IS 456:2000 + IS 13920:2016 " "(with capacity-based shear for seismic zones)" ) else: remarks.append( "✓ All checks satisfied - Design is SAFE per IS 456:2000 + IS 13920:2016" ) return remarks
[docs] def calculate(self, inputs: Dict[str, Any]) -> Dict[str, Any]: """Main calculation method with Interaction Diagram support.""" try: # Extract parameters geometry, materials, loads, eff_lengths, seismic = self._extract_parameters(inputs) # Check D/B ratio db_check = self._check_db_ratio(geometry) # Calculate geometric properties ( gross_area, effective_length_x, effective_length_y, slenderness_ratio_x, slenderness_ratio_y, is_short_column, ) = self._calculate_geometric_properties(geometry, eff_lengths) # Apply load factors factored_loads = self._apply_load_factors(loads) # Calculate minimum eccentricity min_eccentricity = self._calculate_minimum_eccentricity(geometry, factored_loads[0]) # Calculate slenderness moments slenderness_moments = self._calculate_slenderness_moments( geometry, factored_loads[0], effective_length_x, effective_length_y, is_short_column, ) # Calculate design moments design_moments = self._calculate_design_moments( (factored_loads[1], factored_loads[2]), min_eccentricity, slenderness_moments, ) # Calculate steel requirements (with strategy pattern) steel_requirements = self._calculate_steel_requirements( geometry, materials, design_moments, factored_loads[0], is_short_column ) # Design reinforcement layout bar_details = self._design_reinforcement_layout(geometry, steel_requirements) # Calculate special confining reinforcement confining_reinforcement = self._calculate_special_confining_reinforcement( geometry, materials, bar_details["ductile_link_spacing"] ) # Calculate capacities capacities = self._calculate_capacities(geometry, materials, bar_details) # Calculate shear design shear_design = self._calculate_shear_design( geometry, materials, factored_loads, bar_details, seismic ) # Perform checks checks = self._perform_design_checks( capacities, design_moments, factored_loads, slenderness_ratio_x, slenderness_ratio_y, steel_requirements["percentage"], shear_design, confining_reinforcement["check"], ) # Generate remarks pmm_failed = steel_requirements.get("pmm_interaction_failed", False) remarks = self._generate_remarks( checks, slenderness_ratio_x, slenderness_ratio_y, steel_requirements["percentage"], confining_reinforcement["utilization_ratio"], seismic.enabled, steel_requirements["calculation_method"], pmm_failed, ) # Override status if PMM interaction failed if pmm_failed: checks["overall"] = False # Compile results return { "inputs": inputs, "fck": materials.fck, "fy": materials.fy, "fyshear": materials.fyshear, "db_ratio": db_check["db_ratio"], "db_ratio_limit": db_check["db_ratio_limit"], "column_classification": db_check["classification"], "gross_area": gross_area, "effective_length_x": effective_length_x, "effective_length_y": effective_length_y, "slenderness_ratio_x": slenderness_ratio_x, "slenderness_ratio_y": slenderness_ratio_y, "column_type": "Short column" if is_short_column else "Long column", "factored_axial_load": factored_loads[0], "factored_moment_x": factored_loads[1], "factored_moment_y": factored_loads[2], "factored_shear_x": factored_loads[3], "factored_shear_y": factored_loads[4], "min_ecc_x": min_eccentricity["min_ecc_x"], "min_ecc_y": min_eccentricity["min_ecc_y"], "min_moment_x": min_eccentricity["min_moment_x"], "min_moment_y": min_eccentricity["min_moment_y"], "min_eccentricity_method": "One Axis at a Time", "analysis_moment_x": design_moments["analysis_moment_x"], "analysis_moment_y": design_moments["analysis_moment_y"], "max_of_analysis_minecc_x": design_moments["max_of_analysis_minecc_x"], "max_of_analysis_minecc_y": design_moments["max_of_analysis_minecc_y"], "slenderness_moment_x": slenderness_moments["slenderness_moment_x"], "slenderness_moment_y": slenderness_moments["slenderness_moment_y"], "design_moment_x": design_moments["design_moment_x"], "design_moment_y": design_moments["design_moment_y"], "resultant_moment": design_moments["resultant_moment"], "min_steel_percentage": 0.8, "max_steel_percentage": 4.0, "required_steel_area": steel_requirements["required_area"], "provided_steel_area": bar_details["provided_area"], "steel_percentage": steel_requirements["percentage"], "calculation_method": steel_requirements["calculation_method"], "main_bars_count": bar_details["count"], "main_bars_description": bar_details["description"], "normal_link_dia": bar_details["normal_link_dia"], "normal_link_spacing": bar_details["normal_link_spacing"], "normal_link_description": bar_details["normal_link_description"], "ductile_link_dia": bar_details["ductile_link_dia"], "ductile_link_spacing": bar_details["ductile_link_spacing"], "ductile_link_description": bar_details["ductile_link_description"], "ductile_zone_length": bar_details["ductile_zone_length"], "hoop_dimension_h": confining_reinforcement["hoop_dimension_h"], "core_area_Ak": confining_reinforcement["core_area_Ak"], "Ash1_confinement": confining_reinforcement["Ash1_confinement"], "Ash2_shear": confining_reinforcement["Ash2_shear"], "Ash_required": confining_reinforcement["Ash_required"], "link_area_provided": confining_reinforcement["link_area_provided"], "confining_reinforcement_check": confining_reinforcement["check"], "ash_utilization_ratio": confining_reinforcement["utilization_ratio"], "seismic_design_enabled": shear_design["seismic_enabled"], "estimated_hst_x": shear_design["estimated_hst_x"], "estimated_hst_y": shear_design["estimated_hst_y"], "capacity_based_shear_x": shear_design["capacity_based_shear_x"], "capacity_based_shear_y": shear_design["capacity_based_shear_y"], "design_shear_x_value": shear_design["design_shear_x"], "design_shear_y_value": shear_design["design_shear_y"], "governing_shear_case_x": shear_design["governing_case_x"], "governing_shear_case_y": shear_design["governing_case_y"], "shear_stress_x": shear_design["shear_stress_x"], "shear_stress_y": shear_design["shear_stress_y"], "shear_capacity_concrete_x": shear_design["shear_capacity_concrete_x"], "shear_capacity_concrete_y": shear_design["shear_capacity_concrete_y"], "shear_capacity_enhanced_x": shear_design["tau_c_enhanced_x"], "shear_capacity_enhanced_y": shear_design["tau_c_enhanced_y"], "axial_capacity": capacities["axial"], "moment_capacity_x": capacities["moment_x"], "moment_capacity_y": capacities["moment_y"], "capacity_interaction_ratio": checks["capacity_interaction_ratio"], "capacity_check": checks["individual"]["capacity"], "slenderness_check": checks["individual"]["slenderness_x"] and checks["individual"]["slenderness_y"], "reinforcement_check": checks["individual"]["reinforcement"], "shear_check_x": checks["individual"]["shear_x"], "shear_check_y": checks["individual"]["shear_y"], "is_13920_compliant": seismic.enabled and checks["overall"], "status": "PASS" if checks["overall"] else "FAIL", "remarks": remarks, } except Exception as e: self.logger.error("Column design calculation failed", error=str(e)) raise CalculationError(f"Column design calculation failed: {e}")