Spaces:
Running
Running
File size: 6,892 Bytes
b154e4c | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 | """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))
@dataclass
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()
|