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()