STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
autonomous_calibration_engine.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2"""
3Autonomous Calibration Engine
4Advanced self-learning calibration system that evolves priors based on system behavior.
5
6Key Features:
7- Online learning with recursive Bayesian updates
8- Temporal evolution modeling with forgetting factors
9- Active learning: System requests calibration when needed
10- Drift detection and automatic adaptation
11- Empirical Bayes for continuous prior improvement
12- Confidence-based autonomous decision making
13- Transfer learning across sensors and sessions
14"""
15
16import numpy as np
17import logging
18from typing import Dict, List, Tuple, Optional, Callable
19from dataclasses import dataclass, field
20from collections import deque
21import time
22from scipy.stats import chi2, norm
23from scipy.linalg import cho_factor, cho_solve
24
25logger = logging.getLogger(__name__)
26
27# ===========================================================================================
28# TEMPORAL EVOLUTION AND ADAPTATION
29# ===========================================================================================
30
31
32@dataclass
34 """State of system evolution over time"""
35
36 timestamp: float
37 parameters: np.ndarray # Current parameter estimate
38 covariance: np.ndarray # Current uncertainty
39 confidence: float # System confidence in current state
40 drift_rate: float # Estimated rate of parameter drift
41 quality_score: float # Quality of recent predictions
42
43
44@dataclass
46 """Autonomous calibration request from the system"""
47
48 sensor_id: int
49 voltage: float
50 reason: str # Why calibration is needed
51 urgency: float # 0-1, how urgent
52 suggested_pressure_range: Tuple[float, float] # Expected pressure range
53 confidence: float # Current prediction confidence
54
55
57 """
58 Evolves population prior based on ongoing system performance.
59 Uses empirical Bayes to continuously improve prior from all sensors.
60 """
61
62 def __init__(self, n_params: int, forgetting_factor: float = 0.995):
63 self.n_params = n_params
64 self.forgetting_factor = forgetting_factor # Decay old information
65
66 # Initialize with weakly informative prior
67 self.prior_mean = np.zeros(n_params)
68 self.prior_mean[1] = 200.0 # Linear term
69 self.prior_covariance = np.diag(
70 [10000.0, 100000.0] + [10000.0] * (n_params - 2)
71 )
72
73 # Empirical Bayes statistics
74 self.sensor_posteriors: Dict[int, Tuple[np.ndarray, np.ndarray]] = {}
75 self.update_count = 0
76 self.effective_sample_size = 1.0 # Starts low, grows with data
77
78 # Temporal evolution
79 self.history: deque = deque(maxlen=100)
81
82 logger.info(
83 f"Adaptive prior evolution initialized: n_params={n_params}, λ={forgetting_factor}"
84 )
85
87 self,
88 sensor_id: int,
89 posterior_mean: np.ndarray,
90 posterior_cov: np.ndarray,
91 quality: float,
92 ):
93 """
94 Update population prior from individual sensor posterior.
95 Uses empirical Bayes: prior is the mean of all sensor posteriors.
96 """
97 # Apply forgetting factor to existing data
99 self.effective_sample_size += quality # Add new sample weighted by quality
100
101 # Store sensor posterior
102 self.sensor_posteriors[sensor_id] = (
103 posterior_mean.copy(),
104 posterior_cov.copy(),
105 )
106
107 # Empirical Bayes update: Pool all sensor posteriors
108 if len(self.sensor_posteriors) >= 2:
110
111 self.update_count += 1
112
113 # Record history
114 self.history.append(
116 timestamp=time.time(),
117 parameters=self.prior_mean.copy(),
118 covariance=self.prior_covariance.copy(),
119 confidence=self._compute_confidence(),
120 drift_rate=self.drift_detector.estimate_drift_rate(),
121 quality_score=quality,
122 )
123 )
124
125 if self.update_count % 10 == 0:
126 logger.info(
127 f"Prior evolution: {len(self.sensor_posteriors)} sensors, "
128 f"ESS={self.effective_sample_size:.1f}, "
129 f"confidence={self._compute_confidence():.3f}"
130 )
131
133 """
134 Empirical Bayes: Estimate population prior from sensor posteriors.
135
136 Model: θⱼ ~ N(μ_pop, Σ_pop) (sensors drawn from population)
137
138 Estimate μ_pop = mean(θⱼ) and Σ_pop = var(θⱼ) + mean(Σⱼ)
139 """
140 posteriors = list(self.sensor_posteriors.values())
141 means = np.array([m for m, _ in posteriors])
142 covs = np.array([c for _, c in posteriors])
143
144 # Between-sensor variance (how much sensors differ)
145 between_var = np.cov(means.T)
146
147 # Within-sensor variance (average uncertainty)
148 within_var = np.mean(covs, axis=0)
149
150 # Total population variance
151 # Shrink towards diagonal for numerical stability
152 pop_var = between_var + within_var
153 pop_var = 0.8 * pop_var + 0.2 * np.diag(np.diag(pop_var))
154
155 # Population mean (weighted by inverse variance)
156 weights = []
157 for _, cov in posteriors:
158 try:
159 # Weight by precision (inverse covariance)
160 prec = np.linalg.inv(cov + 1e-6 * np.eye(self.n_params))
161 weights.append(np.trace(prec))
162 except:
163 weights.append(1.0)
164
165 weights = np.array(weights)
166 weights /= np.sum(weights)
167
168 pop_mean = np.sum(means.T * weights, axis=1)
169
170 # Apply forgetting: blend old prior with new estimate
171 blend = 1.0 / (1.0 + self.effective_sample_size)
172 self.prior_mean = (1 - blend) * self.prior_mean + blend * pop_mean
173 self.prior_covariance = (1 - blend) * self.prior_covariance + blend * pop_var
174
175 # Regularization for numerical stability
176 self.prior_covariance += 1e-6 * np.eye(self.n_params)
177
178 logger.debug(
179 f"Empirical Bayes update: blend={blend:.3f}, "
180 f"mean_shift={np.linalg.norm(pop_mean - self.prior_mean):.2e}"
181 )
182
183 def _compute_confidence(self) -> float:
184 """Compute confidence in current prior"""
185 # Confidence grows with effective sample size and decreases with uncertainty
186 ess_factor = np.tanh(
187 self.effective_sample_size / 10.0
188 ) # Saturates at ~10 samples
189
190 # Uncertainty factor: lower variance = higher confidence
191 try:
192 trace = np.trace(self.prior_covariance)
193 uncertainty_factor = 1.0 / (1.0 + trace / 1e6)
194 except:
195 uncertainty_factor = 0.1
196
197 confidence = 0.7 * ess_factor + 0.3 * uncertainty_factor
198 return float(np.clip(confidence, 0, 1))
199
200 def get_prior(self) -> Tuple[np.ndarray, np.ndarray, float]:
201 """Get current prior (mean, covariance, confidence)"""
202 return (
203 self.prior_mean.copy(),
204 self.prior_covariance.copy(),
205 self._compute_confidence(),
206 )
207
208 def predict_future_drift(
209 self, time_horizon: float
210 ) -> Tuple[np.ndarray, np.ndarray]:
211 """
212 Predict how parameters will drift over time horizon.
213
214 Returns: (predicted_mean, predicted_covariance)
215 """
216 drift_rate = self.drift_detector.estimate_drift_rate()
217
218 # Drift increases uncertainty
219 drift_variance = (drift_rate * time_horizon) ** 2 * np.eye(self.n_params)
220
221 # Mean stays the same (unbiased drift), covariance increases
222 predicted_mean = self.prior_mean.copy()
223 predicted_cov = self.prior_covariance + drift_variance
224
225 return predicted_mean, predicted_cov
226
227
228class DriftDetector:
229 """Detect and quantify parameter drift over time"""
230
231 def __init__(self, n_params: int, window_size: int = 50):
232 self.n_params = n_params
233 self.window_size = window_size
234 self.parameter_history: deque = deque(maxlen=window_size)
235 self.timestamp_history: deque = deque(maxlen=window_size)
236
237 def add_observation(self, parameters: np.ndarray, timestamp: float):
238 """Add parameter observation"""
239 self.parameter_history.append(parameters.copy())
240 self.timestamp_history.append(timestamp)
241
242 def estimate_drift_rate(self) -> float:
243 """
244 Estimate rate of parameter drift (norm of change per second).
245
246 Returns: drift_rate in parameter units per second
247 """
248 if len(self.parameter_history) < 2:
249 return 0.0
250
251 params = np.array(list(self.parameter_history))
252 times = np.array(list(self.timestamp_history))
253
254 # Compute rate of change via finite differences
255 dparams = np.diff(params, axis=0)
256 dtimes = np.diff(times)
257
258 # Avoid division by zero
259 dtimes = np.maximum(dtimes, 1e-6)
260
261 # Rate of change (per second)
262 rates = np.linalg.norm(dparams, axis=1) / dtimes
263
264 # Robust estimate: median rate
265 drift_rate = float(np.median(rates))
266
267 return drift_rate
268
269 def detect_abrupt_change(self, threshold: float = 3.0) -> bool:
270 """Detect abrupt parameter change (e.g., harness swap)"""
271 if len(self.parameter_history) < 10:
272 return False
273
274 params = np.array(list(self.parameter_history))
275
276 # Recent mean vs historical mean
277 recent_mean = np.mean(params[-5:], axis=0)
278 historical_mean = np.mean(params[:-5], axis=0)
279
280 # Difference normalized by standard deviation
281 std = np.std(params, axis=0) + 1e-6
282 normalized_diff = np.linalg.norm((recent_mean - historical_mean) / std)
283
284 return normalized_diff > threshold
285
286
287# ===========================================================================================
288# ACTIVE LEARNING: SYSTEM REQUESTS CALIBRATION
289# ===========================================================================================
290
291
292class ActiveLearningAgent:
293 """
294 Intelligent agent that decides when and where calibration is needed.
295 Uses uncertainty sampling and query-by-committee strategies.
296 """
297
298 def __init__(self, n_sensors: int, n_params: int):
299 self.n_sensors = n_sensors
300 self.n_params = n_params
301
302 # Track when each sensor was last calibrated
303 self.last_calibration_time: Dict[int, float] = {}
304
305 # Track prediction errors
306 self.error_history: Dict[int, deque] = {
307 i: deque(maxlen=50) for i in range(n_sensors)
308 }
309
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
314
315 logger.info(f"Active learning agent initialized: {n_sensors} sensors")
316
317 def should_request_calibration(
318 self,
319 sensor_id: int,
320 prediction: float,
321 uncertainty: float,
322 voltage_or_adc: float,
323 *,
324 is_adc_code: bool = False,
325 ) -> Optional[CalibrationRequest]:
326 """
327 Decide if calibration should be requested for this sensor.
328
329 Returns: CalibrationRequest if calibration needed, None otherwise
330 """
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()
335
336 # Check if budget exhausted
337 if self.requests_this_hour >= self.calibration_budget:
338 return None
339
340 reasons = []
341 urgency = 0.0
342
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)
347
348 # Reason 2: Extrapolation (voltage/ADC outside calibrated range)
349 if is_adc_code:
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)
354 else:
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)
358
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))
365 else:
366 reasons.append("never calibrated")
367 urgency = max(urgency, 0.9)
368
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))
376
377 # Make request if urgency is high enough
378 if urgency > 0.5 and reasons:
379 self.requests_this_hour += 1
380
381 # Suggest pressure range based on prediction
382 suggested_range = (
383 max(0, prediction - 2 * uncertainty),
384 prediction + 2 * uncertainty,
385 )
386
387 return CalibrationRequest(
388 sensor_id=sensor_id,
389 voltage=voltage_or_adc,
390 reason="; ".join(reasons),
391 urgency=float(urgency),
392 suggested_pressure_range=suggested_range,
393 confidence=1.0 - urgency,
394 )
395
396 return None
397
398 def record_calibration(self, sensor_id: int):
399 """Record that sensor was calibrated"""
400 self.last_calibration_time[sensor_id] = time.time()
401
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)
406
407
408# ===========================================================================================
409# INTELLIGENT ONLINE LEARNING
410# ===========================================================================================
411
412
413class OnlineBayesianLearner:
414 """
415 Online Bayesian learning with recursive updates.
416 Continuously improves calibration as new data arrives.
417 """
418
419 def __init__(self, sensor_id: int, prior_mean: np.ndarray, prior_cov: np.ndarray):
420 self.sensor_id = sensor_id
421 self.posterior_mean = prior_mean.copy()
422 self.posterior_cov = prior_cov.copy()
423
424 # Cholesky factorization for efficient updates
425 try:
426 self.chol_factor = cho_factor(prior_cov, lower=True)
427 except:
428 self.chol_factor = None
429
430 # Track data for quality assessment
431 self.n_observations = 0
432 self.residual_history: deque = deque(maxlen=100)
433
434 def recursive_update(
435 self, design_vector: np.ndarray, observation: float, observation_variance: float
436 ) -> Tuple[np.ndarray, np.ndarray]:
437 """
438 Recursive Bayesian update (Kalman filter form).
439
440 Much more efficient than recomputing from scratch.
441
442 Args:
443 design_vector: φ(v, e) - basis functions
444 observation: Measured pressure
445 observation_variance: σ² - measurement uncertainty
446
447 Returns: (updated_mean, updated_covariance)
448 """
449 # Innovation (prediction error)
450 prediction = np.dot(design_vector, self.posterior_mean)
451 innovation = observation - prediction
452
453 # Innovation variance
454 S = (
455 np.dot(design_vector, np.dot(self.posterior_cov, design_vector))
456 + observation_variance
457 )
458 S = max(S, 1e-6) # Numerical stability
459
460 # Kalman gain
461 K = np.dot(self.posterior_cov, design_vector) / S
462
463 # Update
464 self.posterior_mean = self.posterior_mean + K * innovation
465
466 # Joseph form for numerical stability
467 I_KH = np.eye(len(self.posterior_mean)) - np.outer(K, design_vector)
468 self.posterior_cov = np.dot(
469 I_KH, np.dot(self.posterior_cov, I_KH.T)
470 ) + observation_variance * np.outer(K, K)
471
472 # Symmetrize
473 self.posterior_cov = 0.5 * (self.posterior_cov + self.posterior_cov.T)
474
475 # Record
476 self.n_observations += 1
477 self.residual_history.append(innovation / np.sqrt(S)) # Normalized residual
478
479 # Update Cholesky factorization if possible
480 try:
481 self.chol_factor = cho_factor(self.posterior_cov, lower=True)
482 except:
483 self.chol_factor = None
484
485 return self.posterior_mean.copy(), self.posterior_cov.copy()
486
487 def predict_with_uncertainty(
488 self, design_vector: np.ndarray
489 ) -> Tuple[float, float]:
490 """Fast prediction with uncertainty"""
491 mean = float(np.dot(design_vector, self.posterior_mean))
492 variance = float(
493 np.dot(design_vector, np.dot(self.posterior_cov, design_vector))
494 )
495 uncertainty = np.sqrt(max(variance, 0))
496 return mean, uncertainty
497
498 def compute_quality_score(self) -> float:
499 """
500 Compute calibration quality score (0-1).
501 Based on normalized residuals and sample size.
502 """
503 if self.n_observations == 0:
504 return 0.0
505
506 # Sample size factor
507 sample_factor = np.tanh(self.n_observations / 10.0)
508
509 # Residual factor: Good calibration has residuals ~ N(0,1)
510 if len(self.residual_history) > 0:
511 residuals = np.array(list(self.residual_history))
512 # Chi-squared test: sum of squared normalized residuals
513 chi2_stat = np.sum(residuals**2)
514 expected_chi2 = len(residuals)
515 residual_factor = np.exp(
516 -abs(chi2_stat - expected_chi2) / (2 * expected_chi2)
517 )
518 else:
519 residual_factor = 0.5
520
521 quality = 0.6 * sample_factor + 0.4 * residual_factor
522 return float(np.clip(quality, 0, 1))
523
524
525# ===========================================================================================
526# AUTONOMOUS CALIBRATION ENGINE
527# ===========================================================================================
528
529
530class AutonomousCalibrationEngine:
531 """
532 Main autonomous calibration engine integrating all components.
533
534 Features:
535 - Learns and evolves priors from all sensors
536 - Requests calibration when needed
537 - Adapts to drift and changes
538 - Provides confidence-aware predictions
539 """
540
541 def __init__(
542 self, n_sensors: int = 16, n_params: int = 9, forgetting_factor: float = 0.995
543 ):
544 self.n_sensors = n_sensors
545 self.n_params = n_params
546
547 # Adaptive prior evolution
548 self.prior_evolution = AdaptivePriorEvolution(n_params, forgetting_factor)
549
550 # Active learning agent
551 self.active_learner = ActiveLearningAgent(n_sensors, n_params)
552
553 # Online learners for each sensor
554 prior_mean, prior_cov, _ = self.prior_evolution.get_prior()
555 self.online_learners: Dict[int, OnlineBayesianLearner] = {
556 i: OnlineBayesianLearner(i, prior_mean, prior_cov) for i in range(n_sensors)
557 }
558
559 # Statistics
560 self.total_calibrations = 0
561 self.autonomous_requests = 0
562 self.start_time = time.time()
563
564 logger.info(
565 f"🤖 Autonomous Calibration Engine initialized: "
566 f"{n_sensors} sensors, {n_params} parameters"
567 )
568
569 def add_calibration_point(
570 self,
571 sensor_id: int,
572 design_vector: np.ndarray,
573 pressure: float,
574 uncertainty: float = 0.05,
575 ):
576 """
577 Add calibration point and update system.
578
579 This is called when user provides a pressure reading.
580 The system learns from this and improves its prior.
581 """
582 if sensor_id not in self.online_learners:
583 logger.error(f"Invalid sensor_id: {sensor_id}")
584 return
585
586 # Online update for this sensor
587 learner = self.online_learners[sensor_id]
588 posterior_mean, posterior_cov = learner.recursive_update(
589 design_vector, pressure, uncertainty**2
590 )
591
592 # Quality of this sensor's calibration
593 quality = learner.compute_quality_score()
594
595 # Update population prior from this sensor's posterior
596 self.prior_evolution.update_from_sensor(
597 sensor_id, posterior_mean, posterior_cov, quality
598 )
599
600 # Record calibration for active learning
601 self.active_learner.record_calibration(sensor_id)
602
603 self.total_calibrations += 1
604
605 # Propagate improved prior to all sensors
606 self._propagate_prior_to_sensors()
607
608 logger.info(
609 f"✅ PT{sensor_id} calibration added: quality={quality:.3f}, "
610 f"total_cal={self.total_calibrations}"
611 )
612
613 def _propagate_prior_to_sensors(self):
614 """
615 Propagate improved population prior to all sensors.
616 Each sensor blends global prior with its own data.
617 """
618 prior_mean, prior_cov, prior_confidence = self.prior_evolution.get_prior()
619
620 for sensor_id, learner in self.online_learners.items():
621 if learner.n_observations == 0:
622 # No data: use pure prior
623 learner.posterior_mean = prior_mean.copy()
624 learner.posterior_cov = prior_cov.copy()
625 else:
626 # Has data: blend prior with posterior
627 # More observations → less influence from prior
628 blend = 1.0 / (1.0 + learner.n_observations / 5.0)
629 learner.posterior_mean = (
630 1 - blend
631 ) * learner.posterior_mean + blend * prior_mean
632 # Covariance doesn't blend (stays as computed)
633
634 def predict(
635 self,
636 sensor_id: int,
637 design_vector: np.ndarray,
638 adc_code: Optional[float] = None,
639 ) -> Tuple[float, float, Optional[CalibrationRequest]]:
640 """
641 Predict pressure with uncertainty and possibly request calibration.
642
643 Returns: (pressure, uncertainty, calibration_request)
644 """
645 if sensor_id not in self.online_learners:
646 return 0.0, 1000.0, None
647
648 learner = self.online_learners[sensor_id]
649 pressure, uncertainty = learner.predict_with_uncertainty(design_vector)
650
651 # Check if calibration should be requested
652 if adc_code is not None:
653 calibration_request = self.active_learner.should_request_calibration(
654 sensor_id, pressure, uncertainty, adc_code, is_adc_code=True
655 )
656 else:
657 voltage = design_vector[1] if len(design_vector) > 1 else 0.0
658 calibration_request = self.active_learner.should_request_calibration(
659 sensor_id, pressure, uncertainty, voltage
660 )
661
662 return pressure, uncertainty, calibration_request
663
664 def get_system_status(self) -> Dict:
665 """Get comprehensive system status"""
666 _, _, prior_confidence = self.prior_evolution.get_prior()
667
668 # Sensor qualities
669 sensor_qualities = {
670 i: learner.compute_quality_score()
671 for i, learner in self.online_learners.items()
672 }
673
674 avg_quality = np.mean(list(sensor_qualities.values()))
675
676 # Drift info
677 drift_rate = self.prior_evolution.drift_detector.estimate_drift_rate()
678
679 uptime = time.time() - self.start_time
680
681 return {
682 "uptime_seconds": uptime,
683 "total_calibrations": self.total_calibrations,
684 "autonomous_requests": self.autonomous_requests,
685 "prior_confidence": prior_confidence,
686 "average_sensor_quality": avg_quality,
687 "drift_rate": drift_rate,
688 "effective_sample_size": self.prior_evolution.effective_sample_size,
689 "sensor_qualities": sensor_qualities,
690 }
691
692 def export_learned_prior(self) -> Dict:
693 """Export learned prior for saving"""
694 prior_mean, prior_cov, confidence = self.prior_evolution.get_prior()
695 return {
696 "prior_mean": prior_mean.tolist(),
697 "prior_covariance": prior_cov.tolist(),
698 "confidence": float(confidence),
699 "effective_sample_size": float(self.prior_evolution.effective_sample_size),
700 "update_count": self.prior_evolution.update_count,
701 }
702
703 def import_learned_prior(self, data: Dict):
704 """Import previously learned prior"""
705 try:
706 self.prior_evolution.prior_mean = np.array(data["prior_mean"])
707 self.prior_evolution.prior_covariance = np.array(data["prior_covariance"])
708 self.prior_evolution.effective_sample_size = float(
709 data.get("effective_sample_size", 1.0)
710 )
711 self.prior_evolution.update_count = int(data.get("update_count", 0))
712 logger.info(
713 f"✅ Imported learned prior: ESS={self.prior_evolution.effective_sample_size:.1f}"
714 )
715 except Exception as e:
716 logger.error(f"Failed to import learned prior: {e}")
717
718
719if __name__ == "__main__":
720 # Demo usage
721 logging.basicConfig(level=logging.INFO, format="%(levelname)s - %(message)s")
722
723 # Create engine
724 engine = AutonomousCalibrationEngine(n_sensors=16, n_params=9)
725
726 # Simulate calibration points
727 for i in range(5):
728 sensor_id = i % 16
729 voltage = 2.5 + i * 0.5
730 design_vector = np.array([1, voltage] + [0] * 7) # Simplified
731 pressure = 200 * voltage + np.random.normal(0, 1)
732
733 engine.add_calibration_point(sensor_id, design_vector, pressure)
734
735 # Make predictions
736 for sensor_id in range(3):
737 voltage = 3.0
738 design_vector = np.array([1, voltage] + [0] * 7)
739 p, u, req = engine.predict(sensor_id, design_vector)
740 print(f"\nPT{sensor_id}: {p:.1f} ± {u:.1f} PSI")
741 if req:
742 print(
743 f" 📝 Calibration requested: {req.reason} (urgency: {req.urgency:.2f})"
744 )
745
746 # System status
747 status = engine.get_system_status()
748 print(f"\nSystem Status:")
749 print(f" Prior confidence: {status['prior_confidence']:.3f}")
750 print(f" Average quality: {status['average_sensor_quality']:.3f}")
751 print(f" Drift rate: {status['drift_rate']:.2e}")
update_from_sensor(self, int sensor_id, np.ndarray posterior_mean, np.ndarray posterior_cov, float quality)
__init__(self, int n_params, float forgetting_factor=0.995)
TOML_NODISCARD_CTOR time() noexcept=default