use crate::report::{PlanningWarning, WarningSeverity};
use crate::score::Score01;
#[derive(Debug, Clone, PartialEq)]
pub struct StructuralDiagnosticEffects {
pub abstain_reason: Option<String>,
pub warnings: Vec<PlanningWarning>,
pub confidence_penalty: Score01,
}
pub fn structural_effects(
report: &mikiwame::MaterialDiagnosticReport,
) -> StructuralDiagnosticEffects {
let mut warnings: Vec<PlanningWarning> = report
.findings
.iter()
.filter(|f| f.severity != mikiwame::Severity::Info)
.map(|f| PlanningWarning {
message: format!(
"mikiwame finding {}: {} ({:?} severity)",
f.code.as_str(),
f.explanation,
f.severity
),
severity: map_severity(f.severity),
})
.collect();
let (abstain_reason, verdict_penalty): (Option<String>, f64) = match report.overall.verdict {
mikiwame::Verdict::StructurallyConsistent => (None, 0.0),
mikiwame::Verdict::ReviewRecommended => (None, 0.3),
mikiwame::Verdict::OutOfDomain => (None, 0.5),
mikiwame::Verdict::StrongAnomalyDetected => (
Some(
"mikiwame reported a strong structural anomaly (Verdict::StrongAnomalyDetected)"
.to_string(),
),
1.0,
),
mikiwame::Verdict::InvalidInput => (
Some(
"mikiwame could not validate the input structure (Verdict::InvalidInput)"
.to_string(),
),
1.0,
),
};
let applicability_penalty = match report.applicability.level {
mikiwame::ApplicabilityLevel::FullyApplicable => 0.0,
mikiwame::ApplicabilityLevel::PartiallyApplicable => 0.3,
mikiwame::ApplicabilityLevel::LimitedApplicability => 0.6,
mikiwame::ApplicabilityLevel::NotApplicable => 1.0,
_ => {
warnings.push(PlanningWarning {
message: format!(
"mikiwame returned an applicability level this adapter does not \
recognize ({:?}); treating it as limited applicability rather than \
assuming full applicability",
report.applicability.level
),
severity: WarningSeverity::Caution,
});
0.6
}
};
let confidence_penalty = verdict_penalty.max(applicability_penalty);
StructuralDiagnosticEffects {
abstain_reason,
warnings,
confidence_penalty: Score01::new(confidence_penalty)
.expect("verdict_penalty and applicability_penalty are hardcoded within [0, 1]"),
}
}
fn map_severity(severity: mikiwame::Severity) -> WarningSeverity {
match severity {
mikiwame::Severity::Info => WarningSeverity::Info,
mikiwame::Severity::Low | mikiwame::Severity::Medium => WarningSeverity::Caution,
mikiwame::Severity::High | mikiwame::Severity::Critical => WarningSeverity::Severe,
_ => WarningSeverity::Severe,
}
}
#[cfg(test)]
mod tests {
use super::*;
use mikiwame::{AnalysisConfig, OwnedStructure, Site};
fn cubic_lattice(a: f64) -> [[f64; 3]; 3] {
[[a, 0.0, 0.0], [0.0, a, 0.0], [0.0, 0.0, a]]
}
fn site(element: &str, frac: [f64; 3]) -> Site {
site_with_occupancy(element, frac, 1.0)
}
fn site_with_occupancy(element: &str, frac: [f64; 3], occupancy: f64) -> Site {
Site {
element: element.to_string(),
fractional: frac,
occupancy,
}
}
fn clean_structure() -> OwnedStructure {
OwnedStructure::new(
cubic_lattice(5.64),
vec![site("Na", [0.0, 0.0, 0.0]), site("Cl", [0.5, 0.5, 0.5])],
)
}
#[test]
fn structurally_consistent_report_proceeds_with_no_penalty() {
let report = mikiwame::analyze(&clean_structure(), &AnalysisConfig::default());
let effects = structural_effects(&report);
assert_eq!(effects.abstain_reason, None);
assert_eq!(effects.confidence_penalty, Score01::new(0.0).unwrap());
}
#[test]
fn invalid_input_structure_produces_an_abstain_reason() {
let empty = OwnedStructure::new(cubic_lattice(5.0), vec![]);
let report = mikiwame::analyze(&empty, &AnalysisConfig::default());
assert_eq!(report.overall.verdict, mikiwame::Verdict::InvalidInput);
let effects = structural_effects(&report);
assert!(effects.abstain_reason.is_some());
assert_eq!(effects.confidence_penalty, Score01::new(1.0).unwrap());
}
#[test]
fn a_structural_finding_becomes_a_planning_warning() {
let duplicate_site = OwnedStructure::new(
cubic_lattice(5.64),
vec![
site("Na", [0.0, 0.0, 0.0]),
site("Na", [0.0, 0.0, 0.0]),
site("Cl", [0.5, 0.5, 0.5]),
],
);
let report = mikiwame::analyze(&duplicate_site, &AnalysisConfig::default());
assert!(
report
.findings
.iter()
.any(|f| f.code == mikiwame::FindingCode::SiteDuplicate)
);
let effects = structural_effects(&report);
assert!(
effects
.warnings
.iter()
.any(|w| w.message.contains("SITE_DUPLICATE"))
);
}
#[test]
fn a_high_severity_finding_maps_to_a_severe_warning() {
let overfull = OwnedStructure::new(
cubic_lattice(5.64),
vec![
site_with_occupancy("Na", [0.25, 0.25, 0.25], 1.0),
site_with_occupancy("K", [0.25, 0.25, 0.25], 1.0),
],
);
let report = mikiwame::analyze(&overfull, &AnalysisConfig::default());
assert!(report.findings.iter().any(|f| {
f.code == mikiwame::FindingCode::DisorderOccupancySumExceedsOne
&& f.severity == mikiwame::Severity::High
}));
let effects = structural_effects(&report);
assert!(
effects
.warnings
.iter()
.any(|w| w.message.contains("DISORDER_OCCUPANCY_SUM_EXCEEDS_ONE")
&& w.severity == WarningSeverity::Severe)
);
}
#[test]
fn info_severity_findings_alone_do_not_produce_warnings_or_abstention() {
let disordered = OwnedStructure::new(
cubic_lattice(5.64),
vec![
site_with_occupancy("Na", [0.25, 0.25, 0.25], 0.5),
site_with_occupancy("K", [0.25, 0.25, 0.25], 0.5),
],
);
let report = mikiwame::analyze(&disordered, &AnalysisConfig::default());
assert_eq!(
report.overall.verdict,
mikiwame::Verdict::StructurallyConsistent,
"unexpected findings: {:?}",
report.findings
);
let effects = structural_effects(&report);
assert_eq!(effects.abstain_reason, None);
assert!(effects.warnings.is_empty());
}
}