1pub const W_CRITICAL: u64 = 4;
16pub const W_HIGH: u64 = 3;
18pub const W_MEDIUM: u64 = 2;
20pub const W_LOW: u64 = 1;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
27pub enum CriticalityBand {
28 Critical,
30 High,
32 Medium,
34 Low,
36}
37
38#[derive(Debug, Clone, Copy)]
47pub struct ValidatedThresholdSet {
48 high_end: f64,
50 medium_end: f64,
52}
53
54impl ValidatedThresholdSet {
55 #[must_use]
60 pub fn new(high_end: f64, medium_end: f64) -> Self {
61 assert!(
62 high_end > 0.0 && high_end < 1.0,
63 "high_end must be in (0, 1), got {high_end}"
64 );
65 assert!(
66 medium_end > 0.0 && medium_end < 1.0,
67 "medium_end must be in (0, 1), got {medium_end}"
68 );
69 assert!(
70 high_end < medium_end,
71 "high_end ({high_end}) must be strictly less than medium_end ({medium_end})"
72 );
73 Self {
74 high_end,
75 medium_end,
76 }
77 }
78
79 #[must_use]
81 pub fn default_thirds() -> Self {
82 Self {
83 high_end: 1.0 / 3.0,
84 medium_end: 2.0 / 3.0,
85 }
86 }
87}
88
89#[must_use]
100pub fn classify(float: u64, max_float: u64) -> CriticalityBand {
101 let thresholds = ValidatedThresholdSet::default_thirds();
102 classify_with(float, max_float, &thresholds)
103}
104
105#[must_use]
107pub fn classify_with(
108 float: u64,
109 max_float: u64,
110 thresholds: &ValidatedThresholdSet,
111) -> CriticalityBand {
112 if float == 0 || max_float == 0 {
113 return CriticalityBand::Critical;
114 }
115 let ratio = float as f64 / max_float as f64;
116 if ratio <= thresholds.high_end {
117 CriticalityBand::High
118 } else if ratio <= thresholds.medium_end {
119 CriticalityBand::Medium
120 } else {
121 CriticalityBand::Low
122 }
123}
124
125#[derive(Debug, Clone, PartialEq)]
129pub enum ActivityRiskResult {
130 Risk(f64),
132 AllCritical,
135}
136
137#[must_use]
154pub fn activity_risk(floats: &[u64], max_float: u64) -> ActivityRiskResult {
155 if max_float == 0 || floats.is_empty() {
156 return ActivityRiskResult::AllCritical;
157 }
158 let sum: u64 = floats.iter().sum();
159 let denominator = max_float * floats.len() as u64;
160 ActivityRiskResult::Risk(1.0 - (sum as f64) / (denominator as f64))
161}
162
163#[must_use]
175pub fn criticality_risk(bands: &[CriticalityBand]) -> f64 {
176 if bands.is_empty() {
177 return 1.0;
178 }
179 let sum: u64 = bands
180 .iter()
181 .map(|b| match b {
182 CriticalityBand::Critical => W_CRITICAL,
183 CriticalityBand::High => W_HIGH,
184 CriticalityBand::Medium => W_MEDIUM,
185 CriticalityBand::Low => W_LOW,
186 })
187 .sum();
188 let max_possible = W_CRITICAL * bands.len() as u64;
189 sum as f64 / max_possible as f64
190}
191
192#[derive(Debug, Clone, Copy, PartialEq, Eq)]
196pub enum RiskBandFlag {
197 InTarget,
199 HighRisk,
201 OverDecompressed,
203}
204
205#[must_use]
215pub fn band_flag(risk: f64) -> RiskBandFlag {
216 if risk < 0.4 {
217 RiskBandFlag::OverDecompressed
218 } else if risk > 0.75 {
219 RiskBandFlag::HighRisk
220 } else {
221 RiskBandFlag::InTarget
222 }
223}
224
225#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
234 fn test_weights_are_exactly_4_3_2_1() {
235 assert_eq!(W_CRITICAL, 4, "W_CRITICAL must be exactly 4");
236 assert_eq!(W_HIGH, 3, "W_HIGH must be exactly 3");
237 assert_eq!(W_MEDIUM, 2, "W_MEDIUM must be exactly 2");
238 assert_eq!(W_LOW, 1, "W_LOW must be exactly 1");
239 }
240
241 #[test]
244 fn test_classify_zero_float_is_critical() {
245 assert_eq!(classify(0, 12), CriticalityBand::Critical);
246 }
247
248 #[test]
249 fn test_classify_high_band() {
250 assert_eq!(classify(3, 12), CriticalityBand::High);
252 assert_eq!(classify(4, 12), CriticalityBand::High);
254 }
255
256 #[test]
257 fn test_classify_medium_band() {
258 assert_eq!(classify(5, 12), CriticalityBand::Medium);
260 assert_eq!(classify(8, 12), CriticalityBand::Medium);
262 }
263
264 #[test]
265 fn test_classify_low_band() {
266 assert_eq!(classify(9, 12), CriticalityBand::Low);
268 assert_eq!(classify(12, 12), CriticalityBand::Low);
270 }
271
272 #[test]
273 fn test_classify_max_float_zero_returns_critical() {
274 assert_eq!(classify(5, 0), CriticalityBand::Critical);
275 }
276
277 #[test]
280 #[should_panic]
281 fn test_threshold_inverted_panics() {
282 let _ = ValidatedThresholdSet::new(0.7, 0.3);
283 }
284
285 #[test]
286 #[should_panic]
287 fn test_threshold_equal_panics() {
288 let _ = ValidatedThresholdSet::new(0.5, 0.5);
289 }
290
291 #[test]
294 fn test_activity_risk_all_critical_when_max_float_zero() {
295 let floats = [0u64, 0, 0];
296 assert_eq!(activity_risk(&floats, 0), ActivityRiskResult::AllCritical);
297 }
298
299 #[test]
300 fn test_activity_risk_all_zero_floats_with_positive_max() {
301 let floats = [0u64, 0, 0];
303 assert_eq!(activity_risk(&floats, 12), ActivityRiskResult::Risk(1.0));
304 }
305
306 #[test]
307 fn test_activity_risk_known_distribution() {
308 let floats = [0u64, 4, 8];
312 let max_float = 12u64;
313 let expected = 1.0 - 12.0_f64 / 36.0_f64;
314 match activity_risk(&floats, max_float) {
315 ActivityRiskResult::Risk(r) => {
316 assert!((r - expected).abs() < 1e-10, "expected {expected}, got {r}");
317 }
318 ActivityRiskResult::AllCritical => panic!("expected Risk, got AllCritical"),
319 }
320 }
321
322 #[test]
323 fn test_activity_risk_empty_returns_all_critical() {
324 assert_eq!(activity_risk(&[], 10), ActivityRiskResult::AllCritical);
325 }
326
327 #[test]
330 fn test_criticality_risk_all_critical_is_one() {
331 let bands = [
334 CriticalityBand::Critical,
335 CriticalityBand::Critical,
336 CriticalityBand::Critical,
337 ];
338 let r = criticality_risk(&bands);
339 assert!(
340 (r - 1.0).abs() < 1e-10,
341 "all-critical criticality_risk must be 1.0, got {r}"
342 );
343 }
344
345 #[test]
346 fn test_criticality_risk_empty_is_one() {
347 let r = criticality_risk(&[]);
348 assert!(
349 (r - 1.0).abs() < 1e-10,
350 "empty criticality_risk must be 1.0, got {r}"
351 );
352 }
353
354 #[test]
355 fn test_criticality_risk_known_distribution() {
356 let bands = [
359 CriticalityBand::Critical,
360 CriticalityBand::High,
361 CriticalityBand::Medium,
362 CriticalityBand::Low,
363 ];
364 let expected = 10.0_f64 / 16.0_f64;
365 let r = criticality_risk(&bands);
366 assert!((r - expected).abs() < 1e-10, "expected {expected}, got {r}");
367 }
368
369 #[test]
370 fn test_criticality_risk_all_low_is_one_quarter() {
371 let bands = [CriticalityBand::Low; 4];
373 let r = criticality_risk(&bands);
374 assert!(
375 (r - 0.25).abs() < 1e-10,
376 "all-low criticality_risk must be 0.25, got {r}"
377 );
378 }
379
380 #[test]
383 fn test_band_flag_below_0_4_is_over_decompressed() {
384 assert_eq!(band_flag(0.0), RiskBandFlag::OverDecompressed);
385 assert_eq!(band_flag(0.39), RiskBandFlag::OverDecompressed);
386 assert_eq!(band_flag(0.4), RiskBandFlag::InTarget);
388 }
389
390 #[test]
391 fn test_band_flag_in_target_range() {
392 assert_eq!(band_flag(0.4), RiskBandFlag::InTarget);
393 assert_eq!(band_flag(0.57), RiskBandFlag::InTarget);
394 assert_eq!(band_flag(0.75), RiskBandFlag::InTarget);
395 }
396
397 #[test]
398 fn test_band_flag_above_0_75_is_high_risk() {
399 assert_eq!(band_flag(0.751), RiskBandFlag::HighRisk);
400 assert_eq!(band_flag(1.0), RiskBandFlag::HighRisk);
401 assert_eq!(band_flag(0.75), RiskBandFlag::InTarget);
403 }
404
405 #[test]
408 fn test_all_critical_schedule_end_to_end() {
409 let floats = [0u64, 0, 0, 0];
411 let max_float = 0u64;
412
413 assert_eq!(
415 activity_risk(&floats, max_float),
416 ActivityRiskResult::AllCritical
417 );
418
419 for &f in &floats {
421 assert_eq!(classify(f, max_float), CriticalityBand::Critical);
422 }
423
424 let bands: Vec<_> = floats.iter().map(|&f| classify(f, max_float)).collect();
426 let cr = criticality_risk(&bands);
427 assert!(
428 (cr - 1.0).abs() < 1e-10,
429 "all-critical criticality_risk must be 1.0, got {cr}"
430 );
431 }
432}