292 lines
11 KiB
Rust
292 lines
11 KiB
Rust
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}"
|
||
);
|
||
}
|