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
- Why Sensor Health Monitoring is Critical
- LiDAR Degradation Modes
- 4D Radar Degradation Modes
- Camera and Thermal Degradation
- Per-Sensor Self-Diagnostics
- Cross-Sensor Consistency Checking
- Degradation Detection Algorithms
- Automated Response Actions
- Fleet-Level Health Analytics
- Maintenance Scheduling Integration
- Implementation Guide
- Key Takeaways
- References
1. Why Sensor Health Monitoring is Critical
1.1 The Silent Failure Problem
Sensor degradation is fundamentally different from sensor failure:
| Complete Failure | Degradation | |
|---|---|---|
| Detection | Trivial (no data) | Difficult (data looks plausible) |
| System response | Immediate fallback | Continued operation with reduced accuracy |
| Risk | Low (known-safe state) | High (confident but wrong) |
| Example | LiDAR disconnects | Dirty 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
| Source | Frequency | Affected Sensors | Severity |
|---|---|---|---|
| De-icing fluid residue | Per treatment | LiDAR, camera | High — glycol film progressively degrades |
| Bug/insect impact | Daily (summer) | LiDAR, camera | Moderate — point blockage, image spots |
| Tarmac rubber dust | Continuous | All optical | Low-moderate — gradual buildup |
| Hydraulic fluid mist | Occasional | LiDAR, camera | High — rapid optics contamination |
| Jet exhaust soot | Per departure | All | Low — gradual carbon buildup |
| Bird droppings | Occasional | Camera, thermal | High — immediate localized blockage |
| Salt spray (coastal) | Seasonal | Radar, metallic | Moderate — corrosion accelerator |
| Standing water splash | Rain events | LiDAR, camera | Temporary — water droplets on lens |
| Pollen | Seasonal | LiDAR, camera | Low — gradual accumulation |
| Snow/ice accumulation | Winter | All | High — 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 temps2.2 Quantitative Impact
| Degradation Mode | Detection Method | Impact on mAP | Time to Onset |
|---|---|---|---|
| 10% lens coverage | Point count drop | -5-10% | Hours-days |
| 25% lens coverage | Angular hole detection | -15-25% | Days |
| 50% lens coverage | Point density anomaly | -30-50% | Days-weeks |
| 75% lens coverage | Near-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 loss3.2 Radar Health Metrics
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 / 124. Camera and Thermal Degradation
4.1 Camera Degradation
| Mode | Detection | Impact | Frequency |
|---|---|---|---|
| Lens water droplets | Image clarity metrics | Localized blur | Rain events |
| Lens dirt/film | Contrast reduction | Global degradation | Days-weeks |
| Sun glare/flare | Overexposure detection | Angular blind zone | Daily |
| Lens crack/damage | Edge detection artifacts | Permanent | Impact event |
| IR filter degradation | Color shift | Gradual accuracy loss | Years |
| Auto-exposure failure | Global over/under exposure | Total failure | Rare |
4.2 Thermal Camera Degradation
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
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_beams6. Cross-Sensor Consistency Checking
6.1 Multi-Sensor Consistency Monitor
The most powerful degradation detection: compare what each sensor sees and flag inconsistencies.
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 = degradation7. 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:
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
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, trend8. Automated Response Actions
8.1 Response Action Matrix
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
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
| Trigger | Threshold | Action | Urgency |
|---|---|---|---|
| LiDAR overall health < 0.7 | Predicted within 24h | Schedule cleaning | Normal |
| LiDAR overall health < 0.5 | Current | Immediate cleaning | High |
| Blocked sectors > 2 | Current | Inspect + clean | High |
| Calibration drift > 0.1° | From cross-sensor check | Recalibrate | Normal |
| Radar detection drop > 20% | Sustained 1h+ | Inspect radome | Normal |
| Thermal NUC quality < 0.6 | Current | Force NUC recalibration | Normal |
| Any sensor failure | Current | Pull from service | Immediate |
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 labor11. Implementation Guide
11.1 ROS Health Monitor Node
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
| Phase | Duration | Deliverable | Cost |
|---|---|---|---|
| 1. LiDAR self-diagnostics | 2 weeks | Per-LiDAR health monitor | $5K |
| 2. Cross-sensor consistency | 2 weeks | Multi-modal consistency check | $5K |
| 3. Automated response | 1 week | Mode switching + speed limiting | $3K |
| 4. Contamination detection | 2 weeks | Sector-based detection + alerting | $5K |
| 5. Fleet analytics | 2 weeks | Predictive maintenance dashboard | $7K |
| 6. Wiper/heater integration | 2 weeks | Hardware + control integration | $10K |
| Total | 11 weeks | Complete sensor health system | $35K |
12. Key Takeaways
Sensor degradation is more dangerous than sensor failure: Degraded sensors produce confident but incorrect detections. Failure triggers known-safe fallback.
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.
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.
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.
AutoGrAN graph attention approach achieves high contamination detection accuracy: Voxel-based sector analysis detects partial lens blockage with >90% reliability.
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.
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.
Automated response matrix prevents unsafe operation: Progressive response: log → reduce speed → switch primary sensor → safe stop. No human intervention needed for degradation response.
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.
Predictive maintenance reduces unplanned downtime 40-60%: Linear extrapolation of health trends predicts cleaning needs 24-48h ahead — schedule during planned charging.
Wiper/heater systems extend cleaning intervals 2-3x: $300-800 per sensor, ❤️ month ROI from reduced manual labor. Essential for 24/7 operations.
ISO 3691-4 mandates sensor performance monitoring: This is not optional — certification requires continuous verification of sensor operation within specification.
Total implementation: $35K over 11 weeks: Core monitoring (4 weeks, $13K) delivers 80% of value. Fleet analytics and hardware additions follow.
13. References
- Jha et al., "AutoGrAN: Autonomous Vehicle LiDAR Contaminant Detection using Graph Attention Networks," ICPE 2024
- Nweke et al., "Sensor Blockage in Autonomous Vehicles: AI-Driven Approaches," WJAETS 2025
- Heinzler et al., "Contaminations on Lidar Sensor Covers: Performance Degradation Including Fault Detection and Modeling," IEEE T-ITS 2023
- Park et al., "Empirical Analysis of Autonomous Vehicle's LiDAR Detection Performance Degradation for Actual Road Driving in Rain and Fog," Sensors 2023
- Bijelic et al., "Benchmarking Image Sensors Under Adverse Weather Conditions for Autonomous Driving," IV 2018
- Continental, "ARS548 4D Imaging Radar Self-Diagnostic Capabilities," Technical Note 2024
- FLIR, "Boson Radiometric Camera Performance and Calibration Guide," 2023
- ISO 3691-4:2020, "Industrial Trucks — Safety Requirements and Verification — Part 4: Driverless Industrial Trucks"