"""
Slab Design Calculator
Concrete slab design as per IS 456:2000.
Performs design of one-way and two-way slabs with comprehensive coefficient tables.
Version: 2.0.0 - IS 456 Compliance Update
- Integrated crack width calculation (IS 456 Annex F via crack_width module)
- Fixed distribution steel using gross area (IS 456 Cl 26.5.2.1)
- Improved shear enhancement factor with linear interpolation
- Added percentage steel tracking
- Enhanced output schema
"""
import math
from typing import Any, Dict, Literal, Optional, Tuple, Type
from pydantic import BaseModel, Field, field_validator
from ..base import BaseCalculator, CalculationError
from .crack_width import CrackWidthCalculator
[docs]
class SlabDesignOutput(BaseModel):
"""Output schema for slab design results."""
# Input summary
inputs: SlabDesignInput
# Basic calculated values
eff_depth_short: float = Field(..., description="Effective depth for shorter span in mm")
eff_depth_long: float = Field(..., description="Effective depth for longer span in mm")
eff_shorter_span: float = Field(..., description="Effective shorter span in mm")
eff_longer_span: float = Field(..., description="Effective longer span in mm")
span_ratio: float = Field(..., description="Span ratio (longer/shorter)")
clear_span_ratio: float = Field(..., description="Clear span ratio")
slab_type: str = Field(..., description="One-way slab or Two-way slab")
# Moment calculations
coefficients: Dict[str, float] = Field(..., description="Moment coefficients used")
moments: Dict[str, float] = Field(..., description="Calculated moments")
# One-way slab results (optional)
M_mid_kNm: Optional[float] = Field(None, description="Midspan moment in kN-m")
M_sup_kNm: Optional[float] = Field(None, description="Support moment in kN-m")
V_kN: Optional[float] = Field(None, description="Shear force in kN")
d_required_for_maxM: Optional[float] = Field(None, description="Depth required for max moment")
# Two-way slab results (optional)
M_short_mid_kNm: Optional[float] = Field(None, description="Short span midspan moment in kN-m")
M_short_sup_kNm: Optional[float] = Field(None, description="Short span support moment in kN-m")
M_long_mid_kNm: Optional[float] = Field(None, description="Long span midspan moment in kN-m")
M_long_sup_kNm: Optional[float] = Field(None, description="Long span support moment in kN-m")
V_sx_kN: Optional[float] = Field(None, description="Shear in short direction in kN")
V_sy_kN: Optional[float] = Field(None, description="Shear in long direction in kN")
# Steel calculations
reinforcement: Dict[str, float] = Field(..., description="Reinforcement details")
# Raw required steel areas for validation
As_short_mid_req: Optional[float] = Field(None, description="Required steel area - short mid")
As_short_sup_req: Optional[float] = Field(None, description="Required steel area - short sup")
As_long_mid_req: Optional[float] = Field(None, description="Required steel area - long mid")
As_long_sup_req: Optional[float] = Field(None, description="Required steel area - long sup")
# Additional provided steel areas for compatibility
As_mid_provided: Optional[float] = Field(None, description="Provided steel area - midspan")
As_sup_provided: Optional[float] = Field(None, description="Provided steel area - support")
As_dist_provided: Optional[float] = Field(
None, description="Provided steel area - distribution"
)
# Spacing details
spacing_mid_provided: Optional[float] = Field(None, description="Midspan spacing provided")
spacing_sup_provided: Optional[float] = Field(None, description="Support spacing provided")
spacing_dist_provided: Optional[float] = Field(
None, description="Distribution spacing provided"
)
# Shear capacity
tau_v: Optional[float] = Field(None, description="Applied shear stress")
tau_v_short: Optional[float] = Field(None, description="Applied shear stress - short direction")
tau_c_eff: Optional[float] = Field(None, description="Effective shear capacity")
tau_c_eff_short: Optional[float] = Field(
None, description="Effective shear capacity - short direction"
)
shear_failure: bool = Field(..., description="Shear failure indicator")
shear_capacity: bool = Field(..., description="Shear capacity check")
# Deflection check
f_s: Optional[float] = Field(None, description="Service stress in steel")
k_t: Optional[float] = Field(None, description="Beeby factor")
l_d_ratio_basic: Optional[float] = Field(None, description="Basic L/d ratio")
l_d_ratio_allowable: Optional[float] = Field(None, description="Allowable L/d ratio")
l_d_ratio_actual: Optional[float] = Field(None, description="Actual L/d ratio")
deflection_check: str = Field(..., description="Deflection check result")
# ====== PHASE 1 ADDITIONS ======
# Percentage steel tracking
rho_actual_short: Optional[float] = Field(None, description="% steel provided - short span")
rho_actual_long: Optional[float] = Field(None, description="% steel provided - long span")
rho_critical: Optional[float] = Field(None, description="% steel critical per code")
# Material properties
Ec: Optional[float] = Field(None, description="Modulus of elasticity of concrete MPa")
Es: float = Field(default=200000.0, description="Modulus of elasticity of steel MPa")
# Stress tracking
fs_service: Optional[float] = Field(None, description="Service stress in steel MPa")
# Crack width per IS 456 Annex F
crack_width: Optional[float] = Field(None, description="Calculated crack width mm")
crack_width_limit: Optional[float] = Field(default=0.3, description="Allowable crack width mm")
crack_width_check: Optional[str] = Field(None, description="Crack width check result")
# ====== END PHASE 1 ADDITIONS ======
# Additional flags
steel_capped_minimum: Optional[Dict[str, bool]] = Field(
None, description="Steel capped to minimum"
)
# Overall status
status: str = Field(..., description="Design status")
error: Optional[str] = Field(None, description="Error message if any")
[docs]
class SlabDesignCalculator(BaseCalculator):
"""
Calculator for concrete slab design per IS 456:2000.
Supports both one-way and two-way slab design with comprehensive
coefficient tables and support conditions.
2.0.0 Updates:
"""
[docs]
def __init__(self) -> None:
super().__init__("slab_design", "2.0.0")
# Initialize coefficient tables from IS 456:2000
self._initialize_coefficients()
@property
def input_schema(self) -> Type[SlabDesignInput]:
return SlabDesignInput
@property
def output_schema(self) -> Type[SlabDesignOutput]:
return SlabDesignOutput
def _initialize_coefficients(self) -> None:
"""Initialize moment and shear coefficient tables from IS 456:2000."""
# One-way slab moment coefficients (IS 456 Cl 22.5)
self.oneway_moment_coeffs = {
"Interior Panel": {
"m_mid_DL": 1 / 16,
"m_sup_DL": 1 / 12,
"m_mid_LL": 1 / 12,
"m_sup_LL": 1 / 9,
},
"End Panel": {
"m_mid_DL": 1 / 12,
"m_sup_DL": 1 / 10,
"m_mid_LL": 1 / 10,
"m_sup_LL": 1 / 9,
},
"Next to End Panel": {
"m_mid_DL": 1 / 16,
"m_sup_DL": 1 / 10,
"m_mid_LL": 1 / 12,
"m_sup_LL": 1 / 9,
},
}
# Two-way slab moment coefficients from IS 456 Table 10.4
self.twoway_moment_coeffs = {
"Interior Panel": {
"short_span": {
"negative": {
1.0: 0.032,
1.1: 0.037,
1.2: 0.043,
1.3: 0.047,
1.4: 0.051,
1.5: 0.053,
1.75: 0.060,
2.0: 0.065,
},
"positive": {
1.0: 0.024,
1.1: 0.028,
1.2: 0.032,
1.3: 0.036,
1.4: 0.039,
1.5: 0.041,
1.75: 0.045,
2.0: 0.049,
},
},
"long_span": {"negative": 0.032, "positive": 0.024},
},
"One short edge discontinuous": {
"short_span": {
"negative": {
1.0: 0.037,
1.1: 0.043,
1.2: 0.048,
1.3: 0.051,
1.4: 0.055,
1.5: 0.057,
1.75: 0.064,
2.0: 0.068,
},
"positive": {
1.0: 0.028,
1.1: 0.032,
1.2: 0.036,
1.3: 0.039,
1.4: 0.041,
1.5: 0.044,
1.75: 0.048,
2.0: 0.052,
},
},
"long_span": {"negative": 0.037, "positive": 0.028},
},
"One long edge discontinuous": {
"short_span": {
"negative": {
1.0: 0.037,
1.1: 0.044,
1.2: 0.052,
1.3: 0.057,
1.4: 0.063,
1.5: 0.067,
1.75: 0.077,
2.0: 0.085,
},
"positive": {
1.0: 0.028,
1.1: 0.033,
1.2: 0.039,
1.3: 0.044,
1.4: 0.047,
1.5: 0.051,
1.75: 0.059,
2.0: 0.065,
},
},
"long_span": {"negative": 0.037, "positive": 0.028},
},
"Two adjacent edges discontinuous": {
"short_span": {
"negative": {
1.0: 0.047,
1.1: 0.053,
1.2: 0.060,
1.3: 0.065,
1.4: 0.071,
1.5: 0.075,
1.75: 0.084,
2.0: 0.091,
},
"positive": {
1.0: 0.035,
1.1: 0.040,
1.2: 0.045,
1.3: 0.049,
1.4: 0.053,
1.5: 0.056,
1.75: 0.063,
2.0: 0.069,
},
},
"long_span": {"negative": 0.047, "positive": 0.035},
},
"Two short edges discontinuous": {
"short_span": {
"negative": {
1.0: 0.045,
1.1: 0.049,
1.2: 0.052,
1.3: 0.056,
1.4: 0.059,
1.5: 0.060,
1.75: 0.065,
2.0: 0.069,
},
"positive": {
1.0: 0.035,
1.1: 0.037,
1.2: 0.040,
1.3: 0.043,
1.4: 0.044,
1.5: 0.045,
1.75: 0.049,
2.0: 0.052,
},
},
"long_span": {"negative": 0, "positive": 0.035},
},
"Two long edges discontinuous": {
"short_span": {
"negative": {
1.0: 0,
1.1: 0,
1.2: 0,
1.3: 0,
1.4: 0,
1.5: 0,
1.75: 0,
2.0: 0,
},
"positive": {
1.0: 0.035,
1.1: 0.043,
1.2: 0.051,
1.3: 0.057,
1.4: 0.063,
1.5: 0.068,
1.75: 0.080,
2.0: 0.088,
},
},
"long_span": {"negative": 0.045, "positive": 0.035},
},
"One long edge continuous": {
"short_span": {
"negative": {
1.0: 0.057,
1.1: 0.064,
1.2: 0.071,
1.3: 0.076,
1.4: 0.080,
1.5: 0.084,
1.75: 0.091,
2.0: 0.097,
},
"positive": {
1.0: 0.043,
1.1: 0.048,
1.2: 0.053,
1.3: 0.057,
1.4: 0.060,
1.5: 0.064,
1.75: 0.069,
2.0: 0.073,
},
},
"long_span": {"negative": 0, "positive": 0.043},
},
"One short edge continuous": {
"short_span": {
"negative": {
1.0: 0,
1.1: 0,
1.2: 0,
1.3: 0,
1.4: 0,
1.5: 0,
1.75: 0,
2.0: 0,
},
"positive": {
1.0: 0.043,
1.1: 0.051,
1.2: 0.059,
1.3: 0.065,
1.4: 0.071,
1.5: 0.076,
1.75: 0.087,
2.0: 0.096,
},
},
"long_span": {"negative": 0.057, "positive": 0.043},
},
"Four edges discontinuous": {
"short_span": {
"negative": {
1.0: 0,
1.1: 0,
1.2: 0,
1.3: 0,
1.4: 0,
1.5: 0,
1.75: 0,
2.0: 0,
},
"positive": {
1.0: 0.056,
1.1: 0.064,
1.2: 0.072,
1.3: 0.079,
1.4: 0.085,
1.5: 0.089,
1.75: 0.100,
2.0: 0.107,
},
},
"long_span": {"negative": 0, "positive": 0.056},
},
}
# Shear coefficients for one-way slab (IS 456 Cl 40.1)
self.shear_coeffs = {
"Interior Panel": {"v_DL": 0.5, "v_LL": 0.5},
"End Panel": {"v_DL": 0.6, "v_LL": 0.6},
"Next to End Panel": {"v_DL": 0.6, "v_LL": 0.6},
}
# Constants for max moment check (IS 456 Annex G)
self.fy_factor = {250: 0.148, 415: 0.138, 500: 0.133, 550: 0.129}
# Two-way slab shear coefficients from IS 456 Table 10.5
self.twoway_shear_coeffs = {
"Interior Panel": {
"short_span": {
1.0: 0.33,
1.1: 0.36,
1.2: 0.39,
1.3: 0.41,
1.4: 0.43,
1.5: 0.45,
1.75: 0.48,
2.0: 0.50,
},
"long_span": 0.33,
},
"One short edge discontinuous": {
"short_span": {
1.0: 0.36,
1.1: 0.39,
1.2: 0.42,
1.3: 0.44,
1.4: 0.45,
1.5: 0.47,
1.75: 0.50,
2.0: 0.52,
},
"long_span": 0.36,
},
"One long edge discontinuous": {
"short_span": {
1.0: 0.36,
1.1: 0.40,
1.2: 0.44,
1.3: 0.47,
1.4: 0.49,
1.5: 0.51,
1.75: 0.55,
2.0: 0.59,
},
"long_span": 0.24,
},
"Two adjacent edges discontinuous": {
"short_span": {
1.0: 0.40,
1.1: 0.44,
1.2: 0.47,
1.3: 0.50,
1.4: 0.52,
1.5: 0.54,
1.75: 0.57,
2.0: 0.60,
},
"long_span": 0.40,
},
"Two short edges discontinuous": {
"short_span": {
1.0: 0.40,
1.1: 0.43,
1.2: 0.45,
1.3: 0.47,
1.4: 0.48,
1.5: 0.49,
1.75: 0.52,
2.0: 0.54,
},
"long_span": 0.26,
},
"Two long edges discontinuous": {
"discontinuous_edge": {
1.0: 0.26,
1.1: 0.30,
1.2: 0.33,
1.3: 0.36,
1.4: 0.38,
1.5: 0.40,
1.75: 0.44,
2.0: 0.47,
},
"long_span": 0.40,
},
"One long edge continuous": {
"short_span": {
1.0: 0.45,
1.1: 0.48,
1.2: 0.51,
1.3: 0.53,
1.4: 0.55,
1.5: 0.57,
1.75: 0.60,
2.0: 0.63,
},
"long_span": 0.0,
},
"One short edge continuous": {
"discontinuous_edge": {
1.0: 0.29,
1.1: 0.33,
1.2: 0.36,
1.3: 0.38,
1.4: 0.40,
1.5: 0.42,
1.75: 0.45,
2.0: 0.48,
},
"long_span": 0.30,
},
"Four edges discontinuous": {
"short_span": {
1.0: 0.33,
1.1: 0.36,
1.2: 0.39,
1.3: 0.41,
1.4: 0.43,
1.5: 0.45,
1.75: 0.48,
2.0: 0.50,
},
"long_span": 0.33,
},
}
[docs]
def get_twoway_coefficients(self, support_condition: str, span_ratio: float) -> Dict[str, Any]:
"""Get moment coefficients for two-way slab with interpolation per IS 456 Table 10.4."""
if support_condition not in self.twoway_moment_coeffs:
return {}
coeffs: dict[str, Any] = self.twoway_moment_coeffs[support_condition]
def interpolate_coefficient(coeff_dict: Any, ratio: float) -> Optional[float]:
if not isinstance(coeff_dict, dict):
if isinstance(coeff_dict, (int, float)):
return float(coeff_dict)
return None
# Type guard to ensure coeff_dict is a dict
coefficient_dict: dict[float, float] = coeff_dict
ratios = sorted(coefficient_dict.keys())
if ratio <= ratios[0]:
val = coefficient_dict[ratios[0]]
return float(val) if isinstance(val, (int, float)) else None
if ratio >= ratios[-1]:
val = coefficient_dict[ratios[-1]]
return float(val) if isinstance(val, (int, float)) else None
for i in range(len(ratios) - 1):
if ratios[i] <= ratio <= ratios[i + 1]:
ratio1, ratio2 = ratios[i], ratios[i + 1]
coeff1, coeff2 = coefficient_dict[ratio1], coefficient_dict[ratio2]
if isinstance(coeff1, (int, float)) and isinstance(coeff2, (int, float)):
return coeff1 + (coeff2 - coeff1) * (ratio - ratio1) / (ratio2 - ratio1)
return None
return {
"short_span": {
"negative": interpolate_coefficient(coeffs["short_span"]["negative"], span_ratio),
"positive": interpolate_coefficient(coeffs["short_span"]["positive"], span_ratio),
},
"long_span": {
"negative": (
float(coeffs["long_span"]["negative"])
if isinstance(coeffs["long_span"]["negative"], (int, float))
else None
),
"positive": (
float(coeffs["long_span"]["positive"])
if isinstance(coeffs["long_span"]["positive"], (int, float))
else None
),
},
}
[docs]
def get_twoway_shear_coefficients(
self, support_condition: str, span_ratio: float
) -> Tuple[Any, ...]:
"""Get shear coefficients for two-way slab with interpolation per IS 456 Table 10.5."""
if support_condition not in self.twoway_shear_coeffs:
return None, None
coeffs = self.twoway_shear_coeffs[support_condition]
if not isinstance(coeffs, dict):
return None, None # type: ignore[unreachable]
long_span_coeff = coeffs.get("long_span")
if not isinstance(long_span_coeff, (int, float)):
long_span_coeff = 0.0
short_span_data = coeffs.get("short_span") or coeffs.get("discontinuous_edge")
if not short_span_data or not isinstance(short_span_data, dict):
return None, None
ratios = sorted(short_span_data.keys())
short_span_coeff = short_span_data[ratios[0]] # Default fallback
if span_ratio <= ratios[0]:
short_span_coeff = short_span_data[ratios[0]]
elif span_ratio >= ratios[-1]:
short_span_coeff = short_span_data[ratios[-1]]
else:
for i in range(len(ratios) - 1):
if ratios[i] <= span_ratio <= ratios[i + 1]:
ratio1, ratio2 = ratios[i], ratios[i + 1]
coeff1, coeff2 = short_span_data[ratio1], short_span_data[ratio2]
if isinstance(coeff1, (int, float)) and isinstance(coeff2, (int, float)):
short_span_coeff = coeff1 + (coeff2 - coeff1) * (span_ratio - ratio1) / (
ratio2 - ratio1
)
break
return short_span_coeff, long_span_coeff
[docs]
def required_steel_area(self, M_kNm: float, b: float, d: float, fck: float, fy: float) -> float:
"""Calculate required steel area per IS 456 Cl 38.1."""
if M_kNm <= 0:
return 0.0
M_Nmm = M_kNm * 1e6
xu_max = 0.48 * d if fy > 250 else 0.53 * d
try:
As = M_Nmm / (0.87 * fy * (d - 0.42 * xu_max))
return max(As, 0.0)
except (ZeroDivisionError, ValueError, TypeError):
return 0.0
[docs]
def round_down_5(self, value: float) -> float:
"""Round down to nearest multiple of 5."""
return math.floor(value / 5) * 5
[docs]
def get_tau_c(self, pt: float, fck: float) -> float:
"""Get design shear strength per IS 456 Table 19."""
# IS 456 Table 19 data
tau_c_table = {
0.15: [0.28, 0.28, 0.29, 0.29, 0.30, 0.30, 0.31, 0.31],
0.25: [0.35, 0.36, 0.36, 0.37, 0.37, 0.38, 0.38, 0.39],
0.50: [0.46, 0.48, 0.49, 0.50, 0.51, 0.51, 0.52, 0.53],
0.75: [0.54, 0.56, 0.57, 0.59, 0.60, 0.61, 0.62, 0.63],
1.00: [0.60, 0.62, 0.64, 0.66, 0.67, 0.68, 0.70, 0.71],
1.25: [0.66, 0.68, 0.70, 0.72, 0.74, 0.75, 0.76, 0.78],
1.50: [0.71, 0.73, 0.75, 0.78, 0.79, 0.81, 0.82, 0.84],
1.75: [0.75, 0.78, 0.80, 0.82, 0.84, 0.86, 0.87, 0.89],
2.00: [0.79, 0.82, 0.84, 0.87, 0.89, 0.91, 0.93, 0.95],
2.25: [0.82, 0.85, 0.88, 0.91, 0.93, 0.95, 0.97, 0.99],
2.50: [0.85, 0.88, 0.91, 0.94, 0.96, 0.99, 1.01, 1.03],
2.75: [0.88, 0.91, 0.94, 0.97, 1.00, 1.02, 1.04, 1.07],
3.00: [0.90, 0.94, 0.97, 0.99, 0.99, 0.99, 1.03, 1.05],
}
fck_values = [15, 20, 25, 30, 35, 40, 45, 50]
pt_values = sorted(tau_c_table.keys())
# Clamp values
pt = max(min(pt, 3.00), 0.15)
fck = max(min(fck, 50), 15)
# Simple bilinear interpolation
pt_lower = pt_upper = 0.15
for i in range(len(pt_values) - 1):
if pt_values[i] <= pt <= pt_values[i + 1]:
pt_lower, pt_upper = pt_values[i], pt_values[i + 1]
break
else:
pt_lower = pt_upper = pt_values[-1] if pt >= pt_values[-1] else pt_values[0]
fck_idx = min(range(len(fck_values)), key=lambda i: abs(fck_values[i] - fck))
if pt_lower == pt_upper:
return tau_c_table[pt_lower][fck_idx]
tau_c_lower = tau_c_table[pt_lower][fck_idx]
tau_c_upper = tau_c_table[pt_upper][fck_idx]
return tau_c_lower + (tau_c_upper - tau_c_lower) * (pt - pt_lower) / (pt_upper - pt_lower)
[docs]
def get_slab_depth_factor(self, d: float) -> float:
"""
Get depth factor per IS 456 Cl 40.2.1.1 with linear interpolation.
Reference: IS 456:2000 Table 19 (enhancement factor)
"""
if d <= 150:
return 1.30
elif d <= 175:
# Linear interpolation between 150 and 175
return 1.30 - (1.30 - 1.25) * (d - 150) / 25
elif d <= 200:
return 1.25 - (1.25 - 1.20) * (d - 175) / 25
elif d <= 225:
return 1.20 - (1.20 - 1.15) * (d - 200) / 25
elif d <= 250:
return 1.15 - (1.15 - 1.10) * (d - 225) / 25
elif d <= 275:
return 1.10 - (1.10 - 1.05) * (d - 250) / 25
elif d <= 300:
return 1.05 - (1.05 - 1.00) * (d - 275) / 25
else:
return 1.00
# ====== PHASE 1 NEW METHODS ======
[docs]
def calculate_modulus_of_elasticity_concrete(self, fck: float) -> float:
"""
Calculate modulus of elasticity of concrete per IS 456 Cl 6.2.3.1.
Reference: IS 456:2000 Clause 6.2.3.1
Ec = 5000 * sqrt(fck) MPa
"""
return 5000.0 * math.sqrt(fck)
[docs]
def calculate_crack_width_via_module(
self,
b: float,
h: float,
d: float,
cover: float,
db: float,
spacing: float,
As: float,
M_service_kNm: float,
concrete_grade: str,
exposure: str = "moderate",
) -> Dict[str, Any]:
"""
Calculate crack width using the standalone CrackWidthCalculator module.
Reference: IS 456:2000 Annex F (via crack_width.py)
Args:
b: Width of section (mm)
h: Overall depth (mm)
d: Effective depth (mm)
cover: Clear cover (mm)
db: Diameter of bar (mm)
spacing: Bar spacing (mm)
As: Area of steel (mm²)
M_service_kNm: Service moment (kN-m)
concrete_grade: Concrete grade string (e.g., "M25")
exposure: Exposure condition (moderate/severe/extreme)
Returns:
Dictionary with crack width results from CrackWidthCalculator
"""
calc = CrackWidthCalculator()
inputs = {
"width": b,
"depth": h,
"effective_depth": d,
"cover": cover,
"bar_diameter": db,
"bar_spacing": spacing,
"steel_area": As,
"service_moment": M_service_kNm,
"concrete_grade": concrete_grade,
"exposure": exposure,
}
return calc.calculate(inputs)
# ====== END PHASE 1 NEW METHODS ======
[docs]
def beeby_factor(self, f_s: float, b: float, d: float, As: float) -> float:
"""Calculate Beeby factor per IS 456 Annex C."""
if As <= 1.0 or As < 50.0:
return 1.0
rho = As / (b * d)
k_t = 1.0 + (f_s / 1000.0) * (1.0 / (1.0 + 10.0 * rho))
return max(1.0, min(k_t, 2.0))
[docs]
def actual_As_provided(self, db: float, spacing: float) -> float:
"""Calculate provided steel area."""
return (math.pi * (db**2) / 4.0) * (1000.0 / spacing)
[docs]
def calculate(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
"""Perform slab design calculation per IS 456:2000."""
try:
self.logger.info("Starting slab design calculation", code_reference="IS 456:2000")
# Extract and validate inputs
validated_inputs = self._validate_and_extract_inputs(inputs)
# Compute effective depths and spans per IS 456 Cl 26.1
results = self._compute_effective_dimensions(validated_inputs)
# Classify and design slab per IS 456 Cl 24.1
span_ratio = results["span_ratio"]
if span_ratio > 2.0:
results["slab_type"] = "One-way slab"
self.logger.info("Slab classified as one-way", span_ratio=span_ratio)
results = self._design_oneway_slab(results, validated_inputs)
else:
results["slab_type"] = "Two-way slab"
self.logger.info("Slab classified as two-way", span_ratio=span_ratio)
results = self._design_twoway_slab(results, validated_inputs)
# Set overall status
if "error" not in results:
passes_shear = results.get("shear_capacity", False)
passes_deflection = "OK" in results.get("deflection_check", "")
results["status"] = "PASS" if (passes_shear and passes_deflection) else "FAIL"
else:
results["status"] = "FAIL"
self.logger.info(
"Slab design completed",
status=results["status"],
slab_type=results["slab_type"],
)
return results
except Exception as e:
error_msg = f"Slab design calculation failed: {e!s}"
self.logger.error("Calculation failed", error=str(e))
raise CalculationError(error_msg)
def _validate_and_extract_inputs(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
"""
Validate and extract inputs for slab design.
Returns:
dict: Extracted and validated inputs
Raises:
ValueError: If steel grade is unsupported
"""
# Extract inputs
extracted = {
"clear_shorter_span": inputs["clear_shorter_span"],
"clear_longer_span": inputs["clear_longer_span"],
"slab_depth": inputs["slab_depth"],
"support_condition": inputs["support_condition"],
"fck": inputs["fck"],
"fy": inputs["fy"],
"dead_load": inputs["dead_load"],
"live_load": inputs["live_load"],
"clear_cover": inputs["clear_cover"],
"db_short": inputs["db_short"],
"db_long": inputs["db_long"],
}
# Validate steel grade
if int(extracted["fy"]) not in self.fy_factor:
raise ValueError(f"Unsupported steel grade. Use {list(self.fy_factor.keys())} MPa.")
return extracted
def _compute_effective_dimensions(self, validated_inputs: Dict[str, Any]) -> Dict[str, Any]:
"""
Compute effective depths and spans per IS 456 Cl 26.1.
Args:
validated_inputs: Validated input parameters
Returns:
dict: Effective dimensions and ratios
"""
slab_depth = validated_inputs["slab_depth"]
clear_cover = validated_inputs["clear_cover"]
db_short = validated_inputs["db_short"]
db_long = validated_inputs["db_long"]
clear_shorter_span = validated_inputs["clear_shorter_span"]
clear_longer_span = validated_inputs["clear_longer_span"]
# Calculate effective depths
eff_depth_short = slab_depth - clear_cover - (db_short / 2.0)
eff_depth_long = eff_depth_short - (db_short / 2.0) - (db_long / 2.0)
# Calculate effective spans
eff_shorter_span = clear_shorter_span + eff_depth_short
eff_longer_span = clear_longer_span + eff_depth_long
# Calculate ratios
span_ratio = eff_longer_span / eff_shorter_span
clear_span_ratio = clear_longer_span / clear_shorter_span
return {
"eff_depth_short": eff_depth_short,
"eff_depth_long": eff_depth_long,
"eff_shorter_span": eff_shorter_span,
"eff_longer_span": eff_longer_span,
"span_ratio": span_ratio,
"clear_span_ratio": clear_span_ratio,
}
def _create_error_result(self, inputs: Dict[str, Any], error_msg: str) -> Dict[str, Any]:
"""Create standardized error result."""
return {
"inputs": inputs,
"error": error_msg,
"status": "FAIL",
"slab_type": "Unknown",
"coefficients": {},
"moments": {},
"reinforcement": {},
"shear_failure": True,
"shear_capacity": False,
"deflection_check": "Error in calculation",
"eff_depth_short": 0,
"eff_depth_long": 0,
"eff_shorter_span": 0,
"eff_longer_span": 0,
"span_ratio": 0,
"clear_span_ratio": 0,
}
def _design_oneway_slab(
self, results: Dict[str, Any], validated_inputs: Dict[str, Any]
) -> Dict[str, Any]:
"""Design one-way slab per IS 456."""
support_condition = validated_inputs["support_condition"]
eff_shorter_span = results["eff_shorter_span"]
eff_depth_short = results["eff_depth_short"]
slab_depth = validated_inputs["slab_depth"]
db_short = validated_inputs["db_short"]
db_long = validated_inputs["db_long"]
clear_cover = validated_inputs["clear_cover"]
fck = validated_inputs["fck"]
fy = validated_inputs["fy"]
dead_load = validated_inputs["dead_load"]
live_load = validated_inputs["live_load"]
mc = self.oneway_moment_coeffs[support_condition]
sc = self.shear_coeffs[support_condition]
Lm = eff_shorter_span / 1000.0
# Moment calculations per IS 456 Cl 22.5
M_mid_kNm = 1.5 * (mc["m_mid_DL"] * dead_load + mc["m_mid_LL"] * live_load) * (Lm**2)
M_sup_kNm = 1.5 * (mc["m_sup_DL"] * dead_load + mc["m_sup_LL"] * live_load) * (Lm**2)
results.update(
{
"M_mid_kNm": M_mid_kNm,
"M_sup_kNm": M_sup_kNm,
"coefficients": mc,
"moments": {"M_short_mid": M_mid_kNm, "M_short_sup": M_sup_kNm},
}
)
# Under-reinforced check per IS 456 Annex G
M_max_kNm = max(M_mid_kNm, M_sup_kNm)
b = 1000.0
K = self.fy_factor[int(fy)]
d_req = math.sqrt(M_max_kNm * 1e6 / (K * fck * b))
results["d_required_for_maxM"] = d_req
if d_req > eff_depth_short:
results["error"] = (
f"Insufficient depth. Required {d_req:.1f}mm, provided {eff_depth_short:.1f}mm"
)
return results
# Shear calculations per IS 456 Cl 40.1
V_kN = 1.5 * (sc["v_DL"] * dead_load + sc["v_LL"] * live_load) * Lm
results["V_kN"] = V_kN
# Steel area calculations per IS 456 Cl 38.1
As_mid_raw = self.required_steel_area(M_mid_kNm, b, eff_depth_short, fck, fy)
As_sup_raw = self.required_steel_area(M_sup_kNm, b, eff_depth_short, fck, fy)
# PHASE 1 FIX: Minimum steel per IS 456 Cl 26.5.2.1
# For flexural reinforcement, use effective depth
As_min_flexural = 0.0012 * b * eff_depth_short
As_mid_final = max(As_mid_raw, As_min_flexural)
As_sup_final = max(As_sup_raw, As_min_flexural)
# PHASE 1 FIX: Distribution steel uses GROSS AREA (slab depth, not effective depth)
As_min_dist = 0.0012 * b * slab_depth # Uses overall depth D
# Spacing calculations per IS 456 Cl 26.3.3
max_spacing_main = min(300.0, 3 * eff_depth_short)
max_spacing_dist = min(450.0, 5 * eff_depth_short)
spacing_mid = self.round_down_5(
min(
(math.pi * db_short**2 / 4.0 * 1000.0 / As_mid_final),
max_spacing_main,
)
)
spacing_sup = self.round_down_5(
min(
(math.pi * db_short**2 / 4.0 * 1000.0 / As_sup_final),
max_spacing_main,
)
)
spacing_dist = self.round_down_5(
min((math.pi * db_long**2 / 4.0 * 1000.0 / As_min_dist), max_spacing_dist)
)
As_mid_provided = self.actual_As_provided(db_short, spacing_mid)
As_sup_provided = self.actual_As_provided(db_short, spacing_sup)
As_dist_provided = self.actual_As_provided(db_long, spacing_dist)
results.update(
{
"reinforcement": {
"As_mid": As_mid_final,
"As_sup": As_sup_final,
"As_dist": As_min_dist, # Distribution steel from gross area
"spacing_mid": spacing_mid,
"spacing_sup": spacing_sup,
"spacing_dist": spacing_dist,
"As_mid_provided": As_mid_provided,
"As_sup_provided": As_sup_provided,
"As_dist_provided": As_dist_provided,
},
"As_mid_provided": As_mid_provided,
"As_sup_provided": As_sup_provided,
"As_dist_provided": As_dist_provided,
"spacing_mid_provided": spacing_mid,
"spacing_sup_provided": spacing_sup,
"spacing_dist_provided": spacing_dist,
}
)
# Shear capacity check per IS 456 Cl 40.2.1
pt_short = 100.0 * As_mid_provided / (b * eff_depth_short)
tau_c = self.get_tau_c(pt_short, fck)
# PHASE 1 FIX: Use improved depth factor with linear interpolation
alpha_slab = self.get_slab_depth_factor(eff_depth_short)
tau_c_eff = tau_c * alpha_slab
tau_v = V_kN * 1000.0 / (b * eff_depth_short)
results.update(
{
"tau_v": tau_v,
"tau_c_eff": tau_c_eff,
"shear_failure": tau_v > tau_c_eff,
"shear_capacity": tau_v <= tau_c_eff,
}
)
# Deflection check per IS 456 Cl 24.1 (Note 2)
# For two-way slabs with mild steel: Simply supported = 35, Continuous = 40
# For Fe415 (high strength deformed bars): multiply by 0.8
Lm_shorter = eff_shorter_span / 1000.0 # Span in meters
# Service stress for Beeby factor
fs_service = (
0.58 * fy * (As_mid_final / As_mid_provided) if As_mid_provided > 0 else 0.58 * fy
)
kt = self.beeby_factor(fs_service, b, eff_depth_short, As_mid_provided)
# Basic L/d ratios per IS 456 Cl 24.1 Note 2 (for slabs up to 3.5m)
# Simply supported: 35 × 0.8 = 28, Continuous: 40 × 0.8 = 32 (for Fe415)
fy_factor_defl = 0.8 if fy >= 415 else 1.0
l_d_basic = 40.0 * fy_factor_defl # Assume continuous for most slabs
if Lm_shorter <= 3.5:
# Use span/overall depth ratio per IS 456 Note 2
l_d_allowable = l_d_basic * kt
l_d_actual = eff_shorter_span / slab_depth # Use overall depth D
else:
# For spans > 3.5m, use effective depth with modification
l_d_allowable = l_d_basic * kt * (3.5 / Lm_shorter)
l_d_actual = eff_shorter_span / eff_depth_short
deflection_status = "OK" if l_d_actual <= l_d_allowable else "FAIL"
deflection_msg = (
f"Deflection check is {deflection_status}. "
f"Actual L/d={l_d_actual:.1f}, Allowable={l_d_allowable:.1f}"
)
results.update(
{
"f_s": fs_service,
"k_t": kt,
"l_d_ratio_basic": l_d_basic,
"l_d_ratio_allowable": l_d_allowable,
"l_d_ratio_actual": l_d_actual,
"deflection_check": deflection_msg,
}
)
# ====== PHASE 1 ADDITIONS ======
# Calculate percentage steel
rho_actual = 100.0 * As_mid_provided / (b * eff_depth_short)
# Calculate modulus of elasticity
Ec = self.calculate_modulus_of_elasticity_concrete(fck)
Es = 200000.0
# Calculate crack width using standalone module (IS 456 Annex F)
# Service moment for one-way slab = M_mid / 1.5 (unfactored)
M_service_kNm = M_mid_kNm / 1.5
concrete_grade_str = f"M{int(fck)}"
crack_result = self.calculate_crack_width_via_module(
b=b,
h=slab_depth,
d=eff_depth_short,
cover=clear_cover,
db=db_short,
spacing=spacing_mid,
As=As_mid_provided,
M_service_kNm=M_service_kNm,
concrete_grade=concrete_grade_str,
exposure="moderate",
)
crack_width = crack_result.get("crack_width_calculated", 0.0)
crack_width_limit = crack_result.get("crack_width_allowable", 0.3)
crack_width_status = crack_result.get("status", "SAFE")
crack_width_msg = crack_result.get("status_message", "")
results.update(
{
"rho_actual_short": rho_actual,
"rho_critical": 0.85, # Typical critical percentage for Fe 415
"Ec": Ec,
"Es": Es,
"fs_service": fs_service,
"crack_width": crack_width,
"crack_width_limit": crack_width_limit,
"crack_width_check": crack_width_msg,
}
)
# ====== END PHASE 1 ADDITIONS ======
return results
def _design_twoway_slab(
self, results: Dict[str, Any], validated_inputs: Dict[str, Any]
) -> Dict[str, Any]:
"""Design two-way slab per IS 456."""
support_condition = validated_inputs["support_condition"]
span_ratio = results["span_ratio"]
eff_shorter_span = results["eff_shorter_span"]
eff_longer_span = results["eff_longer_span"]
eff_depth_short = results["eff_depth_short"]
eff_depth_long = results["eff_depth_long"]
slab_depth = validated_inputs["slab_depth"]
db_short = validated_inputs["db_short"]
db_long = validated_inputs["db_long"]
clear_cover = validated_inputs["clear_cover"]
fck = validated_inputs["fck"]
fy = validated_inputs["fy"]
dead_load = validated_inputs["dead_load"]
live_load = validated_inputs["live_load"]
coeffs = self.get_twoway_coefficients(support_condition, span_ratio)
if not coeffs:
results["error"] = f"Invalid support condition: {support_condition}"
return results
# Moment calculations per IS 456 Table 10.4
Ls = eff_shorter_span / 1000.0
total_load = dead_load + live_load
M_short_mid_fact = 1.5 * coeffs["short_span"]["positive"] * total_load * (Ls**2)
M_short_sup_fact = 1.5 * coeffs["short_span"]["negative"] * total_load * (Ls**2)
M_long_mid_fact = 1.5 * coeffs["long_span"]["positive"] * total_load * (Ls**2)
M_long_sup_fact = 1.5 * coeffs["long_span"]["negative"] * total_load * (Ls**2)
results.update(
{
"M_short_mid_kNm": M_short_mid_fact,
"M_short_sup_kNm": M_short_sup_fact,
"M_long_mid_kNm": M_long_mid_fact,
"M_long_sup_kNm": M_long_sup_fact,
"coefficients": {
"alpha_x_mid": coeffs["short_span"]["positive"],
"alpha_x_sup": coeffs["short_span"]["negative"],
"alpha_y_mid": coeffs["long_span"]["positive"],
"alpha_y_sup": coeffs["long_span"]["negative"],
},
"moments": {
"M_short_mid": M_short_mid_fact / 1.5,
"M_short_sup": M_short_sup_fact / 1.5,
"M_long_mid": M_long_mid_fact / 1.5,
"M_long_sup": M_long_sup_fact / 1.5,
},
}
)
# Steel area calculations per IS 456 Cl 38.1
b = 1000.0
As_short_mid = self.required_steel_area(M_short_mid_fact, b, eff_depth_short, fck, fy)
As_short_sup = self.required_steel_area(M_short_sup_fact, b, eff_depth_short, fck, fy)
As_long_mid = self.required_steel_area(M_long_mid_fact, b, eff_depth_long, fck, fy)
As_long_sup = self.required_steel_area(M_long_sup_fact, b, eff_depth_long, fck, fy)
results.update(
{
"As_short_mid_req": As_short_mid,
"As_short_sup_req": As_short_sup,
"As_long_mid_req": As_long_mid,
"As_long_sup_req": As_long_sup,
}
)
# PHASE 1 FIX: Minimum steel per IS 456 Cl 26.5.2.1
# For flexural reinforcement, use effective depth
As_min_short = 0.0012 * b * eff_depth_short
As_min_long = 0.0012 * b * eff_depth_long
As_short_mid_final = max(As_short_mid, As_min_short)
As_short_sup_final = max(As_short_sup, As_min_short)
As_long_mid_final = max(As_long_mid, As_min_long)
As_long_sup_final = max(As_long_sup, As_min_long)
# Spacing calculations per IS 456 Cl 26.3.3
max_spacing = min(300.0, 3 * min(eff_depth_short, eff_depth_long))
spacing_short_mid = self.round_down_5(
min(
(math.pi * db_short**2 / 4.0 * 1000.0 / As_short_mid_final),
max_spacing,
)
)
spacing_short_sup = self.round_down_5(
min(
(math.pi * db_short**2 / 4.0 * 1000.0 / As_short_sup_final),
max_spacing,
)
)
spacing_long_mid = self.round_down_5(
min((math.pi * db_long**2 / 4.0 * 1000.0 / As_long_mid_final), max_spacing)
)
spacing_long_sup = self.round_down_5(
min((math.pi * db_long**2 / 4.0 * 1000.0 / As_long_sup_final), max_spacing)
)
As_short_mid_provided = self.actual_As_provided(db_short, spacing_short_mid)
As_short_sup_provided = self.actual_As_provided(db_short, spacing_short_sup)
As_long_mid_provided = self.actual_As_provided(db_long, spacing_long_mid)
As_long_sup_provided = self.actual_As_provided(db_long, spacing_long_sup)
# Store all reinforcement details
reinforcement = {
"As_short_mid": As_short_mid_final,
"As_short_sup": As_short_sup_final,
"As_long_mid": As_long_mid_final,
"As_long_sup": As_long_sup_final,
"spacing_short_mid": spacing_short_mid,
"spacing_short_sup": spacing_short_sup,
"spacing_long_mid": spacing_long_mid,
"spacing_long_sup": spacing_long_sup,
"As_short_mid_provided": As_short_mid_provided,
"As_short_sup_provided": As_short_sup_provided,
"As_long_mid_provided": As_long_mid_provided,
"As_long_sup_provided": As_long_sup_provided,
}
results["reinforcement"] = reinforcement
results.update(reinforcement) # Add individual keys for compatibility
# Shear calculations per IS 456 Table 10.5
alpha_sx, alpha_sy = self.get_twoway_shear_coefficients(support_condition, span_ratio)
if alpha_sx is None:
results["error"] = f"Could not determine shear coefficients for: {support_condition}"
return results
V_sx_fact = 1.5 * alpha_sx * total_load * Ls
V_sy_fact = 1.5 * alpha_sy * total_load * (eff_longer_span / 1000.0)
results.update({"V_sx_kN": V_sx_fact, "V_sy_kN": V_sy_fact})
# Shear capacity check per IS 456 Cl 40.2.1
pt_short = 100.0 * As_short_mid_provided / (b * eff_depth_short)
tau_c_short = self.get_tau_c(pt_short, fck)
# PHASE 1 FIX: Use improved depth factor with linear interpolation
alpha_slab = self.get_slab_depth_factor(eff_depth_short)
tau_c_eff_short = tau_c_short * alpha_slab
tau_v_short = V_sx_fact * 1000.0 / (b * eff_depth_short)
results.update(
{
"tau_v_short": tau_v_short,
"tau_c_eff_short": tau_c_eff_short,
"shear_failure": tau_v_short > tau_c_eff_short,
"shear_capacity": tau_v_short <= tau_c_eff_short,
}
)
# Deflection check per IS 456 Cl 24.1 (Note 2)
# For two-way slabs with mild steel: Simply supported = 35, Continuous = 40
# For Fe415 (high strength deformed bars): multiply by 0.8
Lm_shorter = eff_shorter_span / 1000.0 # Span in meters
# Service stress for Beeby factor
fs_service = (
0.58 * fy * (As_short_mid_final / As_short_mid_provided)
if As_short_mid_provided > 0
else 0.58 * fy
)
kt = self.beeby_factor(fs_service, b, eff_depth_short, As_short_mid_provided)
# Basic L/d ratios per IS 456 Cl 24.1 Note 2 (for slabs up to 3.5m)
# Simply supported: 35 × 0.8 = 28, Continuous: 40 × 0.8 = 32 (for Fe415)
fy_factor_defl = 0.8 if fy >= 415 else 1.0
l_d_basic = 40.0 * fy_factor_defl # Two-way slabs are typically continuous
if Lm_shorter <= 3.5:
# Use span/overall depth ratio per IS 456 Note 2
l_d_allowable = l_d_basic * kt
l_d_actual = eff_shorter_span / slab_depth # Use overall depth D
else:
# For spans > 3.5m, use effective depth with modification
l_d_allowable = l_d_basic * kt * (3.5 / Lm_shorter)
l_d_actual = eff_shorter_span / eff_depth_short
deflection_status = "OK" if l_d_actual <= l_d_allowable else "FAIL"
deflection_msg = (
f"Deflection check is {deflection_status}. "
f"Actual L/d={l_d_actual:.1f}, Allowable={l_d_allowable:.1f}"
)
results.update(
{
"f_s": fs_service,
"k_t": kt,
"l_d_ratio_basic": l_d_basic,
"l_d_ratio_allowable": l_d_allowable,
"l_d_ratio_actual": l_d_actual,
"deflection_check": deflection_msg,
}
)
# ====== PHASE 1 ADDITIONS ======
# Calculate percentage steel for both directions
rho_actual_short = 100.0 * As_short_mid_provided / (b * eff_depth_short)
rho_actual_long = 100.0 * As_long_mid_provided / (b * eff_depth_long)
# Calculate modulus of elasticity
Ec = self.calculate_modulus_of_elasticity_concrete(fck)
Es = 200000.0
# Calculate crack width using standalone module (IS 456 Annex F)
# Service moment for two-way slab = M_short_mid / 1.5 (unfactored, critical direction)
M_service_kNm = M_short_mid_fact / 1.5
concrete_grade_str = f"M{int(fck)}"
crack_result = self.calculate_crack_width_via_module(
b=b,
h=slab_depth,
d=eff_depth_short,
cover=clear_cover,
db=db_short,
spacing=spacing_short_mid,
As=As_short_mid_provided,
M_service_kNm=M_service_kNm,
concrete_grade=concrete_grade_str,
exposure="moderate",
)
crack_width = crack_result.get("crack_width_calculated", 0.0)
crack_width_limit = crack_result.get("crack_width_allowable", 0.3)
crack_width_status = crack_result.get("status", "SAFE")
crack_width_msg = crack_result.get("status_message", "")
results.update(
{
"rho_actual_short": rho_actual_short,
"rho_actual_long": rho_actual_long,
"rho_critical": 0.85, # Typical critical percentage for Fe 415
"Ec": Ec,
"Es": Es,
"fs_service": fs_service,
"crack_width": crack_width,
"crack_width_limit": crack_width_limit,
"crack_width_check": crack_width_msg,
}
)
# ====== END PHASE 1 ADDITIONS ======
return results