use chematic_core::{AtomIdx, Molecule};
use crate::svg::RenderOptions;
pub fn similarity_map_svg(mol: &Molecule, weights: &[f64]) -> String {
similarity_map_svg_opts(mol, weights, &RenderOptions::default())
}
pub fn similarity_map_svg_opts(mol: &Molecule, weights: &[f64], opts: &RenderOptions) -> String {
let mut opts = opts.clone();
let max_abs = weights
.iter()
.copied()
.map(f64::abs)
.fold(0.0_f64, f64::max);
if max_abs > 1e-9 {
for (i, &w) in weights.iter().enumerate() {
if i >= mol.atom_count() {
break;
}
let t = (w / max_abs).clamp(-1.0, 1.0);
let color = weight_to_hex(t);
opts.atom_color_map.insert(AtomIdx(i as u32), color);
}
}
crate::depict_svg_opts(mol, &opts)
}
fn weight_to_hex(t: f64) -> String {
let t = t.clamp(-1.0, 1.0);
let abs_t = t.abs();
let mid = 112.0_f64;
let full = 255.0_f64;
let accent = (full - (full - mid) * abs_t).round() as u8;
let (r, g, b) = if t >= 0.0 {
(accent, accent, 255u8)
} else {
(255u8, accent, accent)
};
format!("#{r:02X}{g:02X}{b:02X}")
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
fn mol(s: &str) -> Molecule {
parse(s).unwrap_or_else(|e| panic!("parse '{s}': {e}"))
}
#[test]
fn test_weight_to_hex_extremes() {
assert_eq!(weight_to_hex(0.0), "#FFFFFF");
assert_eq!(weight_to_hex(1.0), "#7070FF");
assert_eq!(weight_to_hex(-1.0), "#FF7070");
}
#[test]
fn test_weight_to_hex_half() {
let color = weight_to_hex(0.5);
assert_eq!(color, "#B8B8FF");
let color_neg = weight_to_hex(-0.5);
assert_eq!(color_neg, "#FFB8B8");
}
#[test]
fn test_similarity_map_svg_returns_valid_svg() {
let m = mol("CC(=O)O");
let weights = vec![0.1, -0.2, 0.5, 0.3];
let svg = similarity_map_svg(&m, &weights);
assert!(
svg.starts_with("<svg") || svg.contains("<svg"),
"not SVG: {}",
&svg[..50.min(svg.len())]
);
assert!(svg.contains("fill"), "no fill attributes in SVG");
}
#[test]
fn test_similarity_map_zero_weights_no_circles() {
let m = mol("c1ccccc1");
let weights = vec![0.0; 6];
let svg = similarity_map_svg(&m, &weights);
assert!(
!svg.contains("7070FF"),
"should be no blue circles for zero weights"
);
assert!(
!svg.contains("FF7070"),
"should be no red circles for zero weights"
);
}
#[test]
fn test_similarity_map_short_weights() {
let m = mol("c1ccccc1");
let weights = vec![0.5, -0.5]; let svg = similarity_map_svg(&m, &weights);
assert!(svg.contains("<svg") || svg.contains("svg"));
}
#[test]
fn test_similarity_map_contains_atom_colors() {
let m = mol("CC");
let weights = vec![1.0, -1.0];
let svg = similarity_map_svg(&m, &weights);
assert!(svg.contains("7070FF"), "blue atom circle missing");
assert!(svg.contains("FF7070"), "red atom circle missing");
}
}