use chematic::core::Molecule;
use crate::fragment_corpus::FragmentCorpus;
use crate::report::{Finding, FindingCode, FindingEvidence, ProbabilityLikeScore, Severity};
use crate::rules::FRAGMENT_PRECEDENT_FINDING_THRESHOLD;
pub(crate) struct FragmentPrecedentOutcome {
pub(crate) precedent_penalty: f64,
pub(crate) precedent_support: f64,
pub(crate) finding: Option<Finding>,
}
fn neutral_outcome() -> FragmentPrecedentOutcome {
FragmentPrecedentOutcome {
precedent_penalty: 0.0,
precedent_support: 0.0,
finding: None,
}
}
pub(crate) fn compute(mol: &Molecule, corpus: &FragmentCorpus) -> FragmentPrecedentOutcome {
let counts = chematic::fp::morgan_fp_counts(mol, corpus.radius);
if counts.is_empty() {
return neutral_outcome();
}
let total = corpus.total_molecules_processed.max(1) as f64;
let mean_document_frequency = counts
.keys()
.map(|hash| corpus.frequency.get(hash).copied().unwrap_or(0) as f64 / total)
.sum::<f64>()
/ counts.len() as f64;
let percentile = corpus.percentile_rank(mean_document_frequency);
let signed_signal = (1.0 - 2.0 * percentile).clamp(-1.0, 1.0);
let precedent_penalty = signed_signal.max(0.0);
let precedent_support = (-signed_signal).max(0.0);
let finding = if signed_signal.abs() >= FRAGMENT_PRECEDENT_FINDING_THRESHOLD {
let code = if signed_signal > 0.0 {
FindingCode::FragmentPrecedentWeak
} else {
FindingCode::FragmentPrecedentStrong
};
let evidence = FindingEvidence {
value: Some(percentile * 100.0),
threshold: None,
};
let explanation = crate::explain::render(code, evidence, 0, None);
Some(Finding {
code,
severity: Severity::Low,
confidence: ProbabilityLikeScore::new(1.0),
atoms: Vec::new(),
evidence,
explanation,
})
} else {
None
};
FragmentPrecedentOutcome {
precedent_penalty,
precedent_support,
finding,
}
}