3Calibration Robustness Module
4Provides mission-critical robustness features for the pressure transducer calibration system.
7- Automatic backup and recovery
8- Flight mode vs test mode control
9- Comprehensive validation and sanity checks
10- Health monitoring and diagnostics
11- Anomaly detection and automatic recovery
12- Redundancy and fallback mechanisms
21from datetime
import datetime
22from pathlib
import Path
23from typing
import Dict, List, Tuple, Optional, Any
24from dataclasses
import dataclass, asdict
26from collections
import deque
29logger = logging.getLogger(__name__)
37 """System operation modes"""
40 CALIBRATION =
"calibration"
47 """Mission-critical system configuration"""
49 mode: OperationMode = OperationMode.TEST
50 consensus_enabled: bool =
True
51 auto_backup_enabled: bool =
True
52 backup_interval_seconds: float = 60.0
53 max_backup_files: int = 10
54 validation_enabled: bool =
True
55 health_monitoring_enabled: bool =
True
56 anomaly_detection_enabled: bool =
True
59 max_voltage: float = 10.0
60 min_voltage: float = 0.0
61 max_pressure: float = 1500.0
62 min_pressure: float = -50.0
63 max_uncertainty: float = 500.0
64 consensus_agreement_threshold: float = 0.6
65 min_sensors_for_consensus: int = 2
66 backup_directory: str =
"./calibration_backups"
67 log_directory: str =
"./calibration_logs"
70 """Convert to dictionary for JSON serialization"""
72 d[
"mode"] = self.
mode.value
77 """Load from dictionary"""
78 if "mode" in d
and isinstance(d[
"mode"], str):
82 def save(self, filepath: str =
"system_config.json"):
83 """Save configuration to file"""
84 with open(filepath,
"w")
as f:
85 json.dump(self.
to_dict(), f, indent=2)
86 logger.info(f
"System configuration saved to {filepath}")
89 def load(cls, filepath: str =
"system_config.json") ->
"SystemConfig":
90 """Load configuration from file"""
91 if not os.path.exists(filepath):
92 logger.warning(f
"Config file {filepath} not found, using defaults")
95 with open(filepath,
"r")
as f:
99 f
"System configuration loaded from {filepath}: mode={config.mode.value}"
102 except Exception
as e:
103 logger.error(f
"Failed to load config from {filepath}: {e}, using defaults")
113 """Automatic backup and recovery for calibration data"""
118 self.
backup_dir.mkdir(parents=
True, exist_ok=
True)
121 logger.info(f
"Backup manager initialized: {self.backup_dir}")
124 """Check if it's time for automatic backup"""
125 if not self.
config.auto_backup_enabled:
129 ) >= self.
config.backup_interval_seconds
133 population_prior: Dict,
134 pt_states: Dict[int, Dict],
135 metadata: Optional[Dict] =
None,
138 Backup complete calibration state
141 population_prior: Population prior data (mean, covariance, strength)
142 pt_states: Per-PT calibration states
143 metadata: Optional metadata (timestamp, mode, etc.)
146 True if backup successful
150 timestamp = datetime.now().strftime(
"%Y%m%d_%H%M%S")
151 backup_file = self.
backup_dir / f
"calibration_backup_{timestamp}.json"
155 "datetime": timestamp,
156 "mode": self.
config.mode.value,
157 "population_prior": population_prior,
158 "pt_states": pt_states,
159 "metadata": metadata
or {},
163 temp_file = backup_file.with_suffix(
".tmp")
164 with open(temp_file,
"w")
as f:
166 temp_file.rename(backup_file)
169 logger.info(f
"✅ Calibration backup saved: {backup_file}")
175 except Exception
as e:
176 logger.error(f
"❌ Backup failed: {e}")
180 """Custom JSON serializer for numpy arrays"""
181 if isinstance(obj, np.ndarray):
183 if isinstance(obj, np.integer):
185 if isinstance(obj, np.floating):
187 raise TypeError(f
"Object of type {type(obj)} is not JSON serializable")
190 """Keep only the most recent N backup files"""
192 backup_files = sorted(self.
backup_dir.glob(
"calibration_backup_*.json"))
193 if len(backup_files) > self.
config.max_backup_files:
194 files_to_delete = backup_files[: -self.
config.max_backup_files]
195 for f
in files_to_delete:
197 logger.info(f
"Deleted old backup: {f}")
198 except Exception
as e:
199 logger.error(f
"Backup cleanup failed: {e}")
202 """List available backups with timestamps"""
204 for backup_file
in sorted(self.
backup_dir.glob(
"calibration_backup_*.json")):
206 with open(backup_file,
"r")
as f:
208 timestamp = data.get(
"timestamp", 0.0)
209 backups.append((backup_file, timestamp))
212 return sorted(backups, key=
lambda x: x[1], reverse=
True)
215 """Restore from the most recent backup"""
218 logger.warning(
"No backups available for restore")
221 latest_backup, _ = backups[0]
226 Restore calibration state from backup
229 Backup data dictionary or None if failed
232 with open(backup_file,
"r")
as f:
236 if "population_prior" in data:
237 pp = data[
"population_prior"]
238 if "population_mean" in pp
and isinstance(pp[
"population_mean"], list):
239 pp[
"population_mean"] = np.array(pp[
"population_mean"])
240 if "population_covariance" in pp
and isinstance(
241 pp[
"population_covariance"], list
243 pp[
"population_covariance"] = np.array(pp[
"population_covariance"])
245 logger.info(f
"✅ Restored calibration from backup: {backup_file}")
247 except Exception
as e:
248 logger.error(f
"❌ Restore failed: {e}")
258 """Result of a validation check"""
260 def __init__(self, valid: bool, message: str =
"", severity: str =
"info"):
270 """Comprehensive validation and sanity checks"""
276 """Validate voltage reading"""
277 if not self.
config.validation_enabled:
280 if not np.isfinite(voltage):
282 False, f
"PT{sensor_id}: Non-finite voltage {voltage}",
"error"
285 if voltage < self.
config.min_voltage
or voltage > self.
config.max_voltage:
288 f
"PT{sensor_id}: Voltage {voltage:.3f}V out of range [{self.config.min_voltage}, {self.config.max_voltage}]",
295 """Validate pressure reading"""
296 if not self.
config.validation_enabled:
299 if not np.isfinite(pressure):
301 False, f
"PT{sensor_id}: Non-finite pressure {pressure}",
"error"
304 if pressure < self.
config.min_pressure
or pressure > self.
config.max_pressure:
307 f
"PT{sensor_id}: Pressure {pressure:.1f} PSI out of range [{self.config.min_pressure}, {self.config.max_pressure}]",
314 self, uncertainty: float, sensor_id: int
315 ) -> ValidationResult:
316 """Validate uncertainty value"""
317 if not self.
config.validation_enabled:
320 if not np.isfinite(uncertainty)
or uncertainty < 0:
322 False, f
"PT{sensor_id}: Invalid uncertainty {uncertainty}",
"error"
325 if uncertainty > self.
config.max_uncertainty:
328 f
"PT{sensor_id}: Uncertainty {uncertainty:.1f} PSI exceeds maximum {self.config.max_uncertainty}",
335 self, coefficients: np.ndarray, sensor_id: int
336 ) -> ValidationResult:
337 """Validate calibration coefficients"""
338 if not self.
config.validation_enabled:
341 if coefficients
is None:
343 False, f
"PT{sensor_id}: Coefficients are None",
"error"
346 expected_size = self.
config.model_order + 1
347 if len(coefficients) != expected_size:
350 f
"PT{sensor_id}: Expected {expected_size} coefficients, got {len(coefficients)}",
354 if not np.all(np.isfinite(coefficients)):
356 False, f
"PT{sensor_id}: Non-finite coefficients",
"error"
361 linear_coeff = coefficients[1]
if len(coefficients) > 1
else 0
362 if abs(linear_coeff) > 1000.0:
365 f
"PT{sensor_id}: Linear coefficient {linear_coeff:.1f} seems unreasonable",
372 self, covariance: np.ndarray, sensor_id: int
373 ) -> ValidationResult:
374 """Validate covariance matrix"""
375 if not self.
config.validation_enabled:
378 if covariance
is None:
380 False, f
"PT{sensor_id}: Covariance is None",
"error"
383 expected_shape = (self.
config.model_order + 1, self.
config.model_order + 1)
384 if covariance.shape != expected_shape:
387 f
"PT{sensor_id}: Expected covariance shape {expected_shape}, got {covariance.shape}",
391 if not np.all(np.isfinite(covariance)):
393 False, f
"PT{sensor_id}: Non-finite covariance",
"error"
398 eigenvalues = np.linalg.eigvalsh(covariance)
399 if np.any(eigenvalues <= 0):
402 f
"PT{sensor_id}: Covariance not positive definite (min eigenvalue: {np.min(eigenvalues)})",
405 except np.linalg.LinAlgError:
407 False, f
"PT{sensor_id}: Covariance matrix computation failed",
"error"
413 self, consensus_pressure: float, agreement_score: float, num_sensors: int
414 ) -> ValidationResult:
415 """Validate consensus state"""
416 if not self.
config.validation_enabled:
419 if not np.isfinite(consensus_pressure):
422 if not (0 <= agreement_score <= 1):
424 False, f
"Agreement score {agreement_score} out of [0,1]",
"warning"
427 if num_sensors < self.
config.min_sensors_for_consensus:
430 f
"Only {num_sensors} sensors participating, need {self.config.min_sensors_for_consensus}",
434 if agreement_score < self.
config.consensus_agreement_threshold:
437 f
"Agreement score {agreement_score:.3f} below threshold {self.config.consensus_agreement_threshold}",
451 """System health metrics"""
456 calibrated_sensors: int
457 consensus_confidence: float
458 average_uncertainty: float
459 max_uncertainty: float
460 drift_detected_count: int
461 validation_errors: int
463 uptime_seconds: float
467 """Real-time health monitoring and diagnostics"""
479 log_dir = Path(config.log_directory)
480 log_dir.mkdir(parents=
True, exist_ok=
True)
483 log_file = log_dir / f
"health_log_{datetime.now().strftime('%Y%m%d')}.log"
484 file_handler = logging.FileHandler(log_file)
485 file_handler.setLevel(logging.INFO)
486 formatter = logging.Formatter(
487 "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
489 file_handler.setFormatter(formatter)
490 logger.addHandler(file_handler)
492 logger.info(f
"Health monitor initialized: {log_file}")
495 """Record health metrics"""
503 """Get current system status"""
506 return {
"status":
"no_data"}
510 "status":
"healthy" if self.
is_healthy()
else "degraded",
511 "mode": latest.mode.value,
512 "active_sensors": latest.active_sensors,
513 "calibrated_sensors": latest.calibrated_sensors,
514 "consensus_confidence": latest.consensus_confidence,
515 "average_uncertainty": latest.average_uncertainty,
516 "max_uncertainty": latest.max_uncertainty,
517 "drift_detected": latest.drift_detected_count,
525 """Check if system is healthy"""
532 if latest.calibrated_sensors == 0:
535 if latest.mode == OperationMode.FLIGHT
and latest.consensus_confidence < 0.5:
536 logger.warning(
"Low consensus confidence in flight mode")
539 if latest.max_uncertainty > self.
config.max_uncertainty:
541 f
"Maximum uncertainty {latest.max_uncertainty} exceeds limit"
546 logger.error(
"Too many errors detected")
552 """Log validation error"""
553 self.validation_errors += 1
554 if result.severity == "warning":
555 self.warning_count += 1
556 logger.warning(result.message)
557 elif result.severity in ("error", "critical"):
558 self.error_count += 1
559 logger.error(result.message)
561 def get_diagnostics_report(self) -> str:
562 """Generate comprehensive diagnostics report"""
563 status = self.get_current_status()
567 "CALIBRATION SYSTEM DIAGNOSTICS REPORT",
569 f"Timestamp: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
570 f"Mode: {status['mode']}",
571 f"Status: {status['status'].upper()}",
572 f"Uptime: {status['uptime_seconds']:.1f} seconds",
575 f" Active: {status['active_sensors']}/{self.config.num_sensors}",
576 f" Calibrated: {status['calibrated_sensors']}/{self.config.num_sensors}",
578 "CALIBRATION QUALITY:",
579 f" Average Uncertainty: {status['average_uncertainty']:.2f} PSI",
580 f" Maximum Uncertainty: {status['max_uncertainty']:.2f} PSI",
581 f" Consensus Confidence: {status['consensus_confidence']:.3f}",
583 "ERRORS AND WARNINGS:",
584 f" Validation Errors: {status['validation_errors']}",
585 f" Warnings: {status['warnings']}",
586 f" Errors: {status['errors']}",
587 f" Drift Detected: {status['drift_detected']} sensors",
592 return "\n".join(report)
595# ===========================================================================================
597# ===========================================================================================
600class AnomalyDetector:
601 """Detect anomalies in calibration and sensor readings"""
603 def __init__(self, config: SystemConfig):
605 self.voltage_history: Dict[int, deque] = {
606 i: deque(maxlen=100) for i in range(config.num_sensors)
608 self.pressure_history: Dict[int, deque] = {
609 i: deque(maxlen=100) for i in range(config.num_sensors)
611 self.anomaly_counts: Dict[int, int] = {i: 0 for i in range(config.num_sensors)}
613 def detect_voltage_anomaly(self, sensor_id: int, voltage: float) -> bool:
614 """Detect voltage anomaly using statistical methods"""
615 if sensor_id not in self.voltage_history:
618 history = self.voltage_history[sensor_id]
619 history.append(voltage)
621 if len(history) < 10: # Need some history
624 # Simple outlier detection: > 3 sigma from mean
625 mean = np.mean(history)
626 std = np.std(history)
628 if std < 1e-6: # No variation
631 z_score = abs((voltage - mean) / std)
633 self.anomaly_counts[sensor_id] += 1
635 f"PT{sensor_id} voltage anomaly: {voltage:.3f}V (z-score: {z_score:.2f})"
641 def detect_pressure_anomaly(self, sensor_id: int, pressure: float) -> bool:
642 """Detect pressure anomaly"""
643 if sensor_id not in self.pressure_history:
646 history = self.pressure_history[sensor_id]
647 history.append(pressure)
649 if len(history) < 10:
652 # Check for rapid changes (rate limit)
653 if len(history) >= 2:
654 rate_of_change = abs(pressure - history[-2])
655 if rate_of_change > 100.0: # > 100 PSI change in one sample
657 f"PT{sensor_id} rapid pressure change: {rate_of_change:.1f} PSI/sample"
663 def get_anomaly_report(self) -> Dict[int, int]:
664 """Get anomaly counts per senso
r"""
665 return self.anomaly_counts.copy()
668# ===========================================================================================
669# MAIN ROBUSTNESS MANAGER
670# ===========================================================================================
673class RobustnessManager:
675 Main robustness manager integrating all features.
678 config = SystemConfig.load()
679 manager = RobustnessManager(config)
681 # Check if backup needed
682 if manager.backup_manager.should_backup():
683 manager.backup_calibration_state(...)
686 result = manager.validator.validate_voltage(voltage, sensor_id)
688 manager.health_monitor.log_validation_error(result)
691 print(manager.health_monitor.get_diagnostics_report())
694 def __init__(self, config: Optional[SystemConfig] = None):
695 self.config = config or SystemConfig.load()
696 self.config.save() # Save for future reference
698 self.backup_manager = BackupManager(self.config)
699 self.validator = CalibrationValidator(self.config)
700 self.health_monitor = HealthMonitor(self.config)
701 self.anomaly_detector = AnomalyDetector(self.config)
704 f"🛡️ Robustness Manager initialized: mode={self.config.mode.value}, "
705 f"consensus={self.config.consensus_enabled}, validation={self.config.validation_enabled}"
708 def set_mode(self, mode: OperationMode):
709 """Change operation mode"""
710 old_mode = self.config.mode
711 self.config.mode = mode
713 # Update consensus based on mode
714 if mode == OperationMode.FLIGHT:
715 self.config.consensus_enabled = False
717 "⚠️ FLIGHT MODE: Consensus disabled, measurements are independent"
719 elif mode in (OperationMode.TEST, OperationMode.CALIBRATION):
720 self.config.consensus_enabled = True
721 logger.info(f"✅ {mode.value.upper()} MODE: Consensus enabled")
724 logger.info(f"Mode changed: {old_mode.value} → {mode.value}")
726 def emergency_backup(self, data: Dict) -> bool:
727 """Emergency backup (ignore interval)"""
728 return self.backup_manager.backup_calibration_state(
729 population_prior=data.get("population_prior", {}),
730 pt_states=data.get("pt_states", {}),
731 metadata={"emergency": True},
734 def auto_recover(self) -> Optional[Dict]:
735 """Automatic recovery from latest backup"""
736 logger.warning("🚨 Attempting automatic recovery...")
737 data = self.backup_manager.restore_latest_backup()
739 logger.info("✅ Recovery successful")
741 logger.error("❌ Recovery failed")
745if __name__ == "__main__":
748 level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s"
751 # Create configuration
752 config = SystemConfig()
753 config.mode = OperationMode.TEST
757 manager = RobustnessManager(config)
759 # Simulate some validations
760 v_result = manager.validator.validate_voltage(2.5, 0)
761 print(f"Voltage validation: {v_result.valid} - {v_result.message}")
763 p_result = manager.validator.validate_pressure(500.0, 0)
764 print(f"Pressure validation: {p_result.valid} - {p_result.message}")
766 # Generate diagnostics
767 print("\n" + manager.health_monitor.get_diagnostics_report())
769 # Change to flight mode
770 manager.set_mode(OperationMode.FLIGHT)
771 print(f"\nConsensus enabled: {manager.config.consensus_enabled}")
_cleanup_old_backups(self)
__init__(self, SystemConfig config)
Optional[Dict] restore_from_backup(self, Path backup_file)
bool backup_calibration_state(self, Dict population_prior, Dict[int, Dict] pt_states, Optional[Dict] metadata=None)
_json_serializer(self, obj)
List[Tuple[Path, float]] list_backups(self)
Optional[Dict] restore_latest_backup(self)
__init__(self, SystemConfig config)
ValidationResult validate_voltage(self, float voltage, int sensor_id)
ValidationResult validate_pressure(self, float pressure, int sensor_id)
ValidationResult validate_uncertainty(self, float uncertainty, int sensor_id)
ValidationResult validate_consensus_state(self, float consensus_pressure, float agreement_score, int num_sensors)
ValidationResult validate_covariance_matrix(self, np.ndarray covariance, int sensor_id)
ValidationResult validate_calibration_coefficients(self, np.ndarray coefficients, int sensor_id)
Dict[str, Any] get_current_status(self)
__init__(self, SystemConfig config)
record_metrics(self, HealthMetrics metrics)
log_validation_error(self, ValidationResult result)
save(self, str filepath="system_config.json")
"SystemConfig" load(cls, str filepath="system_config.json")
"SystemConfig" from_dict(cls, Dict d)
__init__(self, bool valid, str message="", str severity="info")
TOML_NODISCARD_CTOR time() noexcept=default