use chematic::core::Molecule;
use chematic::perception::{RingSystemKind, find_ring_families, find_sssr};
use crate::report::{
AtomIndex, ComponentScore, Contribution, Finding, FindingCode, FindingEvidence, FindingRef,
ProbabilityLikeScore, Severity, finite_or_zero,
};
use crate::rules::{
MACROCYCLE_MIN_RING_SIZE, RING_BURDEN_SCALE, RING_FUSED_DENSITY_FINDING_THRESHOLD,
RING_WEIGHT_BRIDGED, RING_WEIGHT_FUSED_BASE, RING_WEIGHT_FUSED_DENSITY,
RING_WEIGHT_MACROCYCLE_BONUS, RING_WEIGHT_SIMPLE, RING_WEIGHT_SPIRO,
};
pub(crate) struct RingTopologyOutcome {
pub(crate) score: ComponentScore,
pub(crate) findings: Vec<Finding>,
pub(crate) contributions: Vec<Contribution>,
}
pub(crate) fn compute(mol: &Molecule) -> RingTopologyOutcome {
let sssr = find_sssr(mol);
let families = find_ring_families(mol, &sssr);
let mut findings = Vec::new();
let mut contributions = Vec::new();
let mut raw = 0.0;
let push = |findings: &mut Vec<Finding>,
contributions: &mut Vec<Contribution>,
code: FindingCode,
severity: Severity,
atoms: Vec<AtomIndex>,
evidence: FindingEvidence,
weight: f64| {
let atom_count = atoms.len();
let explanation = crate::explain::render(code, evidence, atom_count, None);
findings.push(Finding {
code,
severity,
confidence: ProbabilityLikeScore::new(1.0),
atoms,
evidence,
explanation: explanation.clone(),
});
contributions.push(Contribution {
code,
name: explanation,
contribution: ProbabilityLikeScore::new(finite_or_zero(weight)),
});
};
for family in &families {
let atoms: Vec<AtomIndex> = family.atoms.iter().map(|&a| AtomIndex::from(a)).collect();
let max_ring_size = family
.ring_indices
.iter()
.map(|&i| sssr.rings()[i].len())
.max()
.unwrap_or(0);
let kind_weight = match family.kind {
RingSystemKind::Simple => RING_WEIGHT_SIMPLE,
RingSystemKind::Spiro => {
push(
&mut findings,
&mut contributions,
FindingCode::RingSpiro,
Severity::Medium,
atoms.clone(),
FindingEvidence::default(),
RING_WEIGHT_SPIRO,
);
RING_WEIGHT_SPIRO
}
RingSystemKind::Bridged => {
push(
&mut findings,
&mut contributions,
FindingCode::RingBridgedComplexity,
Severity::High,
atoms.clone(),
FindingEvidence::default(),
RING_WEIGHT_BRIDGED,
);
RING_WEIGHT_BRIDGED
}
RingSystemKind::Fused => {
let ring_size_sum: usize = family
.ring_indices
.iter()
.map(|&i| sssr.rings()[i].len())
.sum();
let overlap = ring_size_sum.saturating_sub(family.atoms.len());
let density = if family.atoms.is_empty() {
0.0
} else {
overlap as f64 / family.atoms.len() as f64
};
let weight = RING_WEIGHT_FUSED_BASE + RING_WEIGHT_FUSED_DENSITY * density;
if density > RING_FUSED_DENSITY_FINDING_THRESHOLD {
push(
&mut findings,
&mut contributions,
FindingCode::RingFusedDense,
Severity::Medium,
atoms.clone(),
FindingEvidence {
value: Some(density),
threshold: Some(RING_FUSED_DENSITY_FINDING_THRESHOLD),
},
weight,
);
}
weight
}
};
raw += kind_weight;
if max_ring_size >= MACROCYCLE_MIN_RING_SIZE {
push(
&mut findings,
&mut contributions,
FindingCode::RingMacrocycle,
Severity::Medium,
atoms.clone(),
FindingEvidence {
value: Some(max_ring_size as f64),
threshold: Some(MACROCYCLE_MIN_RING_SIZE as f64),
},
RING_WEIGHT_MACROCYCLE_BONUS,
);
raw += RING_WEIGHT_MACROCYCLE_BONUS;
}
}
let raw = finite_or_zero(raw);
let normalized = ProbabilityLikeScore::new(1.0 - (-raw / RING_BURDEN_SCALE).exp());
let mut ranked: Vec<(Finding, Contribution)> =
findings.into_iter().zip(contributions).collect();
ranked.sort_by(|(_, a), (_, b)| {
b.contribution
.value()
.partial_cmp(&a.contribution.value())
.unwrap_or(std::cmp::Ordering::Equal)
});
let (findings, contributions): (Vec<Finding>, Vec<Contribution>) = ranked.into_iter().unzip();
let score = ComponentScore {
raw,
normalized,
confidence: ProbabilityLikeScore::new(1.0),
contribution: normalized.value(),
findings: (0..findings.len()).map(FindingRef).collect(),
};
RingTopologyOutcome {
score,
findings,
contributions,
}
}