tcjs/tests/calculator_tests.rs

292 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use ceramic_radioactivity::{
calculate_sample, CalibrationParams, Conclusion, DecorClass, MaterialType, NuclideMeasurements,
SampleInput, Validity, Verdict,
};
fn calibration(factor: f64, expanded_uncertainty_percent: f64) -> CalibrationParams {
CalibrationParams {
factor,
expanded_uncertainty_percent,
coverage_factor: 2.0,
}
}
fn default_input() -> SampleInput {
SampleInput {
ra: NuclideMeasurements {
measured_values: vec![100.0, 102.0, 98.0, 101.0, 99.0, 100.0],
calibration: calibration(0.916, 6.3),
},
th: NuclideMeasurements {
measured_values: vec![110.0, 111.0, 109.0, 110.0, 112.0, 108.0],
calibration: calibration(0.884, 6.9),
},
k: NuclideMeasurements {
measured_values: vec![560.0, 565.0, 555.0, 562.0, 558.0, 561.0],
calibration: calibration(0.961, 6.7),
},
material_type: MaterialType::BuildingMainBody,
sample_id: None,
calculation_date: None,
}
}
/// 由目标校准比活度构造 n=6 的等值输入A 类不确定度为 0仅保留 B 类)。
fn from_calibrated(ra_cal: f64, th_cal: f64, k_cal: f64) -> SampleInput {
let mut input = default_input();
input.ra.measured_values = vec![ra_cal / 0.916; 6];
input.th.measured_values = vec![th_cal / 0.884; 6];
input.k.measured_values = vec![k_cal / 0.961; 6];
input
}
#[test]
fn calculates_indices_and_ok_conclusion_for_six_measurements() {
let result = calculate_sample(default_input()).expect("valid sample should calculate");
assert_close(result.ra.mean_measured, 100.0, 1e-9);
assert_close(result.ra.mean_calibrated, 91.6, 1e-9);
assert_close(result.th.mean_calibrated, 97.24, 1e-9);
assert_close(result.k.mean_calibrated, 538.320_166_666_666_6, 1e-9);
assert_close(result.ira.value, 0.458, 1e-9);
assert_close(result.ir.value, 0.749_739_035_821_535_9, 1e-9);
assert_eq!(result.conclusion, Conclusion::Ok);
assert_eq!(result.analysis.verdict, Verdict::Qualified);
}
/// 对齐 PDF 单次测量算例A1=83.439, A2=116.995, A3=554.268)。
#[test]
fn single_measurement_matches_pdf_example() {
let input = SampleInput {
ra: NuclideMeasurements {
measured_values: vec![83.439],
calibration: calibration(0.916, 6.3),
},
th: NuclideMeasurements {
measured_values: vec![116.995],
calibration: calibration(0.884, 6.9),
},
k: NuclideMeasurements {
measured_values: vec![554.268],
calibration: calibration(0.961, 6.7),
},
material_type: MaterialType::BuildingMainBody,
sample_id: Some("PDF-EX".to_string()),
calculation_date: Some("2026-06-11".to_string()),
};
let result = calculate_sample(input).expect("single measurement should calculate");
assert_eq!(result.measurement_count, 1);
// A 类不确定度为 0。
assert_close(result.ra.type_a_uncertainty, 0.0, 1e-12);
// 2.1 检测结果。
assert_close(result.ira.value, 0.38, 5e-3);
assert_close(result.ir.value, 0.73, 5e-3);
// 2.2.3 标准不确定度、2.2.4 扩展不确定度、2.2.6 真值区间。
assert_close(result.ira.standard_uncertainty, 0.012, 5e-4);
assert_close(result.ira.expanded_uncertainty, 0.024, 1e-3);
// PDF 区间用已四舍五入的 0.38±0.024 得 0.36/0.40;此处用未舍入值,放宽容差。
assert_close(result.ira.p2_5, 0.36, 1e-2);
assert_close(result.ira.p97_5, 0.40, 1e-2);
assert_close(result.ir.standard_uncertainty, 0.016, 5e-4);
assert_close(result.ir.expanded_uncertainty, 0.032, 1e-3);
// 2.2.5 相对扩展不确定度 k=2。
assert_close(result.ira.relative_expanded_uncertainty_percent, 6.3, 0.2);
assert_close(result.ir.relative_expanded_uncertainty_percent, 4.4, 0.2);
// 3.1 有效性 + 3.2 判定。
assert_close(result.analysis.total_calibrated_activity, 712.5, 1.0);
assert_eq!(result.analysis.validity, Validity::UncertaintyAcceptable);
assert_eq!(result.analysis.verdict, Verdict::Qualified);
}
#[test]
fn low_activity_sample_is_exempt_and_valid() {
let input = SampleInput {
ra: NuclideMeasurements {
measured_values: vec![2.0],
calibration: calibration(0.916, 6.3),
},
th: NuclideMeasurements {
measured_values: vec![2.0],
calibration: calibration(0.884, 6.9),
},
k: NuclideMeasurements {
measured_values: vec![2.0],
calibration: calibration(0.961, 6.7),
},
material_type: MaterialType::BuildingMainBody,
sample_id: None,
calculation_date: None,
};
let result = calculate_sample(input).expect("low activity sample should calculate");
assert!(result.analysis.total_calibrated_activity <= 37.0);
assert_eq!(result.analysis.validity, Validity::LowActivityExempt);
assert_eq!(result.analysis.verdict, Verdict::Qualified);
}
#[test]
fn high_uncertainty_above_37_is_invalid() {
let mut input = default_input();
input.ra.measured_values = vec![10.0, 200.0, 400.0];
input.th.measured_values = vec![10.0, 200.0, 400.0];
input.k.measured_values = vec![10.0, 200.0, 400.0];
let result = calculate_sample(input).expect("valid sample should calculate");
assert!(result.analysis.total_calibrated_activity > 37.0);
assert_eq!(result.analysis.validity, Validity::Invalid);
assert_eq!(result.analysis.verdict, Verdict::InvalidResult);
}
#[test]
fn main_body_unqualified_when_interval_above_limit() {
// IRa ≈ 1.5,区间整体高于 1.0。
let result =
calculate_sample(from_calibrated(300.0, 50.0, 50.0)).expect("valid sample should calculate");
assert!(result.ira.p2_5 > 1.0);
assert_eq!(result.analysis.verdict, Verdict::Unqualified);
}
#[test]
fn main_body_needs_more_measurements_when_interval_straddles_limit() {
// IRa = 1.0,区间跨越 1.0。
let result =
calculate_sample(from_calibrated(200.0, 50.0, 50.0)).expect("valid sample should calculate");
assert!(result.ira.p2_5 < 1.0 && result.ira.p97_5 > 1.0);
assert_eq!(result.analysis.verdict, Verdict::NeedMoreMeasurements);
}
#[test]
fn decorative_material_classifies_into_tiers() {
// A 类IRa、Ir 均低。
let a = calculate_sample(decorative(100.0, 100.0, 100.0)).expect("valid");
assert_eq!(a.analysis.verdict, Verdict::DecorativeClass(DecorClass::A));
// B 类Ir 超 A 限(1.3) 但在 B 限(1.9) 内IRa 低。
let b = calculate_sample(decorative(100.0, 317.0, 42.0)).expect("valid");
assert_eq!(b.analysis.verdict, Verdict::DecorativeClass(DecorClass::B));
// C 类Ir 超 B 限(1.9) 但在 C 限(2.8) 内。
let c = calculate_sample(decorative(100.0, 520.0, 100.0)).expect("valid");
assert_eq!(c.analysis.verdict, Verdict::DecorativeClass(DecorClass::C));
// 不合格Ir 超 C 限(2.8)。
let fail = calculate_sample(decorative(100.0, 900.0, 100.0)).expect("valid");
assert_eq!(
fail.analysis.verdict,
Verdict::DecorativeClass(DecorClass::Unqualified)
);
}
fn decorative(ra_cal: f64, th_cal: f64, k_cal: f64) -> SampleInput {
let mut input = from_calibrated(ra_cal, th_cal, k_cal);
input.material_type = MaterialType::DecorativeMaterial;
input
}
#[test]
fn asks_for_more_measurements_when_uncertainty_is_high_and_n_is_below_six() {
let mut input = default_input();
input.ra.measured_values = vec![10.0, 200.0, 400.0];
input.th.measured_values = vec![10.0, 200.0, 400.0];
input.k.measured_values = vec![10.0, 200.0, 400.0];
let result = calculate_sample(input).expect("valid sample should calculate");
assert_eq!(result.measurement_count, 3);
assert_eq!(result.conclusion, Conclusion::IncreaseMeasurementsToSix);
}
#[test]
fn asks_for_recalibration_when_uncertainty_is_high_after_six_measurements() {
let mut input = default_input();
input.ra.measured_values = vec![10.0, 200.0, 400.0, 10.0, 200.0, 400.0];
input.th.measured_values = vec![10.0, 200.0, 400.0, 10.0, 200.0, 400.0];
input.k.measured_values = vec![10.0, 200.0, 400.0, 10.0, 200.0, 400.0];
let result = calculate_sample(input).expect("valid sample should calculate");
assert_eq!(result.measurement_count, 6);
assert_eq!(result.conclusion, Conclusion::RecalibrateInstrument);
}
#[test]
fn rejects_mismatched_measurement_counts() {
let mut input = default_input();
input.k.measured_values.pop();
let err = calculate_sample(input).expect_err("mismatched counts should fail");
assert!(err.to_string().contains("same measurement count"));
}
#[test]
fn monte_carlo_matches_analytical_mean_and_uncertainty() {
let result = calculate_sample(default_input()).expect("valid sample should calculate");
assert_eq!(result.mcm.iterations, 10_000);
// MCM 仿真均值/标准偏差应与 GUM 解析结果一致(仿真随机误差在 ~1% 量级)。
assert_close(result.mcm.ira.mean, result.ira.value, 5e-4);
assert_close(result.mcm.ir.mean, result.ir.value, 5e-4);
assert_close(
result.mcm.ira.std_dev,
result.ira.standard_uncertainty,
result.ira.standard_uncertainty * 0.05,
);
assert_close(
result.mcm.ir.std_dev,
result.ir.standard_uncertainty,
result.ir.standard_uncertainty * 0.05,
);
// 95% 包含区间应包住均值。
assert!(result.mcm.ira.p2_5 < result.mcm.ira.mean);
assert!(result.mcm.ira.mean < result.mcm.ira.p97_5);
// 默认样本的 IRa≈0.46、Ir≈0.75 都远低于标准值 1.0,合格概率应为 1。
assert_close(result.mcm.ira.pass_probability, 1.0, 1e-12);
assert_close(result.mcm.ir.pass_probability, 1.0, 1e-12);
assert_close(result.mcm.overall_pass_probability, 1.0, 1e-12);
assert_close(result.mcm.overall_fail_probability, 0.0, 1e-12);
}
#[test]
fn monte_carlo_is_deterministic_for_same_input() {
let first = calculate_sample(default_input()).expect("valid sample should calculate");
let second = calculate_sample(default_input()).expect("valid sample should calculate");
assert_eq!(first.mcm, second.mcm);
}
#[test]
fn monte_carlo_gives_about_half_pass_probability_when_index_equals_limit() {
// 主体材料 IRa 标准值为 1.0;构造 IRa=1.0 的样本,合格概率应接近 0.5。
let result =
calculate_sample(from_calibrated(200.0, 50.0, 50.0)).expect("valid sample should calculate");
assert_close(result.ira.value, 1.0, 1e-9);
assert_close(result.mcm.ira.pass_probability, 0.5, 0.03);
assert_close(
result.mcm.ira.fail_probability,
1.0 - result.mcm.ira.pass_probability,
1e-12,
);
}
fn assert_close(actual: f64, expected: f64, tolerance: f64) {
assert!(
(actual - expected).abs() <= tolerance,
"actual {actual} expected {expected} tolerance {tolerance}"
);
}