///|
/// Online score calibration mode for deployment-specific alert rates.
pub(all) enum ProductionCalibrationMode {
IdentityCalibration
HistogramCalibration
PriorWeightedCalibration
}
///|
pub fn production_calibration_mode_name(
mode : ProductionCalibrationMode,
) -> String {
match mode {
IdentityCalibration => "identity"
HistogramCalibration => "histogram"
PriorWeightedCalibration => "prior-weighted"
}
}
///|
/// A bin of online score-to-outcome evidence.
pub struct ProductionScoreBin {
lower : Double
upper : Double
mut total : Double
mut positives : Double
prior_positive : Double
prior_negative : Double
}
///|
pub fn ProductionScoreBin::new(
lower : Double,
upper : Double,
prior_positive? : Double = 1.0,
prior_negative? : Double = 1.0,
) -> ProductionScoreBin {
{
lower,
upper,
total: 0.0,
positives: 0.0,
prior_positive: if prior_positive < 0.0 {
0.0
} else {
prior_positive
},
prior_negative: if prior_negative < 0.0 {
0.0
} else {
prior_negative
},
}
}
///|
pub fn ProductionScoreBin::contains(
self : ProductionScoreBin,
score : Double,
) -> Bool {
score >= self.lower && score < self.upper
}
///|
pub fn ProductionScoreBin::add(
self : ProductionScoreBin,
positive : Bool,
weight : Double,
) -> Unit {
let safe_weight = if weight <= 0.0 { 1.0 } else { weight }
self.total += safe_weight
if positive {
self.positives += safe_weight
}
}
///|
pub fn ProductionScoreBin::total(self : ProductionScoreBin) -> Double {
self.total
}
///|
pub fn ProductionScoreBin::positives(self : ProductionScoreBin) -> Double {
self.positives
}
///|
pub fn ProductionScoreBin::rate(self : ProductionScoreBin) -> Double {
let numerator = self.positives + self.prior_positive
let denominator = self.total + self.prior_positive + self.prior_negative
if denominator <= 0.0 {
0.0
} else {
numerator / denominator
}
}
///|
pub fn ProductionScoreBin::mean_score(self : ProductionScoreBin) -> Double {
(self.lower + self.upper) / 2.0
}
///|
pub fn ProductionScoreBin::summary(self : ProductionScoreBin) -> String {
self.lower.to_string() +
"-" +
self.upper.to_string() +
":total=" +
self.total.to_string() +
",positives=" +
self.positives.to_string() +
",rate=" +
self.rate().to_string()
}
///|
/// Bounded online calibrator with deployment-safe priors.
pub struct ProductionOnlineCalibrator {
bins : Array[ProductionScoreBin]
mode : ProductionCalibrationMode
mut observations : Int
mut positive : Int
mut negative : Int
}
///|
pub fn ProductionOnlineCalibrator::new(
bin_count? : Int = 16,
mode? : ProductionCalibrationMode = PriorWeightedCalibration,
) -> ProductionOnlineCalibrator {
let count = if bin_count < 2 { 2 } else { bin_count }
let bins : Array[ProductionScoreBin] = []
for i in 0.. ProductionCalibrationMode {
self.mode
}
///|
pub fn ProductionOnlineCalibrator::bin_count(
self : ProductionOnlineCalibrator,
) -> Int {
self.bins.length()
}
///|
pub fn ProductionOnlineCalibrator::observations(
self : ProductionOnlineCalibrator,
) -> Int {
self.observations
}
///|
pub fn ProductionOnlineCalibrator::positive(
self : ProductionOnlineCalibrator,
) -> Int {
self.positive
}
///|
pub fn ProductionOnlineCalibrator::negative(
self : ProductionOnlineCalibrator,
) -> Int {
self.negative
}
///|
pub fn ProductionOnlineCalibrator::update(
self : ProductionOnlineCalibrator,
score : Double,
changed : Bool,
weight? : Double = 1.0,
) -> Unit {
let safe_score = clamp_probability(score)
let index = if safe_score >= 1.0 {
self.bins.length() - 1
} else {
(safe_score * self.bins.length().to_double()).to_int()
}
self.bins[index].add(changed, weight)
self.observations += 1
if changed {
self.positive += 1
} else {
self.negative += 1
}
}
///|
pub fn ProductionOnlineCalibrator::calibrate(
self : ProductionOnlineCalibrator,
score : Double,
) -> Double {
let safe_score = clamp_probability(score)
match self.mode {
IdentityCalibration => safe_score
HistogramCalibration => {
let index = if safe_score >= 1.0 {
self.bins.length() - 1
} else {
(safe_score * self.bins.length().to_double()).to_int()
}
self.bins[index].rate()
}
PriorWeightedCalibration => {
let index = if safe_score >= 1.0 {
self.bins.length() - 1
} else {
(safe_score * self.bins.length().to_double()).to_int()
}
let rate = self.bins[index].rate()
clamp_probability(0.5 * safe_score + 0.5 * rate)
}
}
}
///|
pub fn ProductionOnlineCalibrator::bins(
self : ProductionOnlineCalibrator,
) -> Array[ProductionScoreBin] {
let result : Array[ProductionScoreBin] = []
for bin in self.bins {
result.push(bin)
}
result
}
///|
pub fn ProductionOnlineCalibrator::reliability_error(
self : ProductionOnlineCalibrator,
) -> Double {
if self.observations == 0 {
return 0.0
}
let mut total = 0.0
for bin in self.bins {
if bin.total() > 0.0 {
total += bin.total() * absolute(bin.mean_score() - bin.rate())
}
}
total / self.observations.to_double()
}
///|
pub fn ProductionOnlineCalibrator::reset(
self : ProductionOnlineCalibrator,
) -> Unit {
for bin in self.bins {
bin.total = 0.0
bin.positives = 0.0
}
self.observations = 0
self.positive = 0
self.negative = 0
}
///|
/// Converts a detector score into a calibrated detection result.
pub fn production_calibrate_result(
calibrator : ProductionOnlineCalibrator,
result : DetectionResult,
) -> DetectionResult {
let calibrated = calibrator.calibrate(result.score)
DetectionResult::new(
calibrated >= 0.5 && result.changed,
calibrated,
calibrated,
result.direction,
result.index,
evidence=result.evidence,
)
}
///|
/// Adaptive threshold controller based on a recent score window and hysteresis.
pub struct ProductionThresholdController {
scores : DoubleWindow
target_alert_rate : Double
minimum_threshold : Double
maximum_threshold : Double
hysteresis : Double
mut threshold : Double
mut updates : Int
}
///|
pub fn ProductionThresholdController::new(
window_size? : Int = 256,
target_alert_rate? : Double = 0.01,
minimum_threshold? : Double = 0.1,
maximum_threshold? : Double = 10.0,
hysteresis? : Double = 0.05,
) -> ProductionThresholdController {
let low = if minimum_threshold < 0.0 { 0.0 } else { minimum_threshold }
let high = if maximum_threshold < low { low } else { maximum_threshold }
{
scores: DoubleWindow::new(if window_size < 4 { 4 } else { window_size }),
target_alert_rate: clamp_probability(target_alert_rate),
minimum_threshold: low,
maximum_threshold: high,
hysteresis: if hysteresis < 0.0 {
0.0
} else {
hysteresis
},
threshold: high,
updates: 0,
}
}
///|
pub fn ProductionThresholdController::threshold(
self : ProductionThresholdController,
) -> Double {
self.threshold
}
///|
pub fn ProductionThresholdController::updates(
self : ProductionThresholdController,
) -> Int {
self.updates
}
///|
pub fn ProductionThresholdController::push(
self : ProductionThresholdController,
score : Double,
) -> Double {
if !is_finite(score) {
return self.threshold
}
ignore(self.scores.push(score))
self.updates += 1
if self.scores.length() >= 4 {
let desired = quantile(self.scores.to_array(), 1.0 - self.target_alert_rate)
let bounded = if desired < self.minimum_threshold {
self.minimum_threshold
} else if desired > self.maximum_threshold {
self.maximum_threshold
} else {
desired
}
if absolute(bounded - self.threshold) >= self.hysteresis {
self.threshold = bounded
}
}
self.threshold
}
///|
pub fn ProductionThresholdController::reset(
self : ProductionThresholdController,
) -> Unit {
self.scores.clear()
self.threshold = self.maximum_threshold
self.updates = 0
}
///|
pub fn ProductionThresholdController::summary(
self : ProductionThresholdController,
) -> String {
"threshold=" +
self.threshold.to_string() +
",updates=" +
self.updates.to_string() +
",window=" +
self.scores.length().to_string() +
",target_rate=" +
self.target_alert_rate.to_string()
}