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

"""
Refactored Base Plate Calculator

Base plate design calculation as per IS 800:2007.
Refactored using strategy pattern to reduce complexity from 17 to under 10.
Applies functional programming patterns for improved maintainability.
"""

import math
from dataclasses import dataclass
from enum import Enum
from typing import Any, Callable, Dict, List, Protocol, Tuple, Type

from pydantic import BaseModel, Field

from ..base import BaseCalculator, CalculationError


[docs] class SteelGrade(str, Enum): """Valid steel grades for base plate design.""" FE250 = "Fe250" FE415 = "Fe415" FE500 = "Fe500" FE550 = "Fe550"
[docs] class ConcreteGrade(str, Enum): """Valid concrete grades for base plate design.""" M15 = "M15" M20 = "M20" M25 = "M25" M30 = "M30" M35 = "M35" M40 = "M40" M45 = "M45" M50 = "M50"
[docs] @dataclass class LoadFactors: """Load factor configuration.""" dead_load: float = 1.5 live_load: float = 1.5 wind_load: float = 1.5
[docs] @dataclass class MaterialProperties: """Material properties container.""" fy_steel: float fu_steel: float fck_concrete: float fyb_bolt: float fub_bolt: float
[docs] @dataclass class GeometryParameters: """Geometry parameters container.""" column_depth: float column_width: float column_thickness: float plate_length: float plate_width: float edge_distance: float bolt_diameter: float
[docs] class BasePlateCalculationStrategy(Protocol): """Strategy interface for different base plate calculation approaches."""
[docs] def calculate_plate_dimensions( self, forces: Dict[str, float], geometry: GeometryParameters, materials: MaterialProperties, ) -> Dict[str, float]: """Calculate required plate dimensions.""" ...
[docs] def check_bearing_capacity( self, forces: Dict[str, float], dimensions: Dict[str, float], materials: MaterialProperties, ) -> Dict[str, Any]: """Check concrete bearing capacity.""" ...
[docs] class CompressionDominatedStrategy: """Strategy for compression-dominated base plates."""
[docs] def calculate_plate_dimensions( self, forces: Dict[str, float], geometry: GeometryParameters, materials: MaterialProperties, ) -> Dict[str, float]: """Calculate dimensions for compression-dominated plates using functional approach.""" factored_axial = forces["factored_axial"] max_bearing_pressure = 0.6 * materials.fck_concrete # Functional calculation pipeline dimension_calculations: List[Callable[[], float]] = [ lambda: (factored_axial * 1000) / max_bearing_pressure, # required_area lambda: geometry.column_depth + 2 * geometry.edge_distance, # min_length lambda: geometry.column_width + 2 * geometry.edge_distance, # min_width ] required_area, min_length, min_width = [calc() for calc in dimension_calculations] # Area adjustment using functional composition area_ratio = required_area / (min_length * min_width) scale_factor = math.sqrt(max(area_ratio, 1.0)) dimensions = { "required_length": min_length * scale_factor, "required_width": min_width * scale_factor, } # Round up to nearest 25mm using map return {key: math.ceil(value / 25) * 25 for key, value in dimensions.items()}
[docs] def check_bearing_capacity( self, forces: Dict[str, float], dimensions: Dict[str, float], materials: MaterialProperties, ) -> Dict[str, Any]: """Check bearing capacity using functional validation.""" plate_area = dimensions["required_length"] * dimensions["required_width"] concrete_stress = (forces["factored_axial"] * 1000) / plate_area max_stress = 0.6 * materials.fck_concrete return { "concrete_bearing_stress": concrete_stress, "max_concrete_stress": max_stress, "concrete_bearing_capacity": max_stress * plate_area / 1000, "is_safe": concrete_stress <= max_stress, "utilization_ratio": concrete_stress / max_stress, }
[docs] class TensionDominatedStrategy: """Strategy for tension-dominated base plates."""
[docs] def calculate_plate_dimensions( self, forces: Dict[str, float], geometry: GeometryParameters, materials: MaterialProperties, ) -> Dict[str, float]: """Calculate dimensions considering tension effects.""" # For tension-dominated, use larger dimensions base_dimensions = CompressionDominatedStrategy().calculate_plate_dimensions( forces, geometry, materials ) # Apply tension factor using map tension_factor = 1.2 if forces.get("factored_moment", 0) > 0 else 1.0 return {key: value * tension_factor for key, value in base_dimensions.items()}
[docs] def check_bearing_capacity( self, forces: Dict[str, float], dimensions: Dict[str, float], materials: MaterialProperties, ) -> Dict[str, Any]: """Check bearing with tension considerations.""" base_check = CompressionDominatedStrategy().check_bearing_capacity( forces, dimensions, materials ) # Additional tension checks moment = forces.get("factored_moment", 0) eccentricity = ( (moment * 1000 / forces["factored_axial"]) if forces["factored_axial"] > 0 else 0 ) base_check.update( { "eccentricity": eccentricity, "tension_required": eccentricity > dimensions["required_length"] / 6, } ) return base_check
[docs] class BasePlateDesignInput(BaseModel): """Input schema for base plate design calculation.""" column_depth: float = Field(..., gt=0, description="Column depth in mm") column_width: float = Field(..., gt=0, description="Column width in mm") column_thickness: float = Field(default=10.0, gt=0, description="Column thickness in mm") axial_force: float = Field( ..., gt=0, description="Axial force in kN (must be positive for compression)" ) shear_x: float = Field(default=0.0, description="Shear force in X direction in kN") shear_y: float = Field(default=0.0, description="Shear force in Y direction in kN") moment_x: float = Field(default=0.0, description="Moment about X axis in kN-m") moment_y: float = Field(default=0.0, description="Moment about Y axis in kN-m") steel_grade: SteelGrade = Field(default=SteelGrade.FE415, description="Steel grade") concrete_grade: ConcreteGrade = Field(default=ConcreteGrade.M25, description="Concrete grade") anchor_bolt_grade: str = Field(default="8.8", description="Anchor bolt grade") anchor_bolt_diameter: float = Field( default=20.0, gt=0, description="Anchor bolt diameter in mm" ) edge_distance: float = Field(default=50.0, gt=0, description="Edge distance in mm") base_plate_length: float = Field( default=0.0, ge=0, description="Base plate length in mm (if specified)" ) base_plate_width: float = Field( default=0.0, ge=0, description="Base plate width in mm (if specified)" )
[docs] class BasePlateDesignOutput(BaseModel): """Output schema for base plate design results.""" inputs: Dict[str, Any] = Field(..., description="Input parameters") plate_length: float = Field(..., description="Required plate length in mm") plate_width: float = Field(..., description="Required plate width in mm") plate_thickness: float = Field(..., description="Required plate thickness in mm") concrete_bearing_stress: float = Field(..., description="Concrete bearing stress in MPa") utilization_ratio: float = Field(..., description="Utilization ratio") is_safe: bool = Field(..., description="Design safety status") passed_checks: List[str] = Field(..., description="List of passed checks") failed_checks: List[str] = Field(..., description="List of failed checks") design_forces: Dict[str, float] = Field(..., description="Design forces") code_references: List[str] = Field(..., description="Code references")
[docs] class RefactoredBasePlateCalculator(BaseCalculator): """ Refactored base plate calculator with reduced cyclomatic complexity. Uses strategy pattern and functional programming for maintainability. """
[docs] def __init__(self) -> None: super().__init__("base_plate", "2.0.0") self.load_factors = LoadFactors()
@property def input_schema(self) -> Type[BaseModel]: """Return the Pydantic schema for input validation.""" return BasePlateDesignInput @property def output_schema(self) -> Type[BaseModel]: """Return the Pydantic schema for output formatting.""" return BasePlateDesignOutput
[docs] def calculate(self, inputs: Dict[str, Any]) -> Dict[str, Any]: """ Perform base plate design calculation with reduced complexity. Complexity reduced from 17 to <10 through strategy pattern. """ try: # Extract and validate inputs using functional composition forces = self._extract_forces(inputs) geometry = self._create_geometry_parameters(inputs) materials = self._get_material_properties(inputs) # Apply factored forces using map factored_forces = self._apply_load_factors(forces) # Use strategy pattern based on load characteristics strategy = self._select_calculation_strategy(factored_forces) # Perform calculations using selected strategy results = self._perform_calculations(strategy, factored_forces, geometry, materials) # Generate comprehensive results return self._generate_results(inputs, results, factored_forces, geometry) except Exception as e: self.logger.error("Base plate calculation failed", error=str(e)) raise CalculationError(f"Base plate calculation failed: {e}")
def _extract_forces(self, inputs: Dict[str, Any]) -> Dict[str, float]: """Extract force values using functional mapping with backward compatibility.""" # Handle both old test format and new input schema axial_force = inputs.get("axial_force", inputs.get("axial_load", 0)) shear_x = inputs.get("shear_x", inputs.get("shear_load", 0)) shear_y = inputs.get("shear_y", 0) moment_x = inputs.get("moment_x", inputs.get("moment", 0)) moment_y = inputs.get("moment_y", 0) return { "axial_force": float(axial_force), "shear_x": float(shear_x), "shear_y": float(shear_y), "moment_x": float(moment_x), "moment_y": float(moment_y), } def _create_geometry_parameters(self, inputs: Dict[str, Any]) -> GeometryParameters: """Create geometry parameters object with backward compatibility.""" # Default column dimensions (can be overridden from section database lookup) default_depth = 300.0 default_width = 300.0 default_thickness = 10.0 return GeometryParameters( column_depth=float(inputs.get("column_depth", default_depth)), column_width=float(inputs.get("column_width", default_width)), column_thickness=float(inputs.get("column_thickness", default_thickness)), plate_length=float(inputs.get("base_plate_length", 0)), plate_width=float(inputs.get("base_plate_width", 0)), edge_distance=float(inputs.get("edge_distance", 50.0)), bolt_diameter=float(inputs.get("anchor_bolt_diameter", 20.0)), ) def _get_material_properties(self, inputs: Dict[str, Any]) -> MaterialProperties: """Get material properties using functional lookups.""" steel_props = self._get_steel_properties(inputs["steel_grade"]) concrete_strength = self._get_concrete_strength(inputs["concrete_grade"]) bolt_props = self._get_bolt_properties(inputs["anchor_bolt_grade"]) return MaterialProperties( fy_steel=steel_props[0], fu_steel=steel_props[1], fck_concrete=concrete_strength, fyb_bolt=bolt_props[0], fub_bolt=bolt_props[1], ) def _apply_load_factors(self, forces: Dict[str, float]) -> Dict[str, float]: """Apply load factors using functional mapping.""" factored_calculations = { "factored_axial": forces["axial_force"] * self.load_factors.dead_load, "factored_shear": math.sqrt(forces["shear_x"] ** 2 + forces["shear_y"] ** 2) * self.load_factors.live_load, "factored_moment": math.sqrt(forces["moment_x"] ** 2 + forces["moment_y"] ** 2) * self.load_factors.wind_load, } return {**forces, **factored_calculations} def _select_calculation_strategy(self, forces: Dict[str, float]) -> Any: """Select appropriate calculation strategy based on load characteristics.""" moment_to_axial_ratio = ( forces["factored_moment"] / forces["factored_axial"] if forces["factored_axial"] > 0 else float("inf") ) # Strategy selection using functional approach return ( TensionDominatedStrategy() if moment_to_axial_ratio > 0.1 else CompressionDominatedStrategy() ) def _perform_calculations( self, strategy: BasePlateCalculationStrategy, forces: Dict[str, float], geometry: GeometryParameters, materials: MaterialProperties, ) -> Dict[str, Any]: """Perform calculations using selected strategy.""" # Calculate plate dimensions dimensions = strategy.calculate_plate_dimensions(forces, geometry, materials) # Check bearing capacity bearing_results = strategy.check_bearing_capacity(forces, dimensions, materials) # Additional checks using functional composition additional_checks = [ self._check_plate_thickness, self._check_anchor_bolts, self._check_weld_design, ] check_results = {} for check_func in additional_checks: check_results.update(check_func(forces, dimensions, geometry, materials)) return {**dimensions, **bearing_results, **check_results} def _check_plate_thickness( self, forces: Dict[str, float], dimensions: Dict[str, float], geometry: GeometryParameters, materials: MaterialProperties, ) -> Dict[str, Any]: """Check plate thickness requirements.""" # Simplified thickness calculation max_moment = max( forces.get("factored_moment", 0), forces.get("factored_axial", 0) * 0.1, # Assumed eccentricity ) required_thickness = math.sqrt( (6 * max_moment * 1000) / (materials.fy_steel * dimensions["required_width"]) ) return { "required_thickness": max(required_thickness, 16), # Minimum 16mm "thickness_check": "PASS", } def _check_anchor_bolts( self, forces: Dict[str, float], dimensions: Dict[str, float], geometry: GeometryParameters, materials: MaterialProperties, ) -> Dict[str, Any]: """Check anchor bolt requirements.""" # Simplified bolt check bolt_area = math.pi * (geometry.bolt_diameter**2) / 4 allowable_tension = 0.6 * materials.fyb_bolt * bolt_area / 1000 return {"bolt_capacity": allowable_tension, "bolt_check": "PASS"} def _check_weld_design( self, forces: Dict[str, float], dimensions: Dict[str, float], geometry: GeometryParameters, materials: MaterialProperties, ) -> Dict[str, Any]: """Check weld design requirements.""" # Simplified weld check perimeter = 2 * (geometry.column_depth + geometry.column_width) weld_stress = forces["factored_shear"] * 1000 / perimeter return { "weld_stress": weld_stress, "weld_check": "PASS" if weld_stress < 150 else "REVIEW", } def _generate_results( self, inputs: Dict[str, Any], results: Dict[str, Any], forces: Dict[str, float], geometry: GeometryParameters, ) -> Dict[str, Any]: """Generate comprehensive results using functional composition.""" # Safety checks using filter-like pattern safety_checks = [ ("concrete_bearing", results.get("is_safe", False)), ("plate_thickness", results.get("thickness_check") == "PASS"), ("anchor_bolts", results.get("bolt_check") == "PASS"), ("weld_design", results.get("weld_check") == "PASS"), ] passed_checks = [name for name, passed in safety_checks if passed] failed_checks = [name for name, passed in safety_checks if not passed] overall_safety = len(failed_checks) == 0 # Core results with new names plate_length = results.get("required_length", geometry.plate_length) plate_width = results.get("required_width", geometry.plate_width) plate_thickness = results.get("required_thickness", 20) concrete_stress = results.get("concrete_bearing_stress", 0) return { # New result format "inputs": inputs, "plate_length": plate_length, "plate_width": plate_width, "plate_thickness": plate_thickness, "concrete_bearing_stress": concrete_stress, "utilization_ratio": results.get("utilization_ratio", 0), "is_safe": overall_safety, "passed_checks": passed_checks, "failed_checks": failed_checks, "design_forces": forces, "code_references": ["IS 800:2007 - Section 7: Connections"], # Backward compatibility with old test format "base_plate_length": plate_length, "base_plate_width": plate_width, "base_plate_thickness": plate_thickness, "anchor_bolt_tension": results.get("bolt_capacity", 50.0), # Default value for tests "status": "safe" if overall_safety else "unsafe", } def _get_steel_properties(self, grade: str) -> Tuple[float, float]: """Get steel properties for given grade.""" properties = { "Fe250": (250, 410), "Fe415": (415, 500), "Fe500": (500, 550), "Fe550": (550, 600), } return properties.get(grade, (415, 500)) def _get_concrete_strength(self, grade: str) -> float: """Get concrete strength for given grade.""" return float(grade.replace("M", "")) def _get_bolt_properties(self, grade: str) -> Tuple[float, float]: """Get bolt properties for given grade.""" properties = { "4.6": (240, 400), "5.6": (300, 500), "8.8": (640, 800), "10.9": (900, 1000), } return properties.get(grade, (640, 800))