Files
FusionAGI/fusionagi/maa/layers/physics_authority.py
defiQUG c052b07662
Some checks failed
Tests / test (3.10) (push) Has been cancelled
Tests / test (3.11) (push) Has been cancelled
Tests / test (3.12) (push) Has been cancelled
Tests / lint (push) Has been cancelled
Tests / docker (push) Has been cancelled
Initial commit: add .gitignore and README
2026-02-09 21:51:42 -08:00

450 lines
15 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"],
)