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

"""
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 SlabDesignInput(BaseModel): """Input schema for slab design calculation.""" # Dimensions (in mm for consistency with legacy) clear_shorter_span: float = Field(..., gt=0, description="Clear shorter span in mm") clear_longer_span: float = Field(..., gt=0, description="Clear longer span in mm") slab_depth: float = Field(..., gt=0, description="Slab depth in mm") # Support conditions - comprehensive support types from IS 456 support_condition: Literal[ "Interior Panel", "End Panel", "Next to End Panel", "One short edge discontinuous", "One long edge discontinuous", "Two adjacent edges discontinuous", "Two short edges discontinuous", "Two long edges discontinuous", "One long edge continuous", "One short edge continuous", "Four edges discontinuous", ] = Field(..., description="Support condition as per IS 456") # Material properties fck: float = Field(..., gt=0, description="Characteristic strength of concrete in MPa") fy: float = Field(..., gt=0, description="Yield strength of steel in MPa") # Loading dead_load: float = Field(..., ge=0, description="Dead load in kN/m²") live_load: float = Field(..., ge=0, description="Live load in kN/m²") # Cover and bar details clear_cover: float = Field(default=20, gt=0, description="Clear cover in mm") db_short: float = Field(default=12, gt=0, description="Bar diameter for shorter span in mm") db_long: float = Field(default=10, gt=0, description="Bar diameter for longer span in mm")
[docs] @field_validator("fy") @classmethod def validate_fy(cls, v: float) -> float: valid_grades = [250, 415, 500, 550] if v not in valid_grades: raise ValueError(f"Steel grade must be one of {valid_grades} MPa") return v
[docs] @field_validator("fck") @classmethod def validate_fck(cls, v: float) -> float: """Validate concrete grade per IS 456.""" valid_grades = [15, 20, 25, 30, 35, 40, 45, 50] if v not in valid_grades: raise ValueError(f"Concrete grade must be one of {valid_grades} MPa") return v
[docs] @field_validator("slab_depth") @classmethod def validate_practical_depth(cls, v: float) -> float: """Validate practical slab depth limits.""" if v < 100: raise ValueError("Slab depth should be at least 100mm") if v > 500: raise ValueError("Slab depth exceeds practical limit of 500mm") return v
[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