"""
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 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}")