Source code for api.app.calculators.steel.purlin_design

"""
Purlin Design Calculator

Cold-formed steel purlin design as per IS 801:1975.
Performs comprehensive design including bending, shear, web crippling, deflection checks.
"""

import math
from typing import Any, Dict, List, Literal, Optional, Type

import structlog
from pydantic import BaseModel, Field, field_validator, model_validator

from ..base import BaseCalculator, CalculationError

# Import profile system
from .purlin_profiles import ALL_PROFILES, get_profile_properties

logger = structlog.get_logger(__name__)

# IS 801:1975 Constants
DEGREES_TO_RADIANS = math.pi / 180
KGF_TO_N = 9.81
KGF_CM2_TO_N_MM2 = 10.197  # Conversion factor from kgf/cm² to N/mm²
MIN_STIFFENER_INERTIA = 9.2
DEFAULT_CB = 1  # Default value for Cb in bending calculations


[docs] class SectionProperties(BaseModel): """Section properties for cold-formed steel purlin.""" # Profile selection (alternative to manual input) profile_name: Optional[str] = Field( default=None, description="Standard profile name (e.g., 'Z20015')" ) profile_type: Optional[Literal["Z45", "Z90", "C"]] = Field( default=None, description="Profile type" ) # Manual section properties (used if profile not selected) thickness: Optional[float] = Field(default=None, gt=0, description="Section thickness in mm") depth: Optional[float] = Field(default=None, gt=0, description="Section depth in mm") flange_width_tp: Optional[float] = Field( default=None, gt=0, description="Top flange width in mm" ) flange_width_bt: Optional[float] = Field( default=None, gt=0, description="Bottom flange width in mm" ) lip_depth: Optional[float] = Field(default=None, ge=0, description="Lip depth in mm") lip_angle: Optional[float] = Field( default=None, ge=0, le=90, description="Lip angle in degrees" ) Ixx: Optional[float] = Field( default=None, gt=0, description="Moment of inertia about X-X axis in mm⁴" ) Iyy: Optional[float] = Field( default=None, gt=0, description="Moment of inertia about Y-Y axis in mm⁴" ) Zxx_tp: Optional[float] = Field( default=None, gt=0, description="Section modulus top fiber X-X axis in mm³" ) Zxx_bt: Optional[float] = Field( default=None, gt=0, description="Section modulus bottom fiber X-X axis in mm³" ) Zyy: Optional[float] = Field( default=None, gt=0, description="Section modulus about Y-Y axis in mm³" ) area: Optional[float] = Field(default=None, gt=0, description="Cross-sectional area in mm²") Rad: Optional[float] = Field(default=2.0, gt=0, description="Corner radius in mm") # Computed properties (populated from profile or manual input) _computed_properties: Optional[Dict[str, float]] = None
[docs] def get_properties(self) -> Dict[str, float]: """Get section properties, either from profile lookup or manual input.""" if self._computed_properties is not None: return self._computed_properties # If profile specified, use profile lookup if self.profile_name and self.profile_type: profile_props = get_profile_properties(self.profile_name, self.profile_type) if not profile_props: raise ValueError( f"Profile {self.profile_name} not found in {self.profile_type} profiles" ) # All profile properties are now in mm units, no conversion needed self._computed_properties = { "thickness": ( float(profile_props["thickness"]) if profile_props["thickness"] is not None else 0.0 ), "depth": ( float(profile_props["depth"]) if profile_props["depth"] is not None else 0.0 ), "flange_width_tp": ( float(profile_props["flange_width_tp"]) if profile_props["flange_width_tp"] is not None else 0.0 ), "flange_width_bt": ( float(profile_props["flange_width_bt"]) if profile_props["flange_width_bt"] is not None else 0.0 ), "lip_depth": ( float(profile_props["lip_depth"]) if profile_props["lip_depth"] is not None else 0.0 ), "lip_angle": ( float(profile_props["lip_angle"]) if profile_props["lip_angle"] is not None else 90.0 ), "Rad": float(profile_props["Rad"]) if profile_props["Rad"] is not None else 2.0, "mass": ( float(profile_props.get("mass", 0.0)) if profile_props.get("mass", 0.0) is not None else 0.0 ), "area": float(profile_props["area"]) if profile_props["area"] is not None else 0.0, "Ixx": float(profile_props["Ixx"]) if profile_props["Ixx"] is not None else 0.0, "Iyy": float(profile_props["Iyy"]) if profile_props["Iyy"] is not None else 0.0, "Zxx_tp": ( float(profile_props["Zxx_tp"]) if profile_props["Zxx_tp"] is not None else 0.0 ), "Zxx_bt": ( float(profile_props["Zxx_bt"]) if profile_props["Zxx_bt"] is not None else 0.0 ), "Zyy": float(profile_props["Zyy"]) if profile_props["Zyy"] is not None else 0.0, "Rx": ( float(profile_props.get("Rx", 0.0)) if profile_props.get("Rx", 0.0) is not None else 0.0 ), "Ry": ( float(profile_props.get("Ry", 0.0)) if profile_props.get("Ry", 0.0) is not None else 0.0 ), "Yb": ( float(profile_props.get("Yb", 0.0)) if profile_props.get("Yb", 0.0) is not None else 0.0 ), } return self._computed_properties # If manual properties specified, use them elif all( [ self.thickness is not None, self.depth is not None, self.flange_width_tp is not None, self.flange_width_bt is not None, self.Ixx is not None, self.Iyy is not None, self.Zxx_tp is not None, self.Zxx_bt is not None, self.Zyy is not None, self.area is not None, ] ): self._computed_properties = { "thickness": float(self.thickness) if self.thickness is not None else 0.0, "depth": float(self.depth) if self.depth is not None else 0.0, "flange_width_tp": ( float(self.flange_width_tp) if self.flange_width_tp is not None else 0.0 ), "flange_width_bt": ( float(self.flange_width_bt) if self.flange_width_bt is not None else 0.0 ), "lip_depth": float(self.lip_depth) if self.lip_depth is not None else 0.0, "lip_angle": float(self.lip_angle) if self.lip_angle is not None else 90.0, "Rad": float(self.Rad) if self.Rad is not None else 2.0, "area": float(self.area) if self.area is not None else 0.0, "Ixx": float(self.Ixx) if self.Ixx is not None else 0.0, "Iyy": float(self.Iyy) if self.Iyy is not None else 0.0, "Zxx_tp": float(self.Zxx_tp) if self.Zxx_tp is not None else 0.0, "Zxx_bt": float(self.Zxx_bt) if self.Zxx_bt is not None else 0.0, "Zyy": float(self.Zyy) if self.Zyy is not None else 0.0, } return self._computed_properties else: # Always raise error, never return None raise ValueError( "Either specify standard profile (profile_name + profile_type) or all required manual properties" )
[docs] @model_validator(mode="after") def validate_profile_selection(self) -> "SectionProperties": """Validate profile selection after all fields are set.""" if self.profile_name: if not self.profile_type: raise ValueError("profile_type must be specified when profile_name is provided") # Check if profile exists profile_props = get_profile_properties(self.profile_name, self.profile_type) if not profile_props: raise ValueError( f"Profile {self.profile_name} not found in {self.profile_type} profiles" ) return self
[docs] class MaterialProperties(BaseModel): """Material properties for steel.""" Fy: float = Field(default=350, gt=0, description="Yield strength in MPa") E: float = Field(default=200000, gt=0, description="Elastic modulus in MPa")
[docs] @field_validator("Fy") @classmethod def validate_yield_strength(cls, v: float) -> float: """Validate yield strength range for cold-formed steel.""" if not (200 <= v <= 500): raise ValueError("Yield strength should be between 200-500 MPa for cold-formed steel") return v
[docs] class LoadingData(BaseModel): """Loading data for purlin design.""" roof: float = Field(..., ge=0, description="Roof dead load in kN/m²") purlin: float = Field(..., ge=0, description="Purlin self-weight in kN/m") live: float = Field(..., ge=0, description="Live load in kN/m²") wind: float = Field(..., description="Wind load in kN/m² (can be negative)")
[docs] @field_validator("wind") @classmethod def validate_wind_load(cls, v: float) -> float: """Validate wind load range.""" if not (-10 <= v <= 10): raise ValueError("Wind load should be between -10 and 10 kN/m²") return v
[docs] class PurlinDesignInput(BaseModel): """Input schema for purlin design calculation.""" # Geometric parameters span: float = Field(..., gt=0, description="Purlin span in meters") spacing: float = Field(..., gt=0, description="Purlin spacing in meters") sag_rods: int = Field(default=0, ge=0, description="Number of sag rods") inclination: float = Field(default=0, ge=0, le=45, description="Roof inclination in degrees") # Section and material section: SectionProperties = Field(..., description="Section properties") material: MaterialProperties = Field(..., description="Material properties") # Loading loads: LoadingData = Field(..., description="Loading data")
[docs] @field_validator("span") @classmethod def validate_span(cls, v: float) -> float: """Validate span range.""" if not (1 <= v <= 12): raise ValueError("Span should be between 1-12 meters for typical purlins") return v
[docs] @field_validator("spacing") @classmethod def validate_spacing(cls, v: float) -> float: """Validate spacing range.""" if not (0.5 <= v <= 3): raise ValueError("Spacing should be between 0.5-3 meters") return v
[docs] class LoadResults(BaseModel): """Load calculation results.""" DL: float = Field(..., description="Total dead load in kN/m") LL: float = Field(..., description="Total live load in kN/m") WL: float = Field(..., description="Total wind load in kN/m") DL_major: float = Field(..., description="Dead load major axis component in kN/m") DL_minor: float = Field(..., description="Dead load minor axis component in kN/m") LL_major: float = Field(..., description="Live load major axis component in kN/m") LL_minor: float = Field(..., description="Live load minor axis component in kN/m") WL_major: float = Field(..., description="Wind load major axis component in kN/m") WL_minor: float = Field(..., description="Wind load minor axis component in kN/m")
[docs] class MomentShearResults(BaseModel): """Moment and shear calculation results.""" M_DL_LL_major: float = Field(..., description="DL+LL major axis moment in kN-m") M_DL_LL_minor: float = Field(..., description="DL+LL minor axis moment in kN-m") M_DL_WL_major: float = Field(..., description="DL+WL major axis moment in kN-m") M_DL_WL_minor: float = Field(..., description="DL+WL minor axis moment in kN-m") V_DL_LL: float = Field(..., description="DL+LL shear force in kN") V_DL_WL: float = Field(..., description="DL+WL shear force in kN") max_moment: float = Field(..., description="Maximum moment in kN-m") max_shear: float = Field(..., description="Maximum shear in kN")
[docs] class StiffenerCheck(BaseModel): """Stiffener adequacy check results.""" stiffener_ok: bool = Field(..., description="Stiffener adequacy status") I_actual: float = Field(..., description="Actual stiffener moment of inertia in mm⁴") I_min: float = Field(..., description="Required minimum moment of inertia in mm⁴") d_min: float = Field(..., description="Minimum required lip depth in mm") lip_depth_ok: bool = Field(..., description="Lip depth adequacy status") web_depth_ok: bool = Field(..., description="Web depth-to-thickness ratio check") web_stiffened: bool = Field(..., description="Whether web is stiffened") is_code_reference: str = Field( default="IS 801:1975, Cl. 5.2.2", description="IS code reference for stiffener requirements", )
[docs] class BendingCheck(BaseModel): """Bending stress check results.""" bending_stress_major: float = Field(..., description="Major axis bending stress in N/mm²") bending_stress_minor: float = Field(..., description="Minor axis bending stress in N/mm²") allowable_bending_major: float = Field( ..., description="Allowable major axis bending stress in N/mm²" ) allowable_bending_minor: float = Field( ..., description="Allowable minor axis bending stress in N/mm²" ) combined_bending_ratio: float = Field(..., description="Combined bending stress ratio") bending_safe: bool = Field(..., description="Bending safety status") utilization_ratio: float = Field(..., description="Bending utilization ratio") spans: Dict[str, float] = Field(..., description="Effective spans used") is_code_reference: str = Field( default="IS 801:1975, Cl. 6.3", description="IS code reference for laterally unbraced beams", )
[docs] class WebCripplingCheck(BaseModel): """Web crippling check results.""" Pmax: float = Field(..., description="Web crippling capacity in kN") Pmax_end: float = Field(..., description="End reaction capacity in kN") Pmax_interior: float = Field(..., description="Interior support capacity in kN") controlling_case: str = Field(..., description="Controlling case") shear_force: float = Field(..., description="Applied shear force in kN") web_crippling_ok: bool = Field(..., description="Web crippling safety status") utilization_ratio: float = Field(..., description="Web crippling utilization ratio") is_code_reference: str = Field( default="IS 801:1975, Cl. 6.5", description="IS code reference for web crippling", )
[docs] class DeflectionCheck(BaseModel): """Deflection check results.""" actual_deflection: float = Field(..., description="Actual deflection in mm") permissible_deflection: float = Field(..., description="Permissible deflection in mm") deflection_safe: bool = Field(..., description="Deflection safety status") deflection_ratio: str = Field(..., description="Deflection ratio (span/deflection)") utilization_ratio: float = Field(..., description="Deflection utilization ratio") is_code_reference: str = Field( default="IS 801:1975, Cl. 8.2", description="IS code reference for deflection limits", )
[docs] class PurlinDesignOutput(BaseModel): """Output schema for purlin design results.""" # Input summary inputs: PurlinDesignInput = Field(..., description="Input parameters used") # Load calculations loads: LoadResults = Field(..., description="Load calculation results") # Moment and shear results moments_shears: MomentShearResults = Field(..., description="Moment and shear results") # Structural checks stiffener_check: StiffenerCheck = Field(..., description="Stiffener adequacy check") bending_check: BendingCheck = Field(..., description="Bending stress check") web_crippling_check: WebCripplingCheck = Field(..., description="Web crippling check") deflection_check_major: DeflectionCheck = Field(..., description="Major axis deflection check") deflection_check_minor: DeflectionCheck = Field(..., description="Minor axis deflection check") # Overall results design_safe: bool = Field(..., description="Overall design safety status") critical_check: str = Field(..., description="Most critical check") max_utilization_ratio: float = Field(..., description="Maximum utilization ratio") design_recommendations: list[str] = Field(..., description="Design recommendations") # IS code compliance code_compliance: Dict[str, Any] = Field(..., description="IS 801:1975 code compliance summary")
[docs] class PurlinDesignCalculator(BaseCalculator): """ Cold-formed steel purlin design calculator per IS 801:1975. Performs comprehensive analysis including: - Load resolution and combinations - Bending stress analysis with lateral buckling - Web crippling checks - Deflection analysis - Stiffener adequacy verification """ name = "Purlin Design"
[docs] def __init__(self) -> None: super().__init__("purlin_design", "1.0.0")
@property def input_schema(self) -> Type[BaseModel]: """Return input validation schema class.""" return PurlinDesignInput @property def output_schema(self) -> Type[BaseModel]: """Return output formatting schema class.""" return PurlinDesignOutput
[docs] def get_name(self) -> str: """Return calculator name.""" return "Purlin Design"
[docs] def get_description(self) -> str: """Return calculator description.""" return "Cold-formed steel purlin design as per IS 801:1975"
[docs] def calculate(self, inputs: Dict[str, Any]) -> Dict[str, Any]: """ Perform purlin design calculation. Args: inputs: Validated input parameters Returns: Dictionary containing calculation results """ try: logger.info("=" * 80) logger.info("[PURLIN] Starting purlin design calculation") logger.info(f"[PURLIN] Input keys: {list(inputs.keys())}") # Extract input parameters span = inputs["span"] spacing = inputs["spacing"] sag_rods = inputs["sag_rods"] inclination = inputs["inclination"] section = inputs["section"] # Convert SectionProperties model to dict for calculations if hasattr(section, "get_properties"): section = section.get_properties() elif isinstance(section, dict) and "profile_type" in section: # Handle direct dict calls with profile selection section = get_profile_properties(section["profile_name"], section["profile_type"]) # Validate section is a valid dictionary if not isinstance(section, dict) or section is None: raise CalculationError( "Invalid section properties - section must be a valid dictionary" ) material = inputs["material"] loads = inputs["loads"] logger.info( f"[PURLIN] Extracted params - span: {span}m, spacing: {spacing}m, sag_rods: {sag_rods}, inclination: {inclination}°" ) logger.info(f"[PURLIN] Section type: {type(section)}, Material: {material}") # Calculate loads and forces logger.info("[PURLIN] Calculating loads...") load_results = self._calculate_loads(loads, spacing, inclination) # Calculate moments and shears logger.info("[PURLIN] Calculating moments and shears...") moment_shear_results = self._calculate_moments_shears(load_results, span, sag_rods) logger.info( f"[PURLIN] Max moment: {moment_shear_results.get('max_moment')} kN-m, Max shear: {moment_shear_results.get('max_shear')} kN" ) # Perform structural checks with effective width considerations logger.info("[PURLIN] Performing structural checks...") effective_width_results = self._check_flat_width_ratios(section, material) stiffener_check = self._check_stiffeners(section, material, effective_width_results) bending_check = self._check_bending( section, material, moment_shear_results, span, sag_rods ) web_crippling_check = self._check_web_crippling(section, material, moment_shear_results) deflection_major = self._check_deflection_major(section, material, load_results, span) deflection_minor = self._check_deflection_minor( section, material, load_results, span, sag_rods ) # Determine overall safety and recommendations checks = [ stiffener_check, bending_check, web_crippling_check, deflection_major, deflection_minor, ] design_safe = all( [ stiffener_check["stiffener_ok"], bending_check["bending_safe"], web_crippling_check["web_crippling_ok"], deflection_major["deflection_safe"], deflection_minor["deflection_safe"], ] ) # Find critical check and max utilization utilization_ratios = { "stiffener": 1.0 if not stiffener_check["stiffener_ok"] else 0.9, "bending": ( min(bending_check["utilization_ratio"], 999.9) if not math.isinf(bending_check["utilization_ratio"]) else 999.9 ), "web_crippling": ( min(web_crippling_check["utilization_ratio"], 999.9) if not math.isinf(web_crippling_check["utilization_ratio"]) else 999.9 ), "deflection_major": ( min(deflection_major["utilization_ratio"], 999.9) if not math.isinf(deflection_major["utilization_ratio"]) else 999.9 ), "deflection_minor": ( min(deflection_minor["utilization_ratio"], 999.9) if not math.isinf(deflection_minor["utilization_ratio"]) else 999.9 ), } max_utilization = max(utilization_ratios.values()) critical_check = max(utilization_ratios.items(), key=lambda x: x[1])[0] # Generate recommendations recommendations = self._generate_recommendations( design_safe, critical_check, max_utilization, checks ) # Code compliance summary code_compliance = self._generate_code_compliance(checks) # Prepare outputs in standard format (like slab design and crack width) logger.info( f"[PURLIN] Design safe: {design_safe}, Critical check: {critical_check}, Max utilization: {max_utilization}" ) outputs = { "loads": load_results, "moments_shears": moment_shear_results, "stiffener_check": stiffener_check, "bending_check": bending_check, "web_crippling_check": web_crippling_check, "deflection_check_major": deflection_major, "deflection_check_minor": deflection_minor, "design_safe": design_safe, "critical_check": critical_check.replace("_", " ").title(), "max_utilization_ratio": round(max_utilization, 3), "design_recommendations": recommendations, "code_compliance": code_compliance, } result = { "calculator_type": "purlin_design", "inputs": inputs, "outputs": outputs, # Also include individual results for backward compatibility **outputs, } logger.info("[PURLIN] Calculation completed successfully") logger.info(f"[PURLIN] Result keys: {list(result.keys())}") logger.info("=" * 80) return result except Exception as e: logger.error(f"[PURLIN] Calculation failed with error: {e!s}") logger.exception(e) self.logger.error("Purlin design calculation failed", error=str(e)) raise CalculationError(f"Calculation failed: {e!s}")
def _calculate_loads( self, loads: Dict[str, Any], spacing: float, inclination: float ) -> Dict[str, Any]: """Calculate total loads and resolve into components.""" # Total loads per meter DL = (loads["roof"] * spacing) + loads["purlin"] LL = loads["live"] * spacing WL = loads["wind"] * spacing # Resolve into major/minor axis components theta_rad = inclination * DEGREES_TO_RADIANS cos_theta = math.cos(theta_rad) sin_theta = math.sin(theta_rad) return { "DL": DL, "LL": LL, "WL": WL, "DL_major": DL * cos_theta, "DL_minor": DL * sin_theta, "LL_major": LL * cos_theta, "LL_minor": LL * sin_theta, "WL_major": WL * cos_theta, "WL_minor": WL * sin_theta, } def _calculate_moments_shears( self, loads: Dict[str, Any], span: float, sag_rods: int ) -> Dict[str, Any]: """Calculate bending moments and shear forces.""" # Effective spans Lx = span # Major axis span Ly = span / (sag_rods + 1) if sag_rods > 0 else span # Minor axis span # Moments for load combinations (simply supported beam) M_DL_LL_major = ((loads["DL_major"] + loads["LL_major"]) * Lx**2) / 8 M_DL_LL_minor = ((loads["DL_minor"] + loads["LL_minor"]) * Ly**2) / 8 M_DL_WL_major = ((loads["DL_major"] + loads["WL_major"]) * Lx**2) / 8 M_DL_WL_minor = ((loads["DL_minor"] + loads["WL_minor"]) * Ly**2) / 8 # Shear forces (major axis controls) V_DL_LL = ((loads["DL_major"] + loads["LL_major"]) * Lx) / 2 V_DL_WL = ((loads["DL_major"] + loads["WL_major"]) * Lx) / 2 # Maximum values max_moment = max(abs(M_DL_LL_major), abs(M_DL_WL_major)) max_shear = max(abs(V_DL_LL), abs(V_DL_WL)) return { "M_DL_LL_major": M_DL_LL_major, "M_DL_LL_minor": M_DL_LL_minor, "M_DL_WL_major": M_DL_WL_major, "M_DL_WL_minor": M_DL_WL_minor, "V_DL_LL": V_DL_LL, "V_DL_WL": V_DL_WL, "max_moment": max_moment, "max_shear": max_shear, } def _check_stiffeners( self, section: Dict[str, Any], material: Dict[str, Any], effective_width_results: Optional[Dict[str, Any]] = None, ) -> Dict[str, Any]: """Check stiffener adequacy per IS 801:1975.""" t = section["thickness"] lip_depth = section.get("lip_depth", 0) lip_angle = section.get("lip_angle", 90) # Calculate actual moment of inertia of lip stiffener if lip_depth > 0 and t > 0: lip_angle_rad = lip_angle * DEGREES_TO_RADIANS I_actual = (t * lip_depth**3 * math.sin(lip_angle_rad) ** 2) / 12 else: I_actual = 0.0 # Calculate minimum required moment of inertia w = ( max(section["flange_width_tp"], section["flange_width_bt"]) - 2 * t - 2 * section.get("Rad", 0) ) Fy_kgf = material["Fy"] * 100 # Convert MPa to kgf/cm² (100 = CM2_TO_MM2) if t > 0 and w > 0 and Fy_kgf > 0: w_t_ratio = w / t term = w_t_ratio**2 - 281200 / Fy_kgf if term > 0: I_min = 1.83 * (t**4) * math.sqrt(term) else: I_min = MIN_STIFFENER_INERTIA * (t**4) else: I_min = MIN_STIFFENER_INERTIA * (t**4) I_min = max(I_min, MIN_STIFFENER_INERTIA * (t**4)) # Check minimum lip depth if t > 0 and w > 0 and Fy_kgf > 0: w_t_ratio = w / t term = w_t_ratio**2 - 281200 / Fy_kgf if term > 0: d_min = max(2.8 * t * (term**0.167), 4.8 * t) else: d_min = 4.8 * t else: d_min = 4.8 * t # Check web depth-to-thickness ratio h = section["depth"] - 2 * t h_t_ratio = h / t if t > 0 else 0 is_stiffened = lip_depth > 0 web_depth_ok = h_t_ratio <= (200 if is_stiffened else 150) return { "stiffener_ok": I_actual >= I_min, "I_actual": round(I_actual, 2), "I_min": round(I_min, 2), "d_min": round(d_min, 2), "lip_depth_ok": lip_depth >= d_min, "web_depth_ok": web_depth_ok, "web_stiffened": is_stiffened, "is_code_reference": "IS 801:1975, Cl. 5.2.2 - Stiffener Requirements", } def _check_bending( self, section: Dict[str, Any], material: Dict[str, Any], moments: Dict[str, Any], span: float, sag_rods: int, ) -> Dict[str, Any]: """Check bending stresses per IS 801:1975.""" Lx = span Ly = span / (sag_rods + 1) if sag_rods > 0 else span Sxc = section["Zxx_tp"] Iyc = section["Iyy"] * 0.5 # Half the moment of inertia for compression flange Fy = material["Fy"] E = material["E"] Zxx_bt = section["Zxx_bt"] d = section["depth"] Zyy = section["Zyy"] # Calculate allowable bending stress for laterally unbraced beam L2Sxc_dIyc = (Lx**2 * Sxc) / (d * Iyc) if (d * Iyc) > 0 else 0 condition1_limit = (0.18 * math.pi**2 * E) / Fy condition2_limit = (0.9 * math.pi**2 * E) / Fy Cb = DEFAULT_CB # Lateral torsional buckling coefficient if condition1_limit < L2Sxc_dIyc < condition2_limit: term = ((Fy**2) / (2.7 * math.pi**2 * E * Cb)) * L2Sxc_dIyc Fb = (2 / 3) * Fy - term elif L2Sxc_dIyc >= condition2_limit: Fb = 0.3 * math.pi**2 * E * Cb * (d * Iyc) / (Lx**2 * Sxc) if (Lx**2 * Sxc) > 0 else 0 else: Fb = 0.6 * Fy # Basic design stress for minor axis basic_design_stress = 0.6 * Fy # Calculate actual bending stresses sagging_moment = max(moments["M_DL_LL_major"], moments["M_DL_WL_major"], 0) hogging_moment = min(moments["M_DL_LL_major"], moments["M_DL_WL_major"], 0) bending_stress_sagging = (sagging_moment * 1e6) / Sxc if Sxc > 0 else 0 bending_stress_hogging = abs((hogging_moment * 1e6) / Zxx_bt) if Zxx_bt > 0 else 0 actual_bending_stress_major = max(bending_stress_sagging, bending_stress_hogging) minor_moment = max(abs(moments["M_DL_LL_minor"]), abs(moments["M_DL_WL_minor"])) actual_bending_stress_minor = (minor_moment * 1e6) / Zyy if Zyy > 0 else 0 # Combined bending check per IS 801:1975 combined_ratio = ( (actual_bending_stress_major / Fb) + (actual_bending_stress_minor / basic_design_stress) if (Fb > 0 and basic_design_stress > 0) else float("inf") ) bending_safe = combined_ratio <= 1.0 return { "bending_stress_major": round(actual_bending_stress_major, 2), "bending_stress_minor": round(actual_bending_stress_minor, 2), "allowable_bending_major": round(Fb, 2), "allowable_bending_minor": round(basic_design_stress, 2), "combined_bending_ratio": round(combined_ratio, 3), "bending_safe": bending_safe, "utilization_ratio": round(combined_ratio, 3), "spans": {"Lx": Lx, "Ly": Ly}, "is_code_reference": "IS 801:1975, Cl. 6.3 - Bending Stress", } def _check_web_crippling( self, section: Dict[str, Any], material: Dict[str, Any], moments: Dict[str, Any] ) -> Dict[str, Any]: """Check web crippling per IS 801:1975.""" t = section["thickness"] h = section["depth"] - 2 * t # Web height N = min(h, 25.0) # Bearing length, typically 25mm for purlin supports Fy_kgf = material["Fy"] * 100 # Convert to kgf/cm² (100 = CM2_TO_MM2) R = section.get("Rad", 0) # Convert to cm for formula t_cm = t / 10 h_cm = h / 10 N_cm = N / 10 R_cm = R / 10 # Validate inputs if t_cm <= 0 or h_cm <= 0: return self._create_default_web_crippling_result() # Calculate ratios with bounds N_t_ratio = min(N_cm / t_cm, 100) if t_cm > 0 else 0 h_t_ratio = min(h_cm / t_cm, 150) if t_cm > 0 else 0 Fy_ratio = min(max(Fy_kgf / 2320, 0.1), 2.0) if Fy_kgf > 0 else 0.1 # End reaction capacity with bounds checking term1 = 98 + 4.2 * N_t_ratio term2 = 0.022 * N_t_ratio * h_t_ratio term3 = 0.011 * h_t_ratio bracket_term = term1 - term2 - term3 # Ensure positive bracket term if bracket_term <= 0: bracket_term = 10 # Conservative minimum value factor_term = 1.33 - 0.33 * Fy_ratio # Ensure positive factor term if factor_term <= 0: factor_term = 0.5 # Conservative minimum value Pmax_end = 70 * t_cm**2 * bracket_term * factor_term * Fy_ratio # For extreme h_t_ratio > 150, use reduced capacity if h_t_ratio > 150: Pmax_end = 70 * t_cm**2 * 50 * Fy_ratio # Simplified conservative formula # Interior support capacity with bounds checking term1 = 305 + 2.3 * N_t_ratio term2 = 0.009 * N_t_ratio * h_t_ratio term3 = 0.5 * h_t_ratio bracket_term = term1 - term2 - term3 # Ensure positive bracket term if bracket_term <= 0: bracket_term = 50 # Conservative minimum value for interior factor_term = 1.22 - 0.22 * Fy_ratio # Ensure positive factor term if factor_term <= 0: factor_term = 0.5 # Conservative minimum value Pmax_interior = 70 * t_cm**2 * bracket_term * factor_term * Fy_ratio # For extreme h_t_ratio > 150, use reduced capacity if h_t_ratio > 150: Pmax_interior = 70 * t_cm**2 * 100 * Fy_ratio # Simplified conservative formula # Radius modification with bounds if R_cm > t_cm and R_cm > 0: radius_factor_end = max(1.15 - 0.15 * (R_cm / t_cm), 0.5) radius_factor_int = max(1.06 - 0.06 * (R_cm / t_cm), 0.5) Pmax_end *= radius_factor_end Pmax_interior *= radius_factor_int # Convert to kN and ensure positive values Pmax_end_kN = max(Pmax_end * KGF_TO_N / 1000, 0.1) Pmax_interior_kN = max(Pmax_interior * KGF_TO_N / 1000, 0.1) Pmax_kN = min(Pmax_end_kN, Pmax_interior_kN) controlling_case = "End Reaction" if Pmax_end_kN <= Pmax_interior_kN else "Interior Support" # Applied shear force shear_force = moments["max_shear"] web_crippling_ok = shear_force <= Pmax_kN utilization_ratio = shear_force / Pmax_kN if Pmax_kN > 0 else float("inf") return { "Pmax": round(Pmax_kN, 2), "Pmax_end": round(Pmax_end_kN, 2), "Pmax_interior": round(Pmax_interior_kN, 2), "controlling_case": controlling_case, "shear_force": round(shear_force, 2), "web_crippling_ok": web_crippling_ok, "utilization_ratio": round(utilization_ratio, 3), "is_code_reference": "IS 801:1975, Cl. 6.5 - Web Crippling", } def _create_default_web_crippling_result(self) -> Dict[str, Any]: """Create default web crippling result for invalid inputs.""" return { "Pmax": 0.1, "Pmax_end": 0.1, "Pmax_interior": 0.1, "controlling_case": "End Reaction", "shear_force": 0.0, "web_crippling_ok": False, "utilization_ratio": 999.9, "is_code_reference": "IS 801:1975, Cl. 6.5 - Web Crippling", } def _check_deflection_major( self, section: Dict[str, Any], material: Dict[str, Any], loads: Dict[str, Any], span: float, ) -> Dict[str, Any]: """Check major axis deflection.""" L = span * 1000 # Convert to mm W = loads["DL_major"] + max(loads["LL_major"], loads["WL_major"]) # N/mm I = section["Ixx"] # mm⁴ E = material["E"] # MPa = N/mm² # Deflection for simply supported beam with UDL actual_deflection = (5 * W * L**4) / (384 * E * I) if (E * I) > 0 else float("inf") permissible_deflection = L / 150 # L/150 per IS 801:1975 deflection_safe = actual_deflection <= permissible_deflection deflection_ratio = f"L/{int(L / actual_deflection)}" if actual_deflection > 0 else "L/∞" utilization_ratio = ( actual_deflection / permissible_deflection if permissible_deflection > 0 else float("inf") ) return { "actual_deflection": round(actual_deflection, 2), "permissible_deflection": round(permissible_deflection, 2), "deflection_safe": deflection_safe, "deflection_ratio": deflection_ratio, "utilization_ratio": round(utilization_ratio, 3), "is_code_reference": "IS 801:1975, Cl. 8.2 - Deflection Limits", } def _check_deflection_minor( self, section: Dict[str, Any], material: Dict[str, Any], loads: Dict[str, Any], span: float, sag_rods: int, ) -> Dict[str, Any]: """Check minor axis deflection.""" Ly = span / (sag_rods + 1) if sag_rods > 0 else span L = Ly * 1000 # Convert to mm W = loads["DL_minor"] + max(loads["LL_minor"], loads["WL_minor"]) # N/mm I = section["Iyy"] # mm⁴ E = material["E"] # MPa = N/mm² # Deflection for simply supported beam with UDL actual_deflection = (5 * W * L**4) / (384 * E * I) if (E * I) > 0 else float("inf") permissible_deflection = L / 150 # L/150 per IS 801:1975 deflection_safe = actual_deflection <= permissible_deflection deflection_ratio = f"L/{int(L / actual_deflection)}" if actual_deflection > 0 else "L/∞" utilization_ratio = ( actual_deflection / permissible_deflection if permissible_deflection > 0 else float("inf") ) return { "actual_deflection": round(actual_deflection, 2), "permissible_deflection": round(permissible_deflection, 2), "deflection_safe": deflection_safe, "deflection_ratio": deflection_ratio, "utilization_ratio": round(utilization_ratio, 3), "is_code_reference": "IS 801:1975, Cl. 8.2 - Deflection Limits", } def _generate_recommendations( self, design_safe: bool, critical_check: str, max_utilization: float, checks: List[Any], ) -> list[str]: """Generate design recommendations.""" recommendations = [] if not design_safe: recommendations.append( f"❌ Design FAILS - Critical check: {critical_check.replace('_', ' ').title()}" ) # Specific recommendations based on critical check if critical_check == "stiffener": recommendations.append( "• Increase lip depth or consider different stiffener configuration" ) recommendations.append("• Check flange width-to-thickness ratios") elif critical_check == "bending": recommendations.append("• Increase section depth or select stronger section") recommendations.append( "• Consider additional lateral supports to reduce effective length" ) elif critical_check == "web_crippling": recommendations.append("• Increase section thickness or provide bearing stiffeners") recommendations.append("• Consider load distribution over greater length") elif "deflection" in critical_check: recommendations.append("• Increase moment of inertia or select deeper section") recommendations.append("• Consider reducing span or adding intermediate supports") else: recommendations.append( f"✅ Design is SAFE - Maximum utilization: {max_utilization:.1%}" ) if max_utilization > 0.8: recommendations.append( "• High utilization - consider optimization for better economy" ) elif max_utilization < 0.5: recommendations.append("• Conservative design - smaller section may be adequate") # Add general recommendations recommendations.append("• Verify connection details and end restraints") recommendations.append("• Consider constructability and deflection under service loads") return recommendations def _generate_code_compliance(self, checks: List[Any]) -> Dict[str, Any]: """Generate IS 801:1975 code compliance summary.""" return { "applicable_standard": "IS 801:1975 - Code of Practice for Use of Cold-formed Light Gauge Steel Structural Members in General Building Construction", "design_method": "Working Stress Design Method", "load_factors": { "dead_load": 1.0, "live_load": 1.0, "wind_load": 1.0, "note": "Working stress method - no load factors applied", }, "safety_factors": { "material_strength": "Built into allowable stresses", "deflection_limit": "L/150 for general construction", }, "verification_status": { "stiffener_requirements": "Cl. 6.2 - Checked", "bending_stress": "Cl. 5.1 - Checked", "web_crippling": "Cl. 7.3 - Checked", "deflection_limits": "Cl. 8.2 - Checked", }, } def _calculate_effective_width( self, w: float, t: float, Fy: float, calculation_type: str = "load" ) -> tuple[float, bool]: """ Calculate effective design width per IS 801:1975 Cl. 5.2.1.1. Args: w: Flat width in mm t: Thickness in mm Fy: Yield strength in MPa calculation_type: "load" or "deflection" Returns: Tuple of (effective_width, is_fully_effective) """ if t <= 0 or Fy <= 0: return w, True # Convert Fy to kgf/cm² for IS 801 formulas Fy_kgf = Fy * KGF_CM2_TO_N_MM2 sqrt_F = math.sqrt(Fy_kgf) w_t_ratio = w / t if calculation_type == "load": # For load determination w_t_limit = 1435 / sqrt_F if w_t_ratio <= w_t_limit: return w, True # Fully effective else: # Apply reduction formula term = 1 - (465 / (w_t_ratio * sqrt_F)) if term <= 0: return 0.0, False b_t = (2120 / sqrt_F) * math.sqrt(term) b_effective = b_t * t return min(b_effective, w), False elif calculation_type == "deflection": # For deflection determination w_t_limit = 1850 / sqrt_F if w_t_ratio <= w_t_limit: return w, True # Fully effective else: # Apply reduction formula term = 1 - (600 / (w_t_ratio * sqrt_F)) if term <= 0: return 0.0, False b_t = (2710 / sqrt_F) * math.sqrt(term) b_effective = b_t * t return min(b_effective, w), False return w, True def _check_flat_width_ratios( self, section: Dict[str, Any], material: Dict[str, Any] ) -> Dict[str, Any]: """ Check flat-width ratios and calculate effective widths per IS 801:1975. Args: section: Section properties dictionary material: Material properties dictionary Returns: Dictionary with effective width results """ t = section["thickness"] Fy = material["Fy"] # Calculate flat widths (excluding fillets and lips) flange_width_top = section["flange_width_tp"] - 2 * section.get("Rad", 0) flange_width_bottom = section["flange_width_bt"] - 2 * section.get("Rad", 0) web_height = section["depth"] - 2 * t # Check effective widths for load calculations b_eff_top_load, top_fully_eff_load = self._calculate_effective_width( flange_width_top, t, Fy, "load" ) b_eff_bottom_load, bottom_fully_eff_load = self._calculate_effective_width( flange_width_bottom, t, Fy, "load" ) # Check effective widths for deflection calculations b_eff_top_defl, top_fully_eff_defl = self._calculate_effective_width( flange_width_top, t, Fy, "deflection" ) b_eff_bottom_defl, bottom_fully_eff_defl = self._calculate_effective_width( flange_width_bottom, t, Fy, "deflection" ) # Web is considered stiffened (between flanges) web_w_t_ratio = web_height / t if t > 0 else 0 return { "flange_top": { "flat_width": flange_width_top, "w_t_ratio": flange_width_top / t if t > 0 else 0, "effective_width_load": b_eff_top_load, "effective_width_deflection": b_eff_top_defl, "fully_effective_load": top_fully_eff_load, "fully_effective_deflection": top_fully_eff_defl, }, "flange_bottom": { "flat_width": flange_width_bottom, "w_t_ratio": flange_width_bottom / t if t > 0 else 0, "effective_width_load": b_eff_bottom_load, "effective_width_deflection": b_eff_bottom_defl, "fully_effective_load": bottom_fully_eff_load, "fully_effective_deflection": bottom_fully_eff_defl, }, "web": { "flat_width": web_height, "w_t_ratio": web_w_t_ratio, "stiffened": True, # Web is stiffened between flanges "effective_width": web_height, # Web assumed fully effective for stiffened case }, } def _get_available_profiles(self, profile_type: Optional[str] = None) -> Dict[str, List[Any]]: """ Get list of available profiles, optionally filtered by type. Args: profile_type: Optional filter by profile type ('Z45', 'Z90', 'C') Returns: Dictionary with profile types as keys and lists of profile names as values """ available = {} if profile_type is None: # Return all types for ptype in ALL_PROFILES.keys(): available[ptype] = [profile.name for profile in ALL_PROFILES[ptype]] else: # Return specific type if profile_type in ALL_PROFILES: available[profile_type] = [profile.name for profile in ALL_PROFILES[profile_type]] return available