use chematic_core::Molecule;
#[cfg(feature = "trained-solubility-mlp")]
use chematic_fp::ecfp4;
#[cfg(all(feature = "trained-solubility-mlp", not(target_endian = "little")))]
compile_error!(
"trained-solubility-mlp uses little-endian binary weight files; \
big-endian compilation targets are not supported"
);
pub const MLP_SOLUBILITY_TRAINED: bool = cfg!(feature = "trained-solubility-mlp");
#[cfg(feature = "trained-solubility-mlp")]
const W1_BYTES: &[u8] = include_bytes!("../../../../data/mlp_w1.bin");
#[cfg(feature = "trained-solubility-mlp")]
const B1_BYTES: &[u8] = include_bytes!("../../../../data/mlp_b1.bin");
#[cfg(feature = "trained-solubility-mlp")]
static WEIGHTS: std::sync::OnceLock<(Vec<f32>, Vec<f32>)> = std::sync::OnceLock::new();
pub fn mlp_solubility(mol: &Molecule) -> f64 {
#[cfg(feature = "trained-solubility-mlp")]
{
let fp = ecfp4(mol);
let (w, b) = WEIGHTS.get_or_init(|| (f32_from_bytes(W1_BYTES), f32_from_bytes(B1_BYTES)));
if w.len() != 2048 || b.is_empty() {
return crate::esol::esol_solubility(mol);
}
let mut acc = b[0];
for i in 0..2048 {
if fp.get(i) {
acc += w[i];
}
}
acc as f64
}
#[cfg(not(feature = "trained-solubility-mlp"))]
{
crate::esol::esol_solubility(mol)
}
}
#[cfg(feature = "trained-solubility-mlp")]
fn f32_from_bytes(bytes: &[u8]) -> Vec<f32> {
bytes
.chunks_exact(4)
.map(|c| f32::from_le_bytes([c[0], c[1], c[2], c[3]]))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use chematic_smiles::parse;
fn logs(smiles: &str) -> f64 {
mlp_solubility(&parse(smiles).unwrap())
}
#[test]
fn test_output_finite_and_plausible() {
for smi in ["c1ccccc1", "CCO", "CC(=O)Oc1ccccc1C(=O)O", "CCCCCCCC"] {
let s = logs(smi);
assert!(s.is_finite(), "logS must be finite for {smi}");
assert!(
s > -15.0 && s < 5.0,
"logS={s:.2} out of plausible range for {smi}"
);
}
}
#[test]
fn test_water_more_soluble_than_octane() {
let water = logs("O");
let octane = logs("CCCCCCCC");
assert!(
water > octane,
"water ({water:.2}) should be more soluble than octane ({octane:.2})"
);
}
#[test]
fn test_placeholder_matches_esol() {
if !MLP_SOLUBILITY_TRAINED {
let mol = parse("c1ccccc1").unwrap();
let esol = crate::esol::esol_solubility(&mol);
let mlp = mlp_solubility(&mol);
assert!(
(mlp - esol).abs() < 1e-9,
"placeholder must match ESOL: mlp={mlp:.4} esol={esol:.4}"
);
}
}
}