450 lines
15 KiB
Python
450 lines
15 KiB
Python
"""Layer 4 — Physics Closure & Simulation Authority.
|
||
|
||
Responsible for:
|
||
- Governing equation selection (structural, thermal, fluid)
|
||
- Boundary condition enforcement
|
||
- Safety factor calculation and validation
|
||
- Failure mode completeness analysis
|
||
- Simulation binding (simulations are binding, not illustrative)
|
||
"""
|
||
|
||
import hashlib
|
||
import math
|
||
import uuid
|
||
from abc import ABC, abstractmethod
|
||
from dataclasses import dataclass
|
||
from enum import Enum
|
||
from typing import Any
|
||
|
||
from pydantic import BaseModel, Field
|
||
|
||
from fusionagi._logger import logger
|
||
|
||
|
||
class PhysicsUnderdefinedError(Exception):
|
||
"""Failure state: physics not fully defined."""
|
||
|
||
def __init__(self, message: str, missing_data: list[str] | None = None):
|
||
self.missing_data = missing_data or []
|
||
super().__init__(message)
|
||
|
||
|
||
class ProofResult(str, Enum):
|
||
"""Result of physics validation."""
|
||
|
||
PROOF = "proof"
|
||
PHYSICS_UNDEFINED = "physics_underdefined"
|
||
VALIDATION_FAILED = "validation_failed"
|
||
|
||
|
||
class PhysicsProof(BaseModel):
|
||
"""Binding simulation proof reference."""
|
||
|
||
proof_id: str = Field(...)
|
||
governing_equations: str | None = Field(default=None)
|
||
boundary_conditions_ref: str | None = Field(default=None)
|
||
safety_factor: float | None = Field(default=None)
|
||
failure_modes_covered: list[str] = Field(default_factory=list)
|
||
metadata: dict[str, Any] = Field(default_factory=dict)
|
||
validation_status: str = Field(default="validated")
|
||
warnings: list[str] = Field(default_factory=list)
|
||
|
||
|
||
class PhysicsAuthorityInterface(ABC):
|
||
"""
|
||
Abstract interface for physics validation.
|
||
|
||
Governing equation selection, boundary condition enforcement, safety factor declaration,
|
||
failure-mode completeness. Simulations are binding, not illustrative.
|
||
"""
|
||
|
||
@abstractmethod
|
||
def validate_physics(
|
||
self,
|
||
design_ref: str,
|
||
load_cases: list[dict[str, Any]] | None = None,
|
||
**kwargs: Any,
|
||
) -> PhysicsProof | None:
|
||
"""
|
||
Validate physics for design; return Proof or None (PhysicsUnderdefined).
|
||
Raises PhysicsUnderdefinedError if required data missing.
|
||
"""
|
||
...
|
||
|
||
|
||
# Common material properties database (simplified)
|
||
MATERIAL_PROPERTIES: dict[str, dict[str, float]] = {
|
||
"aluminum_6061": {
|
||
"yield_strength_mpa": 276,
|
||
"ultimate_strength_mpa": 310,
|
||
"elastic_modulus_gpa": 68.9,
|
||
"density_kg_m3": 2700,
|
||
"poisson_ratio": 0.33,
|
||
"thermal_expansion_per_c": 23.6e-6,
|
||
"max_service_temp_c": 150,
|
||
},
|
||
"steel_4140": {
|
||
"yield_strength_mpa": 655,
|
||
"ultimate_strength_mpa": 1020,
|
||
"elastic_modulus_gpa": 205,
|
||
"density_kg_m3": 7850,
|
||
"poisson_ratio": 0.29,
|
||
"thermal_expansion_per_c": 12.3e-6,
|
||
"max_service_temp_c": 400,
|
||
},
|
||
"titanium_ti6al4v": {
|
||
"yield_strength_mpa": 880,
|
||
"ultimate_strength_mpa": 950,
|
||
"elastic_modulus_gpa": 113.8,
|
||
"density_kg_m3": 4430,
|
||
"poisson_ratio": 0.34,
|
||
"thermal_expansion_per_c": 8.6e-6,
|
||
"max_service_temp_c": 350,
|
||
},
|
||
"pla_plastic": {
|
||
"yield_strength_mpa": 60,
|
||
"ultimate_strength_mpa": 65,
|
||
"elastic_modulus_gpa": 3.5,
|
||
"density_kg_m3": 1240,
|
||
"poisson_ratio": 0.36,
|
||
"thermal_expansion_per_c": 68e-6,
|
||
"max_service_temp_c": 55,
|
||
},
|
||
"abs_plastic": {
|
||
"yield_strength_mpa": 40,
|
||
"ultimate_strength_mpa": 44,
|
||
"elastic_modulus_gpa": 2.3,
|
||
"density_kg_m3": 1050,
|
||
"poisson_ratio": 0.35,
|
||
"thermal_expansion_per_c": 90e-6,
|
||
"max_service_temp_c": 85,
|
||
},
|
||
}
|
||
|
||
# Standard failure modes to check
|
||
STANDARD_FAILURE_MODES = [
|
||
"yield_failure",
|
||
"ultimate_failure",
|
||
"buckling",
|
||
"fatigue",
|
||
"creep",
|
||
"thermal_distortion",
|
||
"vibration_resonance",
|
||
]
|
||
|
||
|
||
@dataclass
|
||
class LoadCaseResult:
|
||
"""Result of validating a single load case."""
|
||
|
||
load_case_id: str
|
||
max_stress_mpa: float
|
||
safety_factor: float
|
||
passed: bool
|
||
failure_mode: str | None = None
|
||
details: dict[str, Any] | None = None
|
||
|
||
|
||
class PhysicsAuthority(PhysicsAuthorityInterface):
|
||
"""
|
||
Physics validation authority with actual validation logic.
|
||
|
||
Features:
|
||
- Material property validation
|
||
- Load case analysis
|
||
- Safety factor calculation
|
||
- Failure mode coverage analysis
|
||
- Governing equation selection based on load types
|
||
"""
|
||
|
||
def __init__(
|
||
self,
|
||
required_safety_factor: float = 2.0,
|
||
material_db: dict[str, dict[str, float]] | None = None,
|
||
custom_failure_modes: list[str] | None = None,
|
||
):
|
||
"""
|
||
Initialize the PhysicsAuthority.
|
||
|
||
Args:
|
||
required_safety_factor: Minimum required safety factor (default 2.0).
|
||
material_db: Custom material properties database.
|
||
custom_failure_modes: Additional failure modes to check.
|
||
"""
|
||
self._required_sf = required_safety_factor
|
||
self._materials = material_db or MATERIAL_PROPERTIES
|
||
self._failure_modes = list(STANDARD_FAILURE_MODES)
|
||
if custom_failure_modes:
|
||
self._failure_modes.extend(custom_failure_modes)
|
||
|
||
def validate_physics(
|
||
self,
|
||
design_ref: str,
|
||
load_cases: list[dict[str, Any]] | None = None,
|
||
material: str | None = None,
|
||
dimensions: dict[str, float] | None = None,
|
||
boundary_conditions: dict[str, Any] | None = None,
|
||
**kwargs: Any,
|
||
) -> PhysicsProof | None:
|
||
"""
|
||
Validate physics for a design.
|
||
|
||
Args:
|
||
design_ref: Reference to the design being validated.
|
||
load_cases: List of load cases to validate against.
|
||
material: Material identifier (must be in material database).
|
||
dimensions: Key dimensions for stress calculation.
|
||
boundary_conditions: Boundary condition specification.
|
||
**kwargs: Additional parameters.
|
||
|
||
Returns:
|
||
PhysicsProof if validation passes, None if physics underdefined.
|
||
|
||
Raises:
|
||
PhysicsUnderdefinedError: If critical data is missing.
|
||
"""
|
||
missing_data = []
|
||
|
||
if not design_ref:
|
||
missing_data.append("design_ref")
|
||
if not material:
|
||
missing_data.append("material")
|
||
if not load_cases:
|
||
missing_data.append("load_cases")
|
||
|
||
if missing_data:
|
||
raise PhysicsUnderdefinedError(
|
||
f"Physics validation requires: {', '.join(missing_data)}",
|
||
missing_data=missing_data,
|
||
)
|
||
|
||
# Get material properties
|
||
mat_props = self._materials.get(material.lower().replace(" ", "_"))
|
||
if not mat_props:
|
||
raise PhysicsUnderdefinedError(
|
||
f"Unknown material: {material}. Available: {list(self._materials.keys())}",
|
||
missing_data=["material_properties"],
|
||
)
|
||
|
||
# Validate each load case
|
||
load_case_results: list[LoadCaseResult] = []
|
||
min_safety_factor = float("inf")
|
||
warnings: list[str] = []
|
||
failure_modes_covered: list[str] = []
|
||
|
||
for lc in load_cases:
|
||
result = self._validate_load_case(lc, mat_props, dimensions)
|
||
load_case_results.append(result)
|
||
|
||
if result.safety_factor < min_safety_factor:
|
||
min_safety_factor = result.safety_factor
|
||
|
||
if not result.passed:
|
||
warnings.append(
|
||
f"Load case '{result.load_case_id}' failed: {result.failure_mode}"
|
||
)
|
||
|
||
# Track failure modes analyzed
|
||
if result.failure_mode and result.failure_mode not in failure_modes_covered:
|
||
failure_modes_covered.append(result.failure_mode)
|
||
|
||
# Determine governing equations based on load types
|
||
governing_equations = self._select_governing_equations(load_cases)
|
||
|
||
# Check minimum required failure modes
|
||
required_modes = ["yield_failure", "ultimate_failure"]
|
||
for mode in required_modes:
|
||
if mode not in failure_modes_covered:
|
||
failure_modes_covered.append(mode) # Basic checks are always done
|
||
|
||
# Generate proof ID based on inputs
|
||
proof_hash = hashlib.sha256(
|
||
f"{design_ref}:{material}:{load_cases}".encode()
|
||
).hexdigest()[:16]
|
||
proof_id = f"proof_{design_ref}_{proof_hash}"
|
||
|
||
# Determine validation status
|
||
validation_status = "validated"
|
||
if min_safety_factor < self._required_sf:
|
||
validation_status = "insufficient_safety_factor"
|
||
warnings.append(
|
||
f"Safety factor {min_safety_factor:.2f} < required {self._required_sf}"
|
||
)
|
||
|
||
if any(not r.passed for r in load_case_results):
|
||
validation_status = "load_case_failure"
|
||
|
||
logger.info(
|
||
"Physics validation completed",
|
||
extra={
|
||
"design_ref": design_ref,
|
||
"material": material,
|
||
"min_sf": min_safety_factor,
|
||
"status": validation_status,
|
||
"num_load_cases": len(load_cases),
|
||
},
|
||
)
|
||
|
||
return PhysicsProof(
|
||
proof_id=proof_id,
|
||
governing_equations=governing_equations,
|
||
boundary_conditions_ref=str(boundary_conditions) if boundary_conditions else None,
|
||
safety_factor=min_safety_factor if min_safety_factor != float("inf") else None,
|
||
failure_modes_covered=failure_modes_covered,
|
||
metadata={
|
||
"material": material,
|
||
"material_properties": mat_props,
|
||
"load_case_results": [
|
||
{
|
||
"id": r.load_case_id,
|
||
"max_stress_mpa": r.max_stress_mpa,
|
||
"sf": r.safety_factor,
|
||
"passed": r.passed,
|
||
}
|
||
for r in load_case_results
|
||
],
|
||
"required_safety_factor": self._required_sf,
|
||
},
|
||
validation_status=validation_status,
|
||
warnings=warnings,
|
||
)
|
||
|
||
def _validate_load_case(
|
||
self,
|
||
load_case: dict[str, Any],
|
||
mat_props: dict[str, float],
|
||
dimensions: dict[str, float] | None,
|
||
) -> LoadCaseResult:
|
||
"""Validate a single load case."""
|
||
lc_id = load_case.get("id", str(uuid.uuid4())[:8])
|
||
|
||
# Extract load parameters
|
||
force_n = load_case.get("force_n", 0)
|
||
moment_nm = load_case.get("moment_nm", 0)
|
||
pressure_mpa = load_case.get("pressure_mpa", 0)
|
||
temperature_c = load_case.get("temperature_c", 25)
|
||
|
||
# Get material limits
|
||
yield_strength = mat_props.get("yield_strength_mpa", 100)
|
||
ultimate_strength = mat_props.get("ultimate_strength_mpa", 150)
|
||
max_temp = mat_props.get("max_service_temp_c", 100)
|
||
|
||
# Calculate stress (simplified - assumes basic geometry)
|
||
area_mm2 = 100.0 # Default cross-sectional area
|
||
if dimensions:
|
||
width = dimensions.get("width_mm", 10)
|
||
height = dimensions.get("height_mm", 10)
|
||
area_mm2 = width * height
|
||
|
||
# Basic stress calculation
|
||
axial_stress = force_n / area_mm2 if area_mm2 > 0 else 0
|
||
bending_stress = 0
|
||
if moment_nm and dimensions:
|
||
# Simplified bending: M*c/I where c = height/2, I = width*height^3/12
|
||
height = dimensions.get("height_mm", 10)
|
||
width = dimensions.get("width_mm", 10)
|
||
c = height / 2
|
||
i = width * (height ** 3) / 12
|
||
bending_stress = (moment_nm * 1000 * c) / i if i > 0 else 0
|
||
|
||
# Combined stress (von Mises simplified for 1D)
|
||
max_stress = abs(axial_stress) + abs(bending_stress) + pressure_mpa
|
||
|
||
# Calculate safety factors
|
||
yield_sf = yield_strength / max_stress if max_stress > 0 else float("inf")
|
||
ultimate_sf = ultimate_strength / max_stress if max_stress > 0 else float("inf")
|
||
|
||
# Check temperature limits
|
||
temp_ok = temperature_c <= max_temp
|
||
|
||
# Determine if load case passes
|
||
passed = (
|
||
yield_sf >= self._required_sf
|
||
and ultimate_sf >= self._required_sf
|
||
and temp_ok
|
||
)
|
||
|
||
failure_mode = None
|
||
if yield_sf < self._required_sf:
|
||
failure_mode = "yield_failure"
|
||
elif ultimate_sf < self._required_sf:
|
||
failure_mode = "ultimate_failure"
|
||
elif not temp_ok:
|
||
failure_mode = "thermal_failure"
|
||
|
||
return LoadCaseResult(
|
||
load_case_id=lc_id,
|
||
max_stress_mpa=max_stress,
|
||
safety_factor=min(yield_sf, ultimate_sf),
|
||
passed=passed,
|
||
failure_mode=failure_mode,
|
||
details={
|
||
"axial_stress_mpa": axial_stress,
|
||
"bending_stress_mpa": bending_stress,
|
||
"yield_sf": yield_sf,
|
||
"ultimate_sf": ultimate_sf,
|
||
"temperature_ok": temp_ok,
|
||
},
|
||
)
|
||
|
||
def _select_governing_equations(self, load_cases: list[dict[str, Any]]) -> str:
|
||
"""Select appropriate governing equations based on load types."""
|
||
equations = []
|
||
|
||
# Check load types
|
||
has_static = any(lc.get("type") == "static" or lc.get("force_n") for lc in load_cases)
|
||
has_thermal = any(lc.get("temperature_c") for lc in load_cases)
|
||
has_dynamic = any(lc.get("type") == "dynamic" or lc.get("frequency_hz") for lc in load_cases)
|
||
has_pressure = any(lc.get("pressure_mpa") for lc in load_cases)
|
||
|
||
if has_static:
|
||
equations.append("Linear elasticity (Hooke's Law)")
|
||
if has_thermal:
|
||
equations.append("Thermal expansion (α·ΔT)")
|
||
if has_dynamic:
|
||
equations.append("Modal analysis (eigenvalue)")
|
||
if has_pressure:
|
||
equations.append("Pressure vessel (hoop stress)")
|
||
|
||
if not equations:
|
||
equations.append("Linear elasticity (default)")
|
||
|
||
return "; ".join(equations)
|
||
|
||
def get_material_properties(self, material: str) -> dict[str, float] | None:
|
||
"""Get properties for a material."""
|
||
return self._materials.get(material.lower().replace(" ", "_"))
|
||
|
||
def list_materials(self) -> list[str]:
|
||
"""List available materials."""
|
||
return list(self._materials.keys())
|
||
|
||
def add_material(self, name: str, properties: dict[str, float]) -> None:
|
||
"""Add a custom material to the database."""
|
||
self._materials[name.lower().replace(" ", "_")] = properties
|
||
|
||
|
||
class StubPhysicsAuthority(PhysicsAuthorityInterface):
|
||
"""
|
||
Stub implementation for testing.
|
||
|
||
Returns a minimal proof if design_ref present; else raises PhysicsUnderdefinedError.
|
||
|
||
Note: This is a stub for testing. Use PhysicsAuthority for real validation.
|
||
"""
|
||
|
||
def validate_physics(
|
||
self,
|
||
design_ref: str,
|
||
load_cases: list[dict[str, Any]] | None = None,
|
||
**kwargs: Any,
|
||
) -> PhysicsProof | None:
|
||
if not design_ref:
|
||
raise PhysicsUnderdefinedError("design_ref required")
|
||
return PhysicsProof(
|
||
proof_id=f"stub_proof_{design_ref}",
|
||
failure_modes_covered=["stub"],
|
||
validation_status="stub_validated",
|
||
warnings=["This is a stub validation - not for production use"],
|
||
)
|