Dan Vancea commited on
Commit
ebd57ea
·
1 Parent(s): 632daa5

gdfgdfgdfgfg

Browse files
Files changed (4) hide show
  1. call_models.py +2 -1
  2. predict_from_supabase.py +33 -35
  3. process_inputs.py +54 -0
  4. 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, dtype=float)
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 = None
19
 
20
- def _get_sb():
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
- # Fetch helpers
51
  # ---------------------------------------------------------------------------
52
 
53
- def _fetch_health(printer_id: str, t: datetime) -> np.ndarray:
54
- """Snapshot whose time_step_id matches hours elapsed since last_repair."""
55
- sb = _get_sb()
56
- printer = (
57
- sb.table("printers")
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
- sb.table("snapshots")
72
  .select(", ".join(_HEALTH_COLS))
73
  .eq("id", printer_id)
74
- .gte("time_step_id", time_step_id)
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} at time_step_id={time_step_id} (t={t})")
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
- _get_sb().table("conditions")
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
- return np.array([row[0][c] for c in _CONDITION_COLS], dtype=np.float64)
 
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, t)
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 = datetime.fromisoformat(sys.argv[2]) if len(sys.argv) > 2 else datetime.now()
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
- N, C = 9, 9
482
- lambda_rates = np.array([0.05, 0.10, 0.02, 0.03, 0.01, 0.08, 0.06, 0.04, 0.03])
483
-
484
- deg_model = DegradationModel(
485
- N=N, C=C, lambda_rates=lambda_rates, seed=0
486
- )
487
-
488
- # Synthetic X_series: 8 000 hours of operating conditions drawn from [0, 1]
 
 
 
 
 
 
 
 
 
 
 
489
  rng = np.random.default_rng(42)
490
- #X_series = rng.uniform(0.0, 1.0, size=(8_000, C))
 
 
 
 
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,