Spaces:
Runtime error
Runtime error
| """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", | |
| } | |
| 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) | |
| 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) | |