Spaces:
Sleeping
Sleeping
Dan Vancea commited on
Commit ·
ebd57ea
1
Parent(s): 632daa5
gdfgdfgdfgfg
Browse files- call_models.py +2 -1
- predict_from_supabase.py +33 -35
- process_inputs.py +54 -0
- scheduling_rl.py +25 -10
call_models.py
CHANGED
|
@@ -7,6 +7,7 @@ import numpy as np
|
|
| 7 |
|
| 8 |
from predict_from_supabase import predict_replacements
|
| 9 |
from model import DegradationModel, COMPONENT_NAMES
|
|
|
|
| 10 |
|
| 11 |
load_dotenv()
|
| 12 |
|
|
@@ -44,7 +45,7 @@ def predict():
|
|
| 44 |
return jsonify({"error": f"model file not found: {model_path}"}), 404
|
| 45 |
|
| 46 |
h0 = np.array(h0_raw, dtype=float)
|
| 47 |
-
X = np.array(X_raw,
|
| 48 |
|
| 49 |
if h0.shape != (model.N,):
|
| 50 |
return jsonify({"error": f"h0 must have {model.N} elements, got {h0.shape}"}), 400
|
|
|
|
| 7 |
|
| 8 |
from predict_from_supabase import predict_replacements
|
| 9 |
from model import DegradationModel, COMPONENT_NAMES
|
| 10 |
+
from process_inputs import process_inputs
|
| 11 |
|
| 12 |
load_dotenv()
|
| 13 |
|
|
|
|
| 45 |
return jsonify({"error": f"model file not found: {model_path}"}), 404
|
| 46 |
|
| 47 |
h0 = np.array(h0_raw, dtype=float)
|
| 48 |
+
X = process_inputs(np.array(X_raw, dtype=float))
|
| 49 |
|
| 50 |
if h0.shape != (model.N,):
|
| 51 |
return jsonify({"error": f"h0 must have {model.N} elements, got {h0.shape}"}), 400
|
predict_from_supabase.py
CHANGED
|
@@ -10,27 +10,24 @@ from supabase import create_client
|
|
| 10 |
from stable_baselines3 import PPO
|
| 11 |
from model import DegradationModel
|
| 12 |
from scheduling_rl import _ACTION_TABLE, COMPONENT_NAMES
|
|
|
|
| 13 |
|
| 14 |
# ---------------------------------------------------------------------------
|
| 15 |
# Supabase client
|
| 16 |
# ---------------------------------------------------------------------------
|
| 17 |
|
| 18 |
-
_sb =
|
| 19 |
|
| 20 |
-
|
| 21 |
-
global _sb
|
| 22 |
-
if _sb is None:
|
| 23 |
-
_sb = create_client(os.environ["SUPABASE_URL"], os.environ["SUPABASE_SERVICE_KEY"])
|
| 24 |
-
return _sb
|
| 25 |
-
|
| 26 |
-
# Column order must match the obs vector expected by the PPO (C=7 conditions)
|
| 27 |
_CONDITION_COLS = [
|
| 28 |
"ambient_temperature_c",
|
| 29 |
"build_chamber_temp_c",
|
| 30 |
"ambient_humidity_pct",
|
| 31 |
"powder_contamination_level",
|
|
|
|
| 32 |
"build_volume_cm3",
|
| 33 |
"recoating_speed_mm_s",
|
|
|
|
| 34 |
"maintenance_level",
|
| 35 |
]
|
| 36 |
|
|
@@ -47,48 +44,48 @@ _HEALTH_COLS = [
|
|
| 47 |
]
|
| 48 |
|
| 49 |
# ---------------------------------------------------------------------------
|
| 50 |
-
#
|
| 51 |
# ---------------------------------------------------------------------------
|
| 52 |
|
| 53 |
-
def
|
| 54 |
-
"""
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
.select("last_repair")
|
| 59 |
-
.eq("id", printer_id)
|
| 60 |
-
.single()
|
| 61 |
-
.execute()
|
| 62 |
-
.data
|
| 63 |
-
)
|
| 64 |
-
if not printer:
|
| 65 |
-
raise ValueError(f"Printer {printer_id} not found")
|
| 66 |
|
| 67 |
-
last_repair = datetime.fromisoformat(printer["last_repair"]).replace(tzinfo=timezone.utc)
|
| 68 |
-
time_step_id = int((t - last_repair).total_seconds() // 3600)
|
| 69 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
row = (
|
| 71 |
-
|
| 72 |
.select(", ".join(_HEALTH_COLS))
|
| 73 |
.eq("id", printer_id)
|
| 74 |
-
.
|
| 75 |
-
.order("time_step_id", desc=False)
|
| 76 |
.limit(1)
|
| 77 |
.execute()
|
| 78 |
.data
|
| 79 |
)
|
| 80 |
if not row:
|
| 81 |
-
raise ValueError(f"No snapshot for printer {printer_id}
|
| 82 |
return np.array([row[0][c] for c in _HEALTH_COLS], dtype=np.float64)
|
| 83 |
|
| 84 |
|
| 85 |
def _fetch_conditions(printer_id: str, t: datetime) -> np.ndarray:
|
| 86 |
"""Closest conditions row at or before t."""
|
| 87 |
row = (
|
| 88 |
-
|
| 89 |
.select(", ".join(_CONDITION_COLS))
|
| 90 |
.eq("id", printer_id)
|
| 91 |
-
.lte("timestamp", t.isoformat())
|
| 92 |
.order("timestamp", desc=True)
|
| 93 |
.limit(1)
|
| 94 |
.execute()
|
|
@@ -96,7 +93,8 @@ def _fetch_conditions(printer_id: str, t: datetime) -> np.ndarray:
|
|
| 96 |
)
|
| 97 |
if not row:
|
| 98 |
raise ValueError(f"No conditions found for printer {printer_id} at {t}")
|
| 99 |
-
|
|
|
|
| 100 |
|
| 101 |
# ---------------------------------------------------------------------------
|
| 102 |
# Main prediction
|
|
@@ -115,8 +113,8 @@ def predict_replacements(
|
|
| 115 |
DegradationModel.load(model_path) # validates model exists
|
| 116 |
ppo = PPO.load(ppo_path)
|
| 117 |
|
| 118 |
-
health = _fetch_health(printer_id
|
| 119 |
-
X_t = _fetch_conditions(printer_id, t)
|
| 120 |
|
| 121 |
obs = np.concatenate([health, X_t, [budget_remaining / W], [t_hours]]).astype(np.float32)
|
| 122 |
action, _ = ppo.predict(obs, deterministic=True)
|
|
@@ -125,7 +123,7 @@ def predict_replacements(
|
|
| 125 |
to_replace = [COMPONENT_NAMES[i] for i, b in enumerate(bits) if b]
|
| 126 |
return {
|
| 127 |
"printer_id": printer_id,
|
| 128 |
-
"timestamp": t.isoformat(),
|
| 129 |
"health": dict(zip(COMPONENT_NAMES, health.tolist())),
|
| 130 |
"conditions": dict(zip(_CONDITION_COLS, X_t.tolist())),
|
| 131 |
"replace": to_replace,
|
|
@@ -140,7 +138,7 @@ def predict_replacements(
|
|
| 140 |
if __name__ == "__main__":
|
| 141 |
import json, sys
|
| 142 |
printer_id = sys.argv[1] if len(sys.argv) > 1 else "printer_001"
|
| 143 |
-
t =
|
| 144 |
budget = float(sys.argv[3]) if len(sys.argv) > 3 else 10_000.0
|
| 145 |
|
| 146 |
result = predict_replacements(printer_id, t, budget_remaining=budget)
|
|
|
|
| 10 |
from stable_baselines3 import PPO
|
| 11 |
from model import DegradationModel
|
| 12 |
from scheduling_rl import _ACTION_TABLE, COMPONENT_NAMES
|
| 13 |
+
from process_inputs import process_inputs
|
| 14 |
|
| 15 |
# ---------------------------------------------------------------------------
|
| 16 |
# Supabase client
|
| 17 |
# ---------------------------------------------------------------------------
|
| 18 |
|
| 19 |
+
_sb = create_client(os.environ["SUPABASE_URL"], os.environ["SUPABASE_KEY"])
|
| 20 |
|
| 21 |
+
# Column order must match INPUT_NAMES in model.py (C=9, obs vector is R^20)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
_CONDITION_COLS = [
|
| 23 |
"ambient_temperature_c",
|
| 24 |
"build_chamber_temp_c",
|
| 25 |
"ambient_humidity_pct",
|
| 26 |
"powder_contamination_level",
|
| 27 |
+
"print_hours",
|
| 28 |
"build_volume_cm3",
|
| 29 |
"recoating_speed_mm_s",
|
| 30 |
+
"recoating_cycles",
|
| 31 |
"maintenance_level",
|
| 32 |
]
|
| 33 |
|
|
|
|
| 44 |
]
|
| 45 |
|
| 46 |
# ---------------------------------------------------------------------------
|
| 47 |
+
# Helpers
|
| 48 |
# ---------------------------------------------------------------------------
|
| 49 |
|
| 50 |
+
def _as_aware(dt: datetime) -> datetime:
|
| 51 |
+
"""Return a timezone-aware datetime, treating naive datetimes as UTC."""
|
| 52 |
+
if dt.tzinfo is None:
|
| 53 |
+
return dt.replace(tzinfo=timezone.utc)
|
| 54 |
+
return dt
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 55 |
|
|
|
|
|
|
|
| 56 |
|
| 57 |
+
def _parse_ts(s: str) -> datetime:
|
| 58 |
+
"""Parse an ISO timestamp string that may use a trailing Z."""
|
| 59 |
+
return _as_aware(datetime.fromisoformat(s.replace("Z", "+00:00")))
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# ---------------------------------------------------------------------------
|
| 63 |
+
# Fetch helpers
|
| 64 |
+
# ---------------------------------------------------------------------------
|
| 65 |
+
|
| 66 |
+
def _fetch_health(printer_id: str) -> np.ndarray:
|
| 67 |
+
"""Latest health snapshot for the printer."""
|
| 68 |
row = (
|
| 69 |
+
_sb.table("snapshots")
|
| 70 |
.select(", ".join(_HEALTH_COLS))
|
| 71 |
.eq("id", printer_id)
|
| 72 |
+
.order("time_step_id", desc=True)
|
|
|
|
| 73 |
.limit(1)
|
| 74 |
.execute()
|
| 75 |
.data
|
| 76 |
)
|
| 77 |
if not row:
|
| 78 |
+
raise ValueError(f"No snapshot found for printer {printer_id}")
|
| 79 |
return np.array([row[0][c] for c in _HEALTH_COLS], dtype=np.float64)
|
| 80 |
|
| 81 |
|
| 82 |
def _fetch_conditions(printer_id: str, t: datetime) -> np.ndarray:
|
| 83 |
"""Closest conditions row at or before t."""
|
| 84 |
row = (
|
| 85 |
+
_sb.table("conditions")
|
| 86 |
.select(", ".join(_CONDITION_COLS))
|
| 87 |
.eq("id", printer_id)
|
| 88 |
+
.lte("timestamp", _as_aware(t).isoformat())
|
| 89 |
.order("timestamp", desc=True)
|
| 90 |
.limit(1)
|
| 91 |
.execute()
|
|
|
|
| 93 |
)
|
| 94 |
if not row:
|
| 95 |
raise ValueError(f"No conditions found for printer {printer_id} at {t}")
|
| 96 |
+
# Coerce NULL columns to 0.0 (seed_data.py may omit some fields)
|
| 97 |
+
return np.array([float(row[0][c] or 0.0) for c in _CONDITION_COLS], dtype=np.float64)
|
| 98 |
|
| 99 |
# ---------------------------------------------------------------------------
|
| 100 |
# Main prediction
|
|
|
|
| 113 |
DegradationModel.load(model_path) # validates model exists
|
| 114 |
ppo = PPO.load(ppo_path)
|
| 115 |
|
| 116 |
+
health = _fetch_health(printer_id)
|
| 117 |
+
X_t = process_inputs(_fetch_conditions(printer_id, t))
|
| 118 |
|
| 119 |
obs = np.concatenate([health, X_t, [budget_remaining / W], [t_hours]]).astype(np.float32)
|
| 120 |
action, _ = ppo.predict(obs, deterministic=True)
|
|
|
|
| 123 |
to_replace = [COMPONENT_NAMES[i] for i, b in enumerate(bits) if b]
|
| 124 |
return {
|
| 125 |
"printer_id": printer_id,
|
| 126 |
+
"timestamp": _as_aware(t).isoformat(),
|
| 127 |
"health": dict(zip(COMPONENT_NAMES, health.tolist())),
|
| 128 |
"conditions": dict(zip(_CONDITION_COLS, X_t.tolist())),
|
| 129 |
"replace": to_replace,
|
|
|
|
| 138 |
if __name__ == "__main__":
|
| 139 |
import json, sys
|
| 140 |
printer_id = sys.argv[1] if len(sys.argv) > 1 else "printer_001"
|
| 141 |
+
t = _parse_ts(sys.argv[2]) if len(sys.argv) > 2 else datetime.now(tz=timezone.utc)
|
| 142 |
budget = float(sys.argv[3]) if len(sys.argv) > 3 else 10_000.0
|
| 143 |
|
| 144 |
result = predict_replacements(printer_id, t, budget_remaining=budget)
|
process_inputs.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
def process_inputs(X: np.ndarray) -> np.ndarray:
|
| 5 |
+
"""Convert inputs from natural units to normalized model inputs.
|
| 6 |
+
|
| 7 |
+
Index | Name | Natural unit | Notes
|
| 8 |
+
------|-----------------------|--------------|--------------------------------
|
| 9 |
+
0 | ambient_temperature_c | °C | Kelvin ratio — never 0
|
| 10 |
+
1 | build_chamber_temp_c | °C | Kelvin ratio — never 0
|
| 11 |
+
2 | ambient_humidity_pct | % | floor 0.25 — dry air still allows degradation
|
| 12 |
+
3 | powder_contamination | AQI [0–500] | floor 0.15 — even clean air causes wear
|
| 13 |
+
4 | print_hours | h | 0 → 0 (machine not running)
|
| 14 |
+
5 | build_volume_cm3 | cm³ | 0 → 0 (no active build)
|
| 15 |
+
6 | recoating_speed_mm_s | mm/s | 0 → 0 (recoater stationary)
|
| 16 |
+
7 | recoating_cycles | count | 0 → 0 (no recoating done)
|
| 17 |
+
8 | maintenance_level | [0, 1] | floor 0.2 — even perfect maintenance
|
| 18 |
+
| | | cannot eliminate physical wear
|
| 19 |
+
|
| 20 |
+
The model uses P = ∏ I_c; only a 0 output produces P = 0 (no degradation).
|
| 21 |
+
Inputs whose zero value merely means "minimal stress" (temperatures, humidity,
|
| 22 |
+
contamination, maintenance) carry a non-zero floor so they never falsely gate
|
| 23 |
+
off all degradation. Inputs that are truly "off" when zero (machine not
|
| 24 |
+
running, recoater stationary) map 0 → 0.
|
| 25 |
+
|
| 26 |
+
All outputs are non-negative. The mapping is linear throughout.
|
| 27 |
+
"""
|
| 28 |
+
X = np.asarray(X, dtype=float)
|
| 29 |
+
out = np.empty(9)
|
| 30 |
+
|
| 31 |
+
# --- Temperatures: T_K / T_ref (Kelvin ratio, never 0 above absolute zero) ---
|
| 32 |
+
# Reference points chosen so the maximum expected operating temperature ≈ 1.0.
|
| 33 |
+
# Ambient reference: 70 °C = 343.15 K (factory floor upper bound)
|
| 34 |
+
# Chamber reference: 350 °C = 623.15 K (upper bound for powder-bed processes)
|
| 35 |
+
out[0] = (np.maximum(X[0], -273.15) + 273.15) / 343.15
|
| 36 |
+
out[1] = (np.maximum(X[1], -273.15) + 273.15) / 623.15
|
| 37 |
+
|
| 38 |
+
# --- Humidity: non-zero floor, linear ---
|
| 39 |
+
# floor=0.25 at 0 %, reaching 1.0 at 100 %.
|
| 40 |
+
out[2] = 0.25 + 0.75 * np.clip(X[2], 0.0, 100.0) / 100.0
|
| 41 |
+
|
| 42 |
+
# --- Powder contamination (AQI 0–500): non-zero floor, linear ---
|
| 43 |
+
# floor=0.15 at AQI=0; reaches 1.0 at AQI=500.
|
| 44 |
+
out[3] = 0.15 + 0.85 * np.clip(X[3], 0.0, 500.0) / 500.0
|
| 45 |
+
out[4] = np.clip(X[4], 0.0, 168.0) / 168.0 # print hours (max 168 h/week)
|
| 46 |
+
out[5] = np.clip(X[5], 0.0, 15000.0) / 15000.0 # build volume (max 15 000 cm³/week)
|
| 47 |
+
out[6] = np.clip(X[6], 0.0, 150.0) / 150.0 # recoating speed (max 150 mm/s)
|
| 48 |
+
out[7] = np.clip(X[7], 0.0, 15000.0) / 15000.0 # recoating cycles (max 15 000/week)
|
| 49 |
+
|
| 50 |
+
# --- Maintenance: non-zero floor, linear ---
|
| 51 |
+
# level=0 (perfect) → 0.2 baseline; level=1 (no maintenance) → 1.0.
|
| 52 |
+
out[8] = 0.2 + 0.8 * np.clip(X[8], 0.0, 1.0)
|
| 53 |
+
|
| 54 |
+
return np.maximum(out, 0.0) # guarantee non-negative
|
scheduling_rl.py
CHANGED
|
@@ -477,17 +477,32 @@ def evaluate(
|
|
| 477 |
# ---------------------------------------------------------------------------
|
| 478 |
|
| 479 |
if __name__ == "__main__":
|
| 480 |
-
|
| 481 |
-
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 489 |
rng = np.random.default_rng(42)
|
| 490 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 491 |
|
| 492 |
env = PrinterEnv(
|
| 493 |
model=deg_model,
|
|
|
|
| 477 |
# ---------------------------------------------------------------------------
|
| 478 |
|
| 479 |
if __name__ == "__main__":
|
| 480 |
+
from process_inputs import process_inputs
|
| 481 |
+
|
| 482 |
+
deg_model = DegradationModel.load("model.npz")
|
| 483 |
+
print(f"Loaded model.npz N={deg_model.N} C={deg_model.C}")
|
| 484 |
+
|
| 485 |
+
# Representative operating-condition scenarios in natural units (matches phase2.py SCENARIOS).
|
| 486 |
+
# Each row: [ambient_temp_c, chamber_temp_c, humidity_pct, contamination_aqi,
|
| 487 |
+
# print_hours, build_volume_cm3, recoating_speed_mm_s, recoating_cycles, maintenance_level]
|
| 488 |
+
_SCENARIO_CONDITIONS = np.array([
|
| 489 |
+
[ 22.0, 180.0, 40.0, 20.0, 56.0, 4500.0, 100.0, 5000.0, 0.10], # nominal
|
| 490 |
+
[ 22.0, 180.0, 45.0, 150.0, 56.0, 4500.0, 100.0, 5000.0, 0.20], # high contamination
|
| 491 |
+
[ 28.0, 185.0, 50.0, 80.0, 168.0, 13500.0, 120.0, 15000.0, 0.20], # 24/7 heavy use
|
| 492 |
+
[ 20.0, 175.0, 30.0, 5.0, 300.0, 3000.0, 90.0, 3500.0, 0.05], # optimal lab
|
| 493 |
+
[ 40.0, 200.0, 55.0, 60.0, 56.0, 4500.0, 100.0, 5000.0, 0.20], # hot environment
|
| 494 |
+
[ 22.0, 180.0, 85.0, 30.0, 56.0, 4500.0, 100.0, 5000.0, 0.15], # high humidity
|
| 495 |
+
[ 25.0, 182.0, 45.0, 40.0, 56.0, 4500.0, 100.0, 5000.0, 0.90], # neglected maintenance
|
| 496 |
+
[ 38.0, 195.0, 20.0, 200.0, 80.0, 6500.0, 110.0, 7000.0, 0.40], # desert factory
|
| 497 |
+
], dtype=np.float64)
|
| 498 |
+
|
| 499 |
+
# Build an 8 000-step X_series by cycling through scenarios with small noise
|
| 500 |
rng = np.random.default_rng(42)
|
| 501 |
+
T = 8_000
|
| 502 |
+
base_rows = _SCENARIO_CONDITIONS[np.arange(T) % len(_SCENARIO_CONDITIONS)]
|
| 503 |
+
noise = rng.normal(0.0, 0.02, size=base_rows.shape) * base_rows # 2% relative noise
|
| 504 |
+
X_natural = np.clip(base_rows + noise, 0.0, None)
|
| 505 |
+
X_series = np.stack([process_inputs(row) for row in X_natural]) # (T, C) normalised
|
| 506 |
|
| 507 |
env = PrinterEnv(
|
| 508 |
model=deg_model,
|