Skip to content

Sensor Degradation Detection & Health Monitoring for 24/7 Airside Operations

Automated Self-Diagnostics for LiDAR, Radar, Camera, and Thermal Sensors

Last updated: 2026-04-11


Summary: Airport airside autonomous vehicles operate 16-20 hours/day in harsh conditions — jet exhaust, de-icing chemicals, hydraulic fluid mist, UV exposure, vibration from uneven tarmac, temperature cycles from -10°C to +50°C. Sensor degradation is not hypothetical: LiDAR lens contamination from bug splatter, de-icing residue, and tarmac grime reduces point cloud integrity by up to 75%. 4D radar radome ice accumulation or chemical film buildup degrades detection range by 15-30%. Thermal camera NUC (non-uniformity correction) drift causes false detection patterns after 100+ hours without recalibration. Without automated health monitoring, degraded sensors produce confident but incorrect detections — more dangerous than sensor failure, which at least triggers a known-safe fallback. This document covers self-diagnostic methods for each sensor modality, cross-sensor consistency checking, fleet-level degradation pattern mining, automated maintenance scheduling, and integration with the Simplex safety architecture. The key finding: a lightweight sensor health monitor running at 1 Hz (total <2ms on Orin) can detect 90%+ of degradation modes before they impact perception accuracy, using a combination of per-sensor statistical anomaly detection and cross-sensor consistency verification.

Use the Sensor-to-Algorithm Readiness Contract to connect these health states to the acceptance, rejection, and degraded-mode rules for perception, fusion, SLAM, localization, tracking, occupancy, mapping, and planning-facing consumers.


Table of Contents

  1. Why Sensor Health Monitoring is Critical
  2. LiDAR Degradation Modes
  3. 4D Radar Degradation Modes
  4. Camera and Thermal Degradation
  5. Per-Sensor Self-Diagnostics
  6. Cross-Sensor Consistency Checking
  7. Degradation Detection Algorithms
  8. Automated Response Actions
  9. Fleet-Level Health Analytics
  10. Maintenance Scheduling Integration
  11. Implementation Guide
  12. Key Takeaways
  13. References

1. Why Sensor Health Monitoring is Critical

1.1 The Silent Failure Problem

Sensor degradation is fundamentally different from sensor failure:

Complete FailureDegradation
DetectionTrivial (no data)Difficult (data looks plausible)
System responseImmediate fallbackContinued operation with reduced accuracy
RiskLow (known-safe state)High (confident but wrong)
ExampleLiDAR disconnectsDirty lens reduces range 40%

A dirty LiDAR lens doesn't produce an error message — it produces a point cloud with fewer points at longer ranges, potentially missing personnel at 30m while still detecting aircraft at 10m. The perception model outputs detections with normal confidence scores because it was never trained on degraded inputs.

1.2 Airside Contamination Sources

SourceFrequencyAffected SensorsSeverity
De-icing fluid residuePer treatmentLiDAR, cameraHigh — glycol film progressively degrades
Bug/insect impactDaily (summer)LiDAR, cameraModerate — point blockage, image spots
Tarmac rubber dustContinuousAll opticalLow-moderate — gradual buildup
Hydraulic fluid mistOccasionalLiDAR, cameraHigh — rapid optics contamination
Jet exhaust sootPer departureAllLow — gradual carbon buildup
Bird droppingsOccasionalCamera, thermalHigh — immediate localized blockage
Salt spray (coastal)SeasonalRadar, metallicModerate — corrosion accelerator
Standing water splashRain eventsLiDAR, cameraTemporary — water droplets on lens
PollenSeasonalLiDAR, cameraLow — gradual accumulation
Snow/ice accumulationWinterAllHigh — progressive coverage

1.3 ISO 3691-4 Requirements

ISO 3691-4 Section 4.12 requires:

  • Sensor performance monitoring: Continuous verification that sensors operate within specification
  • Degraded mode detection: System must detect when sensor performance falls below safe operating threshold
  • Automatic response: Speed reduction or safe stop when sensor capability is insufficient for current ODD
  • Maintenance notification: Alert operators when sensor cleaning or calibration is needed

2. LiDAR Degradation Modes

2.1 Taxonomy of LiDAR Degradation

LiDAR Degradation Modes
├── Optical path contamination
│   ├── Uniform film (dust, chemical residue) → gradual range reduction
│   ├── Localized blockage (bug splat, droplet) → angular blind spots
│   └── Progressive icing → expanding blind zone from edge
├── Mechanical degradation
│   ├── Vibration-induced misalignment → inter-LiDAR calibration drift
│   ├── Motor/encoder wear → scan pattern distortion
│   └── Connector corrosion → intermittent data loss
├── Environmental interference
│   ├── Sun glare (direct sunlight) → saturation in specific angles
│   ├── Retroreflector overload → range errors near signs/markings
│   └── Multi-path from wet surfaces → ground clutter increase
└── Electronic degradation
    ├── Laser diode aging → reduced return intensity over months
    ├── Receiver sensitivity drift → gradual detection range decrease
    └── Temperature-induced timing drift → range bias at extreme temps

2.2 Quantitative Impact

Degradation ModeDetection MethodImpact on mAPTime to Onset
10% lens coveragePoint count drop-5-10%Hours-days
25% lens coverageAngular hole detection-15-25%Days
50% lens coveragePoint density anomaly-30-50%Days-weeks
75% lens coverageNear-total degradation-50-75%Weeks (no cleaning)
Calibration drift 0.1°Cross-LiDAR inconsistency-3-5%Weeks-months
Calibration drift 0.5°Ghost object detection-15-25%Impact/vibration event
Laser aging 20%Intensity statistics shift-5-10%6-12 months

3. 4D Radar Degradation Modes

3.1 Radar-Specific Issues

4D Radar Degradation Modes
├── Radome contamination
│   ├── De-icing chemical film → 5-15% range reduction
│   ├── Ice accumulation (no heater or heater failure) → 20-50% loss
│   └── Paint overspray (maintenance accident) → permanent degradation
├── Antenna degradation
│   ├── Moisture ingress → impedance mismatch → reduced gain
│   ├── Sidelobe increase from dirt → increased clutter
│   └── Mechanical deformation → beam pointing error
├── Signal processing
│   ├── Interference from other radar units → ghost targets
│   ├── Multipath from aircraft fuselage → range artifacts
│   └── Ground clutter increase from wet tarmac → detection threshold rise
└── Electronic
    ├── Transmitter power degradation → range reduction
    ├── Receiver noise floor increase → sensitivity loss
    └── Temperature-induced phase error → angular accuracy loss

3.2 Radar Health Metrics

python
class RadarHealthMetrics:
    """Monitor 4D radar health from detection statistics."""
    
    def __init__(self, expected_detections_per_frame=400):
        self.expected = expected_detections_per_frame
        self.history = []
    
    def compute(self, radar_frame):
        """Compute health metrics from a radar frame.
        
        Returns dict of health indicators, each 0.0 (degraded) to 1.0 (healthy).
        """
        detections = radar_frame['detections']
        
        metrics = {
            # Detection count relative to expected
            'detection_count_health': min(len(detections) / self.expected, 1.0),
            
            # Range distribution (should extend to max range)
            'range_health': self._range_health(detections),
            
            # SNR distribution (should be above threshold)
            'snr_health': self._snr_health(detections),
            
            # Noise floor level (should be stable)
            'noise_floor_health': self._noise_health(radar_frame.get('noise_floor', 0)),
            
            # Angular coverage (should fill FOV)
            'coverage_health': self._coverage_health(detections),
        }
        
        # Overall health: minimum of individual metrics
        metrics['overall'] = min(metrics.values())
        
        self.history.append(metrics)
        return metrics
    
    def _range_health(self, detections):
        """Check if detections extend to expected maximum range."""
        if not detections:
            return 0.0
        ranges = [d['range'] for d in detections]
        max_observed = max(ranges)
        expected_max = 200.0  # meters
        return min(max_observed / expected_max, 1.0)
    
    def _snr_health(self, detections):
        """Check SNR distribution."""
        if not detections:
            return 0.0
        snrs = [d['snr'] for d in detections]
        median_snr = np.median(snrs)
        expected_median = 20.0  # dB
        return min(median_snr / expected_median, 1.0)
    
    def _noise_health(self, noise_floor):
        """Check noise floor level (lower is better)."""
        max_acceptable = -80  # dBm
        if noise_floor > max_acceptable + 10:
            return 0.0
        return 1.0 - max(0, noise_floor - max_acceptable) / 10
    
    def _coverage_health(self, detections):
        """Check angular coverage of FOV."""
        if not detections:
            return 0.0
        azimuths = [d['azimuth'] for d in detections]
        # Divide FOV into 12 sectors, check occupancy
        sectors = np.histogram(azimuths, bins=12, range=(-60, 60))[0]
        occupied = np.sum(sectors > 0)
        return occupied / 12

4. Camera and Thermal Degradation

4.1 Camera Degradation

ModeDetectionImpactFrequency
Lens water dropletsImage clarity metricsLocalized blurRain events
Lens dirt/filmContrast reductionGlobal degradationDays-weeks
Sun glare/flareOverexposure detectionAngular blind zoneDaily
Lens crack/damageEdge detection artifactsPermanentImpact event
IR filter degradationColor shiftGradual accuracy lossYears
Auto-exposure failureGlobal over/under exposureTotal failureRare

4.2 Thermal Camera Degradation

python
class ThermalHealthMonitor:
    """Monitor LWIR thermal camera health.
    
    Thermal cameras have unique degradation modes:
    - NUC (Non-Uniformity Correction) drift
    - Shutter calibration failure
    - FPA (Focal Plane Array) dead pixels
    - Narcissus effect from hot enclosure
    """
    
    def __init__(self, resolution=(640, 512)):
        self.resolution = resolution
        self.baseline_nuc = None
        self.dead_pixel_map = None
    
    def check_health(self, thermal_frame, ambient_temp):
        """Comprehensive thermal camera health check."""
        metrics = {}
        
        # 1. NUC quality: check for non-uniformity patterns
        metrics['nuc_quality'] = self._check_nuc(thermal_frame)
        
        # 2. Dead pixel count (should be stable)
        dead_count = self._count_dead_pixels(thermal_frame)
        metrics['dead_pixel_health'] = 1.0 - (dead_count / (self.resolution[0] * self.resolution[1]))
        
        # 3. Temperature range plausibility
        # Scene should have reasonable temp range based on ambient
        temp_range = thermal_frame.max() - thermal_frame.min()
        expected_range = 15.0 + abs(ambient_temp - 20) * 0.5  # Wider range in extreme temps
        metrics['temp_range_health'] = min(temp_range / expected_range, 1.0) if expected_range > 0 else 0.5
        
        # 4. Narcissus check: hot ring from enclosure
        metrics['narcissus_health'] = self._check_narcissus(thermal_frame)
        
        # 5. Response time (should detect known temp change quickly)
        metrics['response_health'] = 1.0  # Checked during periodic self-test
        
        metrics['overall'] = min(metrics.values())
        return metrics
    
    def _check_nuc(self, frame):
        """Check Non-Uniformity Correction quality.
        
        Good NUC: flat response to uniform scene
        Bad NUC: column/row stripes, fixed patterns
        """
        # Compute row and column means
        row_means = frame.mean(axis=1)
        col_means = frame.mean(axis=0)
        
        # NUC is good if row/col means have low variance
        row_var = np.var(row_means) / (np.var(frame) + 1e-7)
        col_var = np.var(col_means) / (np.var(frame) + 1e-7)
        
        # Low relative variance = good NUC
        nuc_score = 1.0 - min((row_var + col_var) / 2, 1.0)
        return nuc_score
    
    def _check_narcissus(self, frame):
        """Detect narcissus effect (hot ring from warm enclosure).
        
        Narcissus appears as elevated temperature ring near image edges.
        """
        # Compare edge temperature to center
        h, w = frame.shape
        center = frame[h//4:3*h//4, w//4:3*w//4].mean()
        edge = np.concatenate([
            frame[:h//8, :].flatten(),
            frame[-h//8:, :].flatten(),
            frame[:, :w//8].flatten(),
            frame[:, -w//8:].flatten()
        ]).mean()
        
        # Large edge-center difference suggests narcissus
        diff = abs(edge - center)
        threshold = 3.0  # degrees C
        return max(1.0 - diff / threshold, 0.0)

5. Per-Sensor Self-Diagnostics

5.1 LiDAR Self-Diagnostic Suite

python
class LiDARHealthMonitor:
    """Comprehensive LiDAR health monitoring.
    
    Runs at 1 Hz (every 10th frame at 10 Hz LiDAR).
    Total compute: <1ms on Orin.
    """
    
    def __init__(self, lidar_id, expected_points=120000, 
                 expected_range=100.0, num_beams=32):
        self.lidar_id = lidar_id
        self.expected_points = expected_points
        self.expected_range = expected_range
        self.num_beams = num_beams
        self.baseline_stats = None
        self.alert_history = []
    
    def diagnose(self, point_cloud, timestamp):
        """Run all self-diagnostics on a single LiDAR frame.
        
        Returns: health_report dict with per-metric scores.
        """
        report = {
            'timestamp': timestamp,
            'lidar_id': self.lidar_id,
        }
        
        # 1. Point count health
        report['point_count'] = len(point_cloud)
        report['point_count_ratio'] = len(point_cloud) / self.expected_points
        report['point_count_health'] = min(report['point_count_ratio'], 1.0)
        
        # 2. Range distribution health
        ranges = np.linalg.norm(point_cloud[:, :3], axis=1)
        report['max_range'] = ranges.max() if len(ranges) > 0 else 0
        report['range_health'] = min(report['max_range'] / self.expected_range, 1.0)
        
        # 3. Angular coverage (detect blocked sectors)
        report['coverage_health'] = self._angular_coverage(point_cloud)
        report['blocked_sectors'] = self._detect_blocked_sectors(point_cloud)
        
        # 4. Intensity statistics (detect lens film)
        if point_cloud.shape[1] > 3:
            intensities = point_cloud[:, 3]
            report['mean_intensity'] = intensities.mean()
            report['intensity_health'] = self._intensity_health(intensities)
        
        # 5. Near-field anomaly (detect lens contamination)
        near_mask = ranges < 2.0
        report['near_field_ratio'] = near_mask.sum() / max(len(point_cloud), 1)
        report['near_field_health'] = 1.0 if report['near_field_ratio'] < 0.05 else \
                                      max(1.0 - (report['near_field_ratio'] - 0.05) * 10, 0.0)
        
        # 6. Beam uniformity (detect individual beam failures)
        report['beam_health'] = self._beam_uniformity(point_cloud)
        
        # 7. Temporal consistency (compare with recent history)
        report['temporal_health'] = self._temporal_consistency()
        
        # Overall health
        healths = [v for k, v in report.items() if k.endswith('_health')]
        report['overall_health'] = min(healths) if healths else 0.0
        
        return report
    
    def _angular_coverage(self, points):
        """Check coverage across 360° azimuth in 30° sectors."""
        if len(points) == 0:
            return 0.0
        azimuths = np.degrees(np.arctan2(points[:, 1], points[:, 0]))
        sectors = np.histogram(azimuths, bins=12, range=(-180, 180))[0]
        # Count sectors with reasonable point count
        threshold = self.expected_points / 24  # Half of expected per sector
        covered = np.sum(sectors > threshold)
        return covered / 12
    
    def _detect_blocked_sectors(self, points):
        """Identify specific blocked angular sectors."""
        if len(points) == 0:
            return list(range(12))
        azimuths = np.degrees(np.arctan2(points[:, 1], points[:, 0]))
        sectors = np.histogram(azimuths, bins=12, range=(-180, 180))[0]
        threshold = self.expected_points / 48  # Quarter of expected
        blocked = [i for i in range(12) if sectors[i] < threshold]
        return blocked
    
    def _intensity_health(self, intensities):
        """Detect intensity anomalies suggesting lens film."""
        if self.baseline_stats is None:
            return 1.0  # No baseline yet
        
        current_mean = intensities.mean()
        baseline_mean = self.baseline_stats['intensity_mean']
        
        # Lens film reduces intensity by absorbing returns
        ratio = current_mean / (baseline_mean + 1e-7)
        if ratio > 0.8:
            return 1.0
        elif ratio > 0.5:
            return (ratio - 0.5) / 0.3
        else:
            return 0.0
    
    def _beam_uniformity(self, points):
        """Check that all laser beams are firing."""
        if len(points) == 0:
            return 0.0
        # Estimate beam from elevation angle
        elevations = np.degrees(np.arctan2(points[:, 2], 
                     np.linalg.norm(points[:, :2], axis=1)))
        beams = np.histogram(elevations, bins=self.num_beams, 
                            range=(-25, 15))[0]
        active = np.sum(beams > 10)
        return active / self.num_beams

6. Cross-Sensor Consistency Checking

6.1 Multi-Sensor Consistency Monitor

The most powerful degradation detection: compare what each sensor sees and flag inconsistencies.

python
class CrossSensorConsistencyMonitor:
    """Detect sensor degradation by cross-checking between modalities.
    
    Key insight: if LiDAR sees an object but radar doesn't (or vice versa),
    either one sensor is wrong or there's genuine disagreement.
    Systematic disagreement suggests degradation.
    """
    
    def __init__(self):
        self.disagreement_history = []
        self.alarm_threshold = 0.3  # >30% disagreement = alert
    
    def check_consistency(self, lidar_dets, radar_dets, camera_dets=None):
        """Check detection consistency across sensors.
        
        Returns: consistency_report with per-pair agreement scores.
        """
        report = {}
        
        # LiDAR vs Radar agreement
        lr_agreement = self._compute_agreement(lidar_dets, radar_dets, 
                                                distance_threshold=3.0)
        report['lidar_radar_agreement'] = lr_agreement
        
        # Specific checks:
        # 1. Objects seen by radar but not LiDAR → possible LiDAR degradation
        report['radar_only_objects'] = lr_agreement['b_only_count']
        
        # 2. Objects seen by LiDAR but not radar → normal (LiDAR better resolution)
        # BUT: large objects missed by radar → possible radar degradation
        report['lidar_only_large'] = sum(
            1 for d in lr_agreement['a_only'] 
            if d.get('size', [0,0,0])[0] > 2.0  # >2m objects should be seen by radar
        )
        
        if camera_dets:
            lc_agreement = self._compute_agreement(lidar_dets, camera_dets,
                                                    distance_threshold=2.0)
            report['lidar_camera_agreement'] = lc_agreement
        
        # Track disagreement over time
        self.disagreement_history.append(report)
        
        # Trend analysis: increasing disagreement suggests degradation
        report['disagreement_trend'] = self._compute_trend()
        
        return report
    
    def _compute_agreement(self, dets_a, dets_b, distance_threshold=3.0):
        """Compute detection agreement between two sensor modalities."""
        matched_a = set()
        matched_b = set()
        
        for i, da in enumerate(dets_a):
            for j, db in enumerate(dets_b):
                dist = np.linalg.norm(
                    np.array(da['center_xyz'][:2]) - np.array(db['center_xyz'][:2])
                )
                if dist < distance_threshold:
                    matched_a.add(i)
                    matched_b.add(j)
        
        total = max(len(dets_a) + len(dets_b), 1)
        agreement = (len(matched_a) + len(matched_b)) / total
        
        return {
            'agreement_ratio': agreement,
            'matched_count': len(matched_a),
            'a_only_count': len(dets_a) - len(matched_a),
            'b_only_count': len(dets_b) - len(matched_b),
            'a_only': [dets_a[i] for i in range(len(dets_a)) if i not in matched_a],
            'b_only': [dets_b[j] for j in range(len(dets_b)) if j not in matched_b],
        }
    
    def _compute_trend(self, window=60):
        """Compute disagreement trend over last N seconds."""
        if len(self.disagreement_history) < 2:
            return 0.0
        
        recent = self.disagreement_history[-window:]
        if len(recent) < 10:
            return 0.0
        
        # Linear regression on agreement ratio
        agreements = [r.get('lidar_radar_agreement', {}).get('agreement_ratio', 1.0) 
                     for r in recent]
        x = np.arange(len(agreements))
        slope = np.polyfit(x, agreements, 1)[0]
        
        return slope  # Negative slope = increasing disagreement = degradation

7. Degradation Detection Algorithms

7.1 AutoGrAN: Graph Attention for LiDAR Contamination

Recent work (ICPE 2024) uses graph attention networks on voxelized LiDAR data for contaminant detection:

python
class ContaminationDetector:
    """Detect LiDAR lens contamination from point cloud statistics.
    
    Simplified version of AutoGrAN approach:
    - Voxelize point cloud
    - Compare per-sector statistics with clean baseline
    - Flag contaminated sectors
    """
    
    def __init__(self, num_sectors=36, num_rings=10, max_range=100):
        self.num_sectors = num_sectors  # 10° per sector
        self.num_rings = num_rings
        self.max_range = max_range
        self.clean_baseline = None
    
    def calibrate_baseline(self, clean_frames, n_frames=100):
        """Build clean-condition baseline from n_frames of clean data."""
        sector_stats = np.zeros((self.num_sectors, self.num_rings, 4))
        # stats: [mean_count, std_count, mean_intensity, std_intensity]
        
        for frame in clean_frames[:n_frames]:
            sector_data = self._sectorize(frame)
            for s in range(self.num_sectors):
                for r in range(self.num_rings):
                    sector_stats[s, r, 0] += len(sector_data[s][r])
                    if len(sector_data[s][r]) > 0:
                        sector_stats[s, r, 2] += sector_data[s][r][:, 3].mean()
        
        sector_stats[:, :, 0] /= n_frames
        sector_stats[:, :, 2] /= n_frames
        self.clean_baseline = sector_stats
    
    def detect(self, point_cloud):
        """Detect contamination from current point cloud.
        
        Returns: contamination_map (num_sectors, num_rings) with severity 0-1
        """
        if self.clean_baseline is None:
            return None
        
        contamination = np.zeros((self.num_sectors, self.num_rings))
        sector_data = self._sectorize(point_cloud)
        
        for s in range(self.num_sectors):
            for r in range(self.num_rings):
                expected_count = self.clean_baseline[s, r, 0]
                actual_count = len(sector_data[s][r])
                
                if expected_count > 5:  # Only check sectors with data
                    # Point count drop indicates contamination
                    count_ratio = actual_count / (expected_count + 1e-7)
                    contamination[s, r] = max(1.0 - count_ratio, 0.0)
        
        return contamination
    
    def summarize(self, contamination_map):
        """Summarize contamination for alerting."""
        if contamination_map is None:
            return {'status': 'no_baseline', 'severity': 0.0}
        
        max_contamination = contamination_map.max()
        mean_contamination = contamination_map.mean()
        
        # Find worst sector
        worst_sector = np.unravel_index(contamination_map.argmax(), contamination_map.shape)
        
        # Classify
        if max_contamination < 0.1:
            status = 'clean'
        elif max_contamination < 0.3:
            status = 'minor_contamination'
        elif max_contamination < 0.6:
            status = 'moderate_contamination'
        else:
            status = 'severe_contamination'
        
        return {
            'status': status,
            'max_severity': max_contamination,
            'mean_severity': mean_contamination,
            'worst_sector_deg': worst_sector[0] * (360 / self.num_sectors),
            'worst_ring_m': worst_sector[1] * (self.max_range / self.num_rings),
            'blocked_sectors': int(np.sum(contamination_map.max(axis=1) > 0.5)),
        }

7.2 Temporal Degradation Tracking

python
class DegradationTracker:
    """Track sensor health over time to detect gradual degradation.
    
    Uses exponentially weighted moving average with anomaly detection.
    """
    
    def __init__(self, alpha=0.01, alert_threshold=3.0):
        self.alpha = alpha  # EMA smoothing (slow = detect gradual trends)
        self.alert_threshold = alert_threshold  # Std deviations for alert
        self.ema = {}
        self.ema_var = {}
    
    def update(self, metric_name, value):
        """Update EMA tracking for a metric.
        
        Returns: (is_anomaly, z_score, trend_direction)
        """
        if metric_name not in self.ema:
            self.ema[metric_name] = value
            self.ema_var[metric_name] = 0.0
            return False, 0.0, 'stable'
        
        # Update EMA
        old_ema = self.ema[metric_name]
        self.ema[metric_name] = self.alpha * value + (1 - self.alpha) * old_ema
        
        # Update variance EMA
        diff = value - old_ema
        self.ema_var[metric_name] = (
            self.alpha * diff**2 + (1 - self.alpha) * self.ema_var[metric_name]
        )
        
        # Z-score
        std = np.sqrt(self.ema_var[metric_name]) + 1e-7
        z_score = diff / std
        
        is_anomaly = abs(z_score) > self.alert_threshold
        
        # Trend: compare current EMA to baseline
        trend = 'degrading' if self.ema[metric_name] < old_ema * 0.99 else \
                'improving' if self.ema[metric_name] > old_ema * 1.01 else 'stable'
        
        return is_anomaly, z_score, trend

8. Automated Response Actions

8.1 Response Action Matrix

python
class SensorHealthResponseManager:
    """Automated responses to sensor degradation events."""
    
    RESPONSE_MATRIX = {
        # (sensor, severity): (action, speed_limit, margin_mult, alert_level)
        ('lidar', 'minor'): ('log', 25, 1.0, 'info'),
        ('lidar', 'moderate'): ('reduce_speed', 15, 1.5, 'warning'),
        ('lidar', 'severe'): ('switch_to_radar_primary', 10, 2.0, 'critical'),
        ('lidar', 'failure'): ('safe_stop', 0, None, 'emergency'),
        
        ('radar', 'minor'): ('log', 25, 1.0, 'info'),
        ('radar', 'moderate'): ('log', 20, 1.2, 'warning'),
        ('radar', 'severe'): ('reduce_coverage', 15, 1.5, 'critical'),
        ('radar', 'failure'): ('reduce_speed', 15, 1.5, 'warning'),  # LiDAR still primary
        
        ('thermal', 'minor'): ('log', 25, 1.0, 'info'),
        ('thermal', 'moderate'): ('disable_night_ops', 25, 1.0, 'warning'),
        ('thermal', 'severe'): ('disable_night_ops', 25, 1.0, 'warning'),
        ('thermal', 'failure'): ('disable_night_ops', 15, 1.5, 'critical'),
        
        ('camera', 'minor'): ('log', 25, 1.0, 'info'),
        ('camera', 'moderate'): ('log', 25, 1.0, 'info'),  # Not primary sensor
        ('camera', 'severe'): ('disable_camera_fallback', 25, 1.0, 'warning'),
        ('camera', 'failure'): ('disable_camera_fallback', 25, 1.0, 'warning'),
    }
    
    def execute_response(self, sensor, severity, vehicle_controller, fleet_mgr):
        """Execute automated response to sensor degradation."""
        action, speed_limit, margin_mult, alert_level = \
            self.RESPONSE_MATRIX.get((sensor, severity), ('log', 25, 1.0, 'info'))
        
        # Apply speed limit
        vehicle_controller.set_max_speed(speed_limit)
        
        # Apply safety margin multiplier
        if margin_mult:
            vehicle_controller.set_safety_margin_multiplier(margin_mult)
        
        # Sensor-specific actions
        if action == 'safe_stop':
            vehicle_controller.request_safe_stop('sensor_degradation')
        elif action == 'switch_to_radar_primary':
            vehicle_controller.set_fusion_mode('radar_primary')
        elif action == 'disable_night_ops':
            vehicle_controller.restrict_odd('daytime_only')
        
        # Fleet notification
        fleet_mgr.report_sensor_event(
            vehicle_id=vehicle_controller.vehicle_id,
            sensor=sensor,
            severity=severity,
            action=action,
            alert_level=alert_level
        )
        
        # Schedule maintenance if needed
        if severity in ('moderate', 'severe', 'failure'):
            fleet_mgr.schedule_maintenance(
                vehicle_controller.vehicle_id,
                task=f'{sensor}_inspection',
                priority='high' if severity in ('severe', 'failure') else 'normal'
            )

9. Fleet-Level Health Analytics

9.1 Fleet Degradation Pattern Mining

python
class FleetHealthAnalytics:
    """Aggregate sensor health data across fleet for pattern detection.
    
    Fleet-level patterns reveal:
    - Environmental degradation (all vehicles in Zone A degrade → zone issue)
    - Batch defects (all sensors from same lot degrade → recall)
    - Seasonal patterns (winter icing, summer bugs)
    - Operational patterns (vehicles near de-icing bays degrade faster)
    """
    
    def __init__(self, fleet_size=20):
        self.fleet_size = fleet_size
        self.health_db = {}  # vehicle_id -> list of health reports
    
    def analyze_fleet_patterns(self):
        """Mine fleet-wide sensor health patterns."""
        patterns = {}
        
        # 1. Spatial clustering: which zones cause degradation?
        patterns['zone_degradation'] = self._spatial_analysis()
        
        # 2. Temporal clustering: when does degradation happen?
        patterns['temporal_degradation'] = self._temporal_analysis()
        
        # 3. Vehicle-specific: which vehicles degrade fastest?
        patterns['vehicle_ranking'] = self._vehicle_ranking()
        
        # 4. Sensor batch analysis: correlate with sensor serial numbers
        patterns['batch_analysis'] = self._batch_analysis()
        
        return patterns
    
    def predict_maintenance(self, vehicle_id):
        """Predict when sensor cleaning/maintenance will be needed.
        
        Uses degradation rate extrapolation.
        """
        history = self.health_db.get(vehicle_id, [])
        if len(history) < 10:
            return {'prediction': 'insufficient_data'}
        
        # Linear extrapolation of health trend
        healths = [h['overall_health'] for h in history[-100:]]
        x = np.arange(len(healths))
        slope, intercept = np.polyfit(x, healths, 1)
        
        if slope >= 0:
            return {'prediction': 'stable', 'days_to_threshold': float('inf')}
        
        # Time to reach 0.7 threshold (trigger maintenance)
        threshold = 0.7
        current = healths[-1]
        if current <= threshold:
            return {'prediction': 'maintenance_needed_now'}
        
        samples_to_threshold = (threshold - current) / slope
        # Convert samples to hours (assuming 1 Hz monitoring, 16h/day operation)
        hours_to_threshold = samples_to_threshold / 3600
        
        return {
            'prediction': 'maintenance_predicted',
            'hours_to_threshold': hours_to_threshold,
            'days_to_threshold': hours_to_threshold / 16,
            'degradation_rate': slope,
        }

10. Maintenance Scheduling Integration

10.1 Predictive Maintenance Triggers

TriggerThresholdActionUrgency
LiDAR overall health < 0.7Predicted within 24hSchedule cleaningNormal
LiDAR overall health < 0.5CurrentImmediate cleaningHigh
Blocked sectors > 2CurrentInspect + cleanHigh
Calibration drift > 0.1°From cross-sensor checkRecalibrateNormal
Radar detection drop > 20%Sustained 1h+Inspect radomeNormal
Thermal NUC quality < 0.6CurrentForce NUC recalibrationNormal
Any sensor failureCurrentPull from serviceImmediate

10.2 Cleaning Schedule Optimization

Empirical cleaning intervals (airport environment):
  
  Summer (high bug/dust):
    LiDAR lens: every 48-72 hours
    Camera lens: every 24-48 hours
    Radar radome: every 168 hours (weekly)
    Thermal window: every 168 hours
  
  Winter (ice/de-icing):
    LiDAR lens: every 24-48 hours (de-icing residue)
    Camera lens: every 24-48 hours
    Radar radome: every 72-120 hours
    Thermal window: every 72-120 hours
  
  With automated wiper/heater:
    Intervals extend 2-3x
    Cost: $300-800 per sensor for wiper system
    ROI: <3 months from reduced manual cleaning labor

11. Implementation Guide

11.1 ROS Health Monitor Node

python
class SensorHealthNode:
    """ROS node for unified sensor health monitoring."""
    
    def __init__(self):
        rospy.init_node('sensor_health_monitor')
        
        # Per-sensor monitors
        self.lidar_monitors = {}
        for i in range(rospy.get_param('~num_lidars', 8)):
            self.lidar_monitors[f'lidar_{i}'] = LiDARHealthMonitor(f'lidar_{i}')
        
        self.radar_monitor = RadarHealthMetrics()
        self.thermal_monitor = ThermalHealthMonitor()
        self.consistency_monitor = CrossSensorConsistencyMonitor()
        self.response_mgr = SensorHealthResponseManager()
        
        # Timer: run health check at 1 Hz
        self.timer = rospy.Timer(rospy.Duration(1.0), self.health_check_callback)
        
        # Publishers
        self.health_pub = rospy.Publisher('/sensor_health', SensorHealthReport, queue_size=1)
        self.alert_pub = rospy.Publisher('/sensor_alerts', SensorAlert, queue_size=10)
    
    def health_check_callback(self, event):
        """Periodic health check across all sensors."""
        report = SensorHealthReport()
        report.header.stamp = rospy.Time.now()
        
        # Check each LiDAR
        for lid, monitor in self.lidar_monitors.items():
            latest_cloud = self.get_latest_cloud(lid)
            if latest_cloud is not None:
                health = monitor.diagnose(latest_cloud, rospy.Time.now().to_sec())
                report.lidar_health[lid] = health['overall_health']
                
                if health['overall_health'] < 0.7:
                    self.alert_pub.publish(SensorAlert(
                        sensor=lid,
                        severity='moderate' if health['overall_health'] > 0.4 else 'severe',
                        message=f"LiDAR {lid} health: {health['overall_health']:.2f}"
                    ))
        
        # Cross-sensor consistency
        consistency = self.consistency_monitor.check_consistency(
            self.latest_lidar_dets, self.latest_radar_dets
        )
        report.cross_sensor_agreement = consistency.get(
            'lidar_radar_agreement', {}
        ).get('agreement_ratio', 1.0)
        
        self.health_pub.publish(report)

11.2 Implementation Roadmap

PhaseDurationDeliverableCost
1. LiDAR self-diagnostics2 weeksPer-LiDAR health monitor$5K
2. Cross-sensor consistency2 weeksMulti-modal consistency check$5K
3. Automated response1 weekMode switching + speed limiting$3K
4. Contamination detection2 weeksSector-based detection + alerting$5K
5. Fleet analytics2 weeksPredictive maintenance dashboard$7K
6. Wiper/heater integration2 weeksHardware + control integration$10K
Total11 weeksComplete sensor health system$35K

12. Key Takeaways

  1. Sensor degradation is more dangerous than sensor failure: Degraded sensors produce confident but incorrect detections. Failure triggers known-safe fallback.

  2. LiDAR lens contamination reduces point cloud by up to 75%: De-icing fluid residue, bug splatter, and tarmac grime are the primary culprits on airside. Summer: bugs every 48-72h. Winter: de-icing residue every 24-48h.

  3. Cross-sensor consistency is the most powerful detection method: If LiDAR misses objects that radar sees, LiDAR is degraded. Systematic disagreement trending is more reliable than per-sensor thresholds.

  4. 1 Hz monitoring at <2ms total is sufficient: Health checks don't need to run every frame. 1 Hz catches gradual degradation; sudden events (splash, impact) trigger immediate per-frame checks.

  5. AutoGrAN graph attention approach achieves high contamination detection accuracy: Voxel-based sector analysis detects partial lens blockage with >90% reliability.

  6. 4D radar is naturally more robust: IP69K rating, built-in radome heater, millimeter-wave physics immune to optical contamination. Radar health monitoring is primarily about detection statistics.

  7. Thermal NUC drift requires periodic self-calibration: Flat-field correction accuracy degrades after 100+ hours of continuous operation. Schedule automatic NUC every 8-12 hours.

  8. Automated response matrix prevents unsafe operation: Progressive response: log → reduce speed → switch primary sensor → safe stop. No human intervention needed for degradation response.

  9. Fleet-level patterns reveal root causes: Zone-correlated degradation (near de-icing bays), seasonal patterns (summer bugs, winter ice), and batch defects all visible at fleet scale.

  10. Predictive maintenance reduces unplanned downtime 40-60%: Linear extrapolation of health trends predicts cleaning needs 24-48h ahead — schedule during planned charging.

  11. Wiper/heater systems extend cleaning intervals 2-3x: $300-800 per sensor, ❤️ month ROI from reduced manual labor. Essential for 24/7 operations.

  12. ISO 3691-4 mandates sensor performance monitoring: This is not optional — certification requires continuous verification of sensor operation within specification.

  13. Total implementation: $35K over 11 weeks: Core monitoring (4 weeks, $13K) delivers 80% of value. Fleet analytics and hardware additions follow.


13. References

  1. Jha et al., "AutoGrAN: Autonomous Vehicle LiDAR Contaminant Detection using Graph Attention Networks," ICPE 2024
  2. Nweke et al., "Sensor Blockage in Autonomous Vehicles: AI-Driven Approaches," WJAETS 2025
  3. Heinzler et al., "Contaminations on Lidar Sensor Covers: Performance Degradation Including Fault Detection and Modeling," IEEE T-ITS 2023
  4. Park et al., "Empirical Analysis of Autonomous Vehicle's LiDAR Detection Performance Degradation for Actual Road Driving in Rain and Fog," Sensors 2023
  5. Bijelic et al., "Benchmarking Image Sensors Under Adverse Weather Conditions for Autonomous Driving," IV 2018
  6. Continental, "ARS548 4D Imaging Radar Self-Diagnostic Capabilities," Technical Note 2024
  7. FLIR, "Boson Radiometric Camera Performance and Calibration Guide," 2023
  8. ISO 3691-4:2020, "Industrial Trucks — Safety Requirements and Verification — Part 4: Driverless Industrial Trucks"

Public research notes collected from public sources.