Spaces:
Running
Running
| """noctilith/sim/material_profiles.py — M13: VoxelDef → MaterialPipelineProfile.""" | |
| from __future__ import annotations | |
| import math | |
| from dataclasses import dataclass, field, replace | |
| from typing import Any, Dict, Optional, TYPE_CHECKING | |
| if TYPE_CHECKING: | |
| from engine.voxel_registry import VoxelDef | |
| from engine.sim.material_degradation import MaterialDegradationConfig | |
| from engine.sim.fracture import FractureBridgeConfig | |
| from engine.sim.world_reaction import WorldReactionConfig | |
| from engine.sim.debris_world_coupling import DebrisWorldCouplingConfig | |
| SCHEMA_VERSION = "noctilith.schema.v1" | |
| MODULE_NAME = "noctilith.sim.material_profiles" | |
| def clamp(v: float, lo: float, hi: float) -> float: | |
| return max(lo, min(hi, v)) | |
| class MaterialPipelineProfile: | |
| voxel_name: str = "unknown" | |
| voxel_id: int = -1 | |
| degradation: MaterialDegradationConfig = field(default_factory=MaterialDegradationConfig) | |
| fracture: FractureBridgeConfig = field(default_factory=FractureBridgeConfig) | |
| world_reaction: WorldReactionConfig = field(default_factory=WorldReactionConfig) | |
| coupling: DebrisWorldCouplingConfig = field(default_factory=DebrisWorldCouplingConfig) | |
| archetype: str = "generic" | |
| thermal_fragility: float = 0.5 | |
| structural_index: float = 0.5 | |
| debris_tendency: float = 0.5 | |
| def summary(self) -> Dict[str, Any]: | |
| return { | |
| "schema_version": SCHEMA_VERSION, | |
| "module_name": MODULE_NAME, | |
| "voxel_name": self.voxel_name, | |
| "voxel_id": self.voxel_id, | |
| "archetype": self.archetype, | |
| "thermal_fragility": float(self.thermal_fragility), | |
| "structural_index": float(self.structural_index), | |
| "debris_tendency": float(self.debris_tendency), | |
| "fracture_threshold": float(self.fracture.fracture_risk_threshold), | |
| "propagation_gain": float(self.fracture.propagation_gain), | |
| "damage_gain": float(self.world_reaction.damage_gain), | |
| "entropy_gain": float(self.coupling.entropy_gain), | |
| "thermal_gain": float(self.coupling.thermal_gain), | |
| "fatigue_exponent": float(self.degradation.fatigue_exponent), | |
| } | |
| def derive_from_voxel(vdef: "VoxelDef") -> MaterialPipelineProfile: | |
| b = float(clamp(vdef.brittleness, 0.0, 1.0)) | |
| h = float(clamp(vdef.hardness, 0.0, 1.0)) | |
| por = float(clamp(vdef.porosity, 0.0, 1.0)) | |
| flm = float(clamp(vdef.flammability, 0.0, 1.0)) | |
| kth = float(max(1e-6, vdef.thermal_conductivity)) | |
| E = float(max(1.0, vdef.young_modulus)) | |
| Tm = float(max(1.0, vdef.melting_point_k)) | |
| k_norm = clamp(math.log10(kth + 1) / 2.5, 0.0, 1.0) | |
| E_norm = clamp(math.log10(E + 1) / 12.0, 0.0, 1.0) | |
| temp_w = clamp(0.25 + k_norm * 0.35, 0.25, 0.60) | |
| entropy_w = clamp(0.20 + por * 0.55, 0.20, 0.75) | |
| rms_w = clamp(1.0 - temp_w - entropy_w, 0.05, 0.40) | |
| fat_exp = clamp(1.05 + E_norm * 0.55 + por * 0.25, 1.05, 1.85) | |
| risk_gain = clamp(0.45 + b * 0.35 + por * 0.20, 0.45, 1.0) | |
| degradation = replace(MaterialDegradationConfig(), | |
| temp_weight=round(temp_w,4), entropy_weight=round(entropy_w,4), | |
| rms_weight=round(rms_w,4), fatigue_exponent=round(fat_exp,4), | |
| risk_gain=round(risk_gain,4)) | |
| frac_thresh = clamp(1.0 - b * 0.60, 0.35, 0.95) | |
| prop_gain = clamp(b * 0.55 + 0.10, 0.10, 0.65) | |
| prop_radius = 1 + (1 if b > 0.65 else 0) | |
| fracture = replace(FractureBridgeConfig(), | |
| fracture_risk_threshold=round(frac_thresh,4), | |
| propagation_gain=round(prop_gain,4), | |
| propagation_radius=prop_radius, | |
| return_fracture_risk_field=True) | |
| dmg_gain = clamp(1.0 - h * 0.65, 0.30, 0.90) | |
| debris_thr = clamp(0.60 + h * 0.30, 0.60, 0.95) | |
| dmg_thr = clamp(0.50 + h * 0.30, 0.50, 0.85) | |
| world_reaction = replace(WorldReactionConfig(), | |
| damage_gain=round(dmg_gain,4), | |
| damage_threshold=round(dmg_thr,4), | |
| debris_threshold=round(debris_thr,4)) | |
| ent_gain = clamp(0.20 + por * 0.65 + b * 0.15, 0.20, 1.0) | |
| therm_gain = clamp(0.08 + k_norm * 0.25, 0.08, 0.35) | |
| frag_thresh = clamp(1.0 - flm * 0.60, 0.35, 0.95) | |
| frag_gain = clamp(0.20 + flm * 0.45, 0.20, 0.65) | |
| coupling = replace(DebrisWorldCouplingConfig(), | |
| entropy_gain=round(ent_gain,4), thermal_gain=round(therm_gain,4), | |
| fragmentation_strength_threshold=round(frag_thresh,4), | |
| fragmentation_strength_gain=round(frag_gain,4)) | |
| thermal_fragility = clamp(1.0 - math.log10(Tm+1)/4.5, 0.0, 1.0) | |
| structural_index = clamp(E_norm*0.6 + h*0.4, 0.0, 1.0) | |
| debris_tendency = clamp(b*0.5 + flm*0.3 + (1.0-h)*0.2, 0.0, 1.0) | |
| archetype = _classify(b, h, por, flm) | |
| return MaterialPipelineProfile( | |
| voxel_name=str(vdef.name), voxel_id=int(vdef.id), | |
| degradation=degradation, fracture=fracture, | |
| world_reaction=world_reaction, coupling=coupling, | |
| archetype=archetype, | |
| thermal_fragility=round(thermal_fragility,4), | |
| structural_index=round(structural_index,4), | |
| debris_tendency=round(debris_tendency,4)) | |
| def _classify(b: float, h: float, por: float, flm: float) -> str: | |
| if flm > 0.5: return "organic" | |
| if por > 0.3: return "porous" | |
| if b > 0.7 and h > 0.5: return "brittle" | |
| if b < 0.3 and h > 0.5: return "ductile" | |
| if b > 0.6 and h < 0.4: return "fused" | |
| return "generic" | |
| class MaterialProfileCache: | |
| def __init__(self) -> None: | |
| self._cache: Dict[int, MaterialPipelineProfile] = {} | |
| def get(self, voxel_id: int, registry=None) -> Optional[MaterialPipelineProfile]: | |
| if voxel_id in self._cache: | |
| return self._cache[voxel_id] | |
| if registry is None: | |
| from engine.voxel_registry import REGISTRY | |
| registry = REGISTRY | |
| vdef = registry.by_id(voxel_id) | |
| if vdef is None: return None | |
| profile = derive_from_voxel(vdef) | |
| self._cache[voxel_id] = profile | |
| return profile | |
| def get_by_name(self, name: str, registry=None) -> Optional[MaterialPipelineProfile]: | |
| if registry is None: | |
| from engine.voxel_registry import REGISTRY | |
| registry = REGISTRY | |
| vdef = registry.by_name(name) | |
| if vdef is None: return None | |
| return self.get(vdef.id, registry) | |
| def warm_all(self, registry=None) -> int: | |
| if registry is None: | |
| from engine.voxel_registry import REGISTRY | |
| registry = REGISTRY | |
| for vid in registry.all_ids(): | |
| self.get(vid, registry) | |
| return len(self._cache) | |
| def clear(self) -> None: | |
| self._cache.clear() | |
| def __len__(self) -> int: | |
| return len(self._cache) | |
| PROFILE_CACHE = MaterialProfileCache() | |