All files / calibrate anomaly-guided.ts

100% Statements 32/32
100% Branches 12/12
100% Functions 2/2
100% Lines 32/32

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47              1x   1x 5x 5x 5x 5x   5x 12x 12x 12x 10x 2x   12x 8x 12x 4x 4x 4x   12x 3x 3x     12x 12x   12x 12x 12x 12x 12x 12x 12x 12x 5x  
/**
 * AnomalyGuidedCalibrator — marks contamination windows and refits.
 *
 * When sustained anomalies are detected, marks the window as contaminated
 * so that the forecast baseline is recalculated on clean data only.
 */
import type { ICalibrator, DetectionResult, ForecastResult, DataPoint, CalibrationResult, CalibrationMode } from '../types.js'
import { clamp } from './forecast-guided.js'
 
export class AnomalyGuidedCalibrator implements ICalibrator {
  readonly mode: CalibrationMode = 'anomaly-guided'
  private contaminated = false
  private consecutiveAnomalies = 0
  private readonly threshold = 3 // consecutive anomalies to trigger contamination
 
  calibrate(detection: DetectionResult, forecast: ForecastResult, currentPoint: DataPoint): CalibrationResult {
    const spread = (forecast.q90[0] ?? 0) - (forecast.q10[0] ?? 0)
    const predicted = forecast.predicted[0] ?? currentPoint.value
    const residual = spread > 1e-10
      ? Math.abs(currentPoint.value - predicted) / (spread / 2)
      : Math.abs(currentPoint.value - predicted)
 
    if (detection.isAnomaly) {
      this.consecutiveAnomalies++
    } else {
      this.consecutiveAnomalies = 0
      this.contaminated = false
    }
 
    if (this.consecutiveAnomalies >= this.threshold) {
      this.contaminated = true
    }
 
    // When contaminated, reduce confidence because baseline may be polluted
    const contaminationPenalty = this.contaminated ? 0.3 : 0
    const jointConfidence = clamp(detection.confidence - contaminationPenalty, 0, 1)
 
    return {
      mode: 'anomaly-guided',
      residual,
      jointConfidence,
      intervalBreached: residual > 1,
      isContaminated: this.contaminated,
    }
  }
}