use chematic::chem::{brenk_matches_detailed, identify_functional_groups};
use chematic::core::Molecule;
use crate::report::{
AtomIndex, ComponentScore, Contribution, Finding, FindingCode, FindingEvidence, FindingRef,
ProbabilityLikeScore, Severity, finite_or_zero,
};
use crate::rules::{
FG_BURDEN_SCALE, FG_CONFIDENCE_BUDGET_EXHAUSTED, FG_DENSE_GROUP_COUNT_THRESHOLD,
FG_WEIGHT_PER_DISTINCT_GROUP, FG_WEIGHT_PER_REACTIVE_GROUP,
};
pub(crate) struct FunctionalGroupLiabilityOutcome {
pub(crate) score: ComponentScore,
pub(crate) findings: Vec<Finding>,
pub(crate) contributions: Vec<Contribution>,
}
pub(crate) fn compute(mol: &Molecule) -> FunctionalGroupLiabilityOutcome {
let alerts = brenk_matches_detailed(mol);
let mut findings = Vec::with_capacity(alerts.len());
let mut contributions = Vec::with_capacity(alerts.len());
for (name, atom_indices) in &alerts {
let budget_exhausted = atom_indices.is_empty();
let confidence = if budget_exhausted {
FG_CONFIDENCE_BUDGET_EXHAUSTED
} else {
1.0
};
let atoms: Vec<AtomIndex> = atom_indices
.iter()
.map(|&idx| AtomIndex::from(idx))
.collect();
let evidence = FindingEvidence {
value: Some(atoms.len() as f64),
threshold: None,
};
let explanation = crate::explain::render(
FindingCode::FunctionalGroupReactive,
evidence,
atoms.len(),
Some(name),
);
findings.push(Finding {
code: FindingCode::FunctionalGroupReactive,
severity: Severity::Medium,
confidence: ProbabilityLikeScore::new(confidence),
atoms,
evidence,
explanation: explanation.clone(),
});
contributions.push(Contribution {
code: FindingCode::FunctionalGroupReactive,
name: explanation,
contribution: ProbabilityLikeScore::new(finite_or_zero(FG_WEIGHT_PER_REACTIVE_GROUP)),
});
}
let groups = identify_functional_groups(mol);
let dense_excess = groups.len().saturating_sub(1);
let dense_weight = finite_or_zero(FG_WEIGHT_PER_DISTINCT_GROUP * dense_excess as f64);
if groups.len() > FG_DENSE_GROUP_COUNT_THRESHOLD {
let evidence = FindingEvidence {
value: Some(groups.len() as f64),
threshold: Some(FG_DENSE_GROUP_COUNT_THRESHOLD as f64),
};
let explanation =
crate::explain::render(FindingCode::FunctionalGroupDense, evidence, 0, None);
findings.push(Finding {
code: FindingCode::FunctionalGroupDense,
severity: Severity::Low,
confidence: ProbabilityLikeScore::new(1.0),
atoms: Vec::new(),
evidence,
explanation: explanation.clone(),
});
contributions.push(Contribution {
code: FindingCode::FunctionalGroupDense,
name: explanation,
contribution: ProbabilityLikeScore::new(dense_weight),
});
}
let raw = finite_or_zero(FG_WEIGHT_PER_REACTIVE_GROUP * alerts.len() as f64 + dense_weight);
let normalized = ProbabilityLikeScore::new(1.0 - (-raw / FG_BURDEN_SCALE).exp());
let confidence = findings
.iter()
.map(|f| f.confidence.value())
.fold(1.0_f64, f64::min);
let score = ComponentScore {
raw,
normalized,
confidence: ProbabilityLikeScore::new(confidence),
contribution: normalized.value(),
findings: (0..findings.len()).map(FindingRef).collect(),
};
FunctionalGroupLiabilityOutcome {
score,
findings,
contributions,
}
}