microfactory-lab / sim /outcome.py
kylebrodeur's picture
Upload folder using huggingface_hub
6b09b49 verified
Raw
History Blame Contribute Delete
4.87 kB
"""Outcome simulator β€” the deterministic stand-in for the printer + sensors.
⚠ THIS IS THE PHYSICAL-WORLD PLACEHOLDER. See ../SIMULATION.md. On real
hardware this whole module is replaced by: stream g-code to the printer β†’
read env sensors β†’ a camera + the 3D-ADAM defect classifier reports the
outcome. Here we model the *same* physics the seed lessons describe, so the
learning loop is reproducible and runs with no hardware and no network.
Crucially this is NOT the model grading its own work. The Chief Engineer
proposes settings; this separate, deterministic world returns an outcome the
model never sees in advance β€” exactly the role a printer + sensors play.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from core.models import Environment, Job, PrintSettings
# Per-material workable bands (nozzle Β°C lo/ideal/hi, bed Β°C lo).
BANDS = {
"PLA": {"n_lo": 195, "n_id": 205, "n_hi": 225, "bed_lo": 52},
"PETG": {"n_lo": 230, "n_id": 240, "n_hi": 252, "bed_lo": 70},
"ABS": {"n_lo": 235, "n_id": 248, "n_hi": 262, "bed_lo": 95},
"TPU": {"n_lo": 215, "n_id": 228, "n_hi": 240, "bed_lo": 40},
}
# outcome strings stay inside models.OUTCOMES; failure_mode carries the detail.
_MODE_TO_OUTCOME = {
"none": "success",
"stringing": "failed_stringing",
"sag": "failed_sag",
"warp": "failed_sag",
"adhesion": "failed_sag",
"under_extrusion": "failed_sag",
}
@dataclass
class SimResult:
outcome: str # in models.OUTCOMES
quality: float # 0..1 β€” how clean the (simulated) print came out
failure_mode: str # "none"|"sag"|"stringing"|"adhesion"|"warp"|"under_extrusion"
penalties: dict[str, float] = field(default_factory=dict)
@property
def detail(self) -> str:
if self.failure_mode == "none":
return f"clean print (quality {self.quality:.2f})"
worst = max(self.penalties, key=self.penalties.get) if self.penalties else self.failure_mode
return f"{self.failure_mode} (quality {self.quality:.2f}, dominant cost: {worst})"
def simulate(settings: PrintSettings, job: Job, env: Environment) -> SimResult:
"""Deterministic physics-lite outcome. Higher quality = cleaner print."""
b = BANDS.get(job.material.upper(), BANDS["PLA"])
geo = job.geometry_type
s = settings
pen: dict[str, float] = {}
# 1) Nozzle temperature band β€” too cold under-extrudes, too hot oozes.
if s.nozzle_temp < b["n_lo"]:
pen["under_extrusion"] = min(0.5, (b["n_lo"] - s.nozzle_temp) / 40)
over = max(0.0, s.nozzle_temp - b["n_hi"]) / 40 # feeds stringing/sag below
# 2) Cooling vs. unsupported geometry (overhang/bridge/vase) β€” the sag axis.
if geo in ("overhang", "bridge", "vase"):
need = {"overhang": 60, "bridge": 82, "vase": 50}[geo] + max(0.0, env.temp - 20) * 3.5
sag = max(0.0, (need - s.fan_pct)) / 100 * 0.7 + over * 0.6
if sag > 0:
pen["sag"] = min(0.6, sag)
# 3) Stringing β€” hygroscopic materials in humid air, hot nozzle, weak retraction.
hygro = job.material.upper() in ("PETG", "TPU", "ABS")
if geo == "stringing" or hygro or env.humidity > 50:
w = 0.6 if geo == "stringing" else 0.4
string = (max(0.0, env.humidity - 45) / 55 * 0.45
+ over * 0.45
+ max(0.0, 2.0 - s.retraction_mm) / 2 * 0.30) * w
if string > 0.02:
pen["stringing"] = min(0.6, string)
# 4) Adhesion β€” first layer needs a hot enough bed and calm air.
if geo == "adhesion":
adh = (max(0.0, b["bed_lo"] - s.bed_temp) / 30 * 0.6
+ s.first_layer_fan_pct / 100 * 0.4)
if adh > 0.02:
pen["adhesion"] = min(0.6, adh)
# 5) ABS hates fan β€” too much cooling cracks/warps it.
if job.material.upper() == "ABS" and s.fan_pct > 40:
pen["warp"] = min(0.4, (s.fan_pct - 40) / 100 * 0.6)
# 6) Build-plate position β€” bed edges/corners run cooler and see more draft, so
# warp + first-layer adhesion suffer there; worst for high-shrink materials.
# 'center' (default) = 0 β†’ no change to prior behavior. See ../SIMULATION.md.
pos_sev = {"center": 0.0, "edge": 0.5, "corner": 1.0}.get(getattr(job, "bed_position", "center"), 0.0)
if pos_sev:
mat = {"ABS": 0.45, "PETG": 0.18, "PLA": 0.06, "TPU": 0.05}.get(job.material.upper(), 0.10)
pen["warp"] = min(0.6, pen.get("warp", 0.0) + pos_sev * mat)
pen["adhesion"] = min(0.6, pen.get("adhesion", 0.0) + pos_sev * mat * 0.5)
total = sum(pen.values())
quality = max(0.0, min(1.0, 1.0 - total))
if quality >= 0.7 or not pen:
return SimResult("success", quality, "none", pen)
mode = max(pen, key=pen.get)
return SimResult(_MODE_TO_OUTCOME.get(mode, "failed_sag"), quality, mode, pen)