298 def __init__(self, n_sensors: int, n_params: int):
299 self.n_sensors = n_sensors
300 self.n_params = n_params
302 # Track when each sensor was last calibrated
303 self.last_calibration_time: Dict[int, float] = {}
305 # Track prediction errors
306 self.error_history: Dict[int, deque] = {
307 i: deque(maxlen=50) for i in range(n_sensors)
310 # Calibration budget: How many calibrations can we request
311 self.calibration_budget = 10 # Per hour
312 self.last_budget_reset = time.time()
313 self.requests_this_hour = 0
315 logger.info(f"Active learning agent initialized: {n_sensors} sensors")
317 def should_request_calibration(
322 voltage_or_adc: float,
324 is_adc_code: bool = False,
325 ) -> Optional[CalibrationRequest]:
327 Decide if calibration should be requested for this sensor.
329 Returns: CalibrationRequest if calibration needed, None otherwise
331 # Reset budget hourly
332 if time.time() - self.last_budget_reset > 3600:
333 self.requests_this_hour = 0
334 self.last_budget_reset = time.time()
336 # Check if budget exhausted
337 if self.requests_this_hour >= self.calibration_budget:
343 # Reason 1: High uncertainty
344 if uncertainty > 50.0: # PSI
345 reasons.append(f"high uncertainty ({uncertainty:.1f} PSI)")
346 urgency = max(urgency, uncertainty / 100.0)
348 # Reason 2: Extrapolation (voltage/ADC outside calibrated range)
350 # ADC code: 24-bit signed, extrapolation if outside typical range
351 if voltage_or_adc < -1e9 or voltage_or_adc > 2.2e9:
352 reasons.append(f"extreme ADC ({voltage_or_adc:.0f})")
353 urgency = max(urgency, 0.8)
355 if voltage_or_adc < 0.5 or voltage_or_adc > 9.5:
356 reasons.append(f"extreme voltage ({voltage_or_adc:.2f}V)")
357 urgency = max(urgency, 0.8)
359 # Reason 3: Long time since last calibration
360 if sensor_id in self.last_calibration_time:
361 time_since = time.time() - self.last_calibration_time[sensor_id]
362 if time_since > 3600: # 1 hour
363 reasons.append(f"stale calibration ({time_since/3600:.1f}h)")
364 urgency = max(urgency, min(0.5, time_since / 7200))
366 reasons.append("never calibrated")
367 urgency = max(urgency, 0.9)
369 # Reason 4: Consistent prediction errors
370 if len(self.error_history[sensor_id]) > 10:
371 errors = list(self.error_history[sensor_id])
372 mean_error = np.mean(np.abs(errors))
373 if mean_error > 20.0: # PSI
374 reasons.append(f"high error rate ({mean_error:.1f} PSI)")
375 urgency = max(urgency, min(0.7, mean_error / 50.0))
377 # Make request if urgency is high enough
378 if urgency > 0.5 and reasons:
379 self.requests_this_hour += 1
381 # Suggest pressure range based on prediction
383 max(0, prediction - 2 * uncertainty),
384 prediction + 2 * uncertainty,
387 return CalibrationRequest(
389 voltage=voltage_or_adc,
390 reason="; ".join(reasons),
391 urgency=float(urgency),
392 suggested_pressure_range=suggested_range,
393 confidence=1.0 - urgency,
402 def record_prediction_error(self, sensor_id: int, error: float):
403 """Record prediction error for learning"""
404 if sensor_id < self.n_sensors:
405 self.error_history[sensor_id].append(error)
408# ===========================================================================================
409# INTELLIGENT ONLINE LEARNING
410# ===========================================================================================