use wasm_bindgen::prelude::*;
const WASM_MAX_ATOMS: usize = 10_000;
const WASM_MAX_INPUT_BYTES: usize = 1_000_000;
const WASM_MAX_BATCH_ITEMS: usize = 1_024;
const WASM_MAX_JSON_STRING_BYTES: usize = 100_000;
const WASM_MAX_SMARTS_MATCHES: usize = 10_000;
mod mol_3d;
mod mol_depict;
mod mol_descriptors;
mod mol_edit;
mod mol_fingerprints;
mod mol_io;
mod mol_reactions;
#[cfg(test)]
mod tests;
pub use mol_3d::*;
pub use mol_depict::*;
pub use mol_descriptors::*;
pub use mol_edit::*;
pub use mol_fingerprints::*;
pub use mol_io::*;
pub use mol_reactions::*;
#[wasm_bindgen(start)]
pub fn start() {
std::panic::set_hook(Box::new(|info| {
web_sys::console::error_1(&info.to_string().into());
}));
}
pub mod workflow;
#[wasm_bindgen]
pub struct MolHandle {
inner: std::rc::Rc<chematic_core::Molecule>,
}
#[wasm_bindgen]
impl MolHandle {
pub fn atom_count(&self) -> usize {
self.inner.atom_count()
}
pub fn bond_count(&self) -> usize {
self.inner.bond_count()
}
pub fn formula(&self) -> String {
molecular_formula(&self.inner)
}
pub fn canonical_smiles(&self) -> String {
chematic_smiles::canonical_smiles(&self.inner)
}
pub fn molecular_weight(&self) -> f64 {
chematic_chem::molecular_weight(&self.inner)
}
pub fn tpsa(&self) -> f64 {
chematic_chem::tpsa(&self.inner)
}
pub fn lipinski_passes(&self) -> bool {
chematic_chem::lipinski_passes(&self.inner)
}
pub fn heavy_atom_count(&self) -> usize {
chematic_chem::heavy_atom_count(&self.inner)
}
pub fn hbd_count(&self) -> usize {
chematic_chem::hbd_count(&self.inner)
}
pub fn hba_count(&self) -> usize {
chematic_chem::hba_count(&self.inner)
}
pub fn logp_crippen(&self) -> f64 {
chematic_chem::logp_crippen(&self.inner)
}
pub fn fsp3(&self) -> f64 {
chematic_chem::fsp3(&self.inner)
}
pub fn aromatic_ring_count(&self) -> usize {
chematic_chem::aromatic_ring_count(&self.inner)
}
pub fn qed(&self) -> f64 {
chematic_chem::qed(&self.inner)
}
pub fn exact_mass(&self) -> f64 {
chematic_chem::exact_mass(&self.inner)
}
pub fn rotatable_bond_count(&self) -> usize {
chematic_chem::rotatable_bond_count(&self.inner)
}
pub fn molar_refractivity(&self) -> f64 {
chematic_chem::molar_refractivity(&self.inner)
}
pub fn formal_charge_sum(&self) -> i32 {
chematic_chem::formal_charge_sum(&self.inner)
}
pub fn veber_passes(&self) -> bool {
chematic_chem::veber_passes(&self.inner)
}
pub fn egan_passes(&self) -> bool {
chematic_chem::egan_passes(&self.inner)
}
pub fn reos_passes(&self) -> bool {
chematic_chem::reos_passes(&self.inner)
}
pub fn ghose_passes(&self) -> bool {
chematic_chem::ghose_passes(&self.inner)
}
pub fn num_heteroatoms(&self) -> usize {
chematic_chem::num_heteroatoms(&self.inner)
}
pub fn ring_count(&self) -> usize {
chematic_chem::ring_count(&self.inner)
}
pub fn num_stereocenters(&self) -> usize {
chematic_chem::num_stereocenters(&self.inner)
}
pub fn pains_passes(&self) -> bool {
chematic_chem::pains_passes(&self.inner)
}
pub fn num_aromatic_heterocycles(&self) -> usize {
chematic_chem::num_aromatic_heterocycles(&self.inner)
}
pub fn num_aliphatic_heterocycles(&self) -> usize {
chematic_chem::num_aliphatic_heterocycles(&self.inner)
}
pub fn num_saturated_heterocycles(&self) -> usize {
chematic_chem::num_saturated_heterocycles(&self.inner)
}
pub fn num_spiro_atoms(&self) -> usize {
chematic_chem::num_spiro_atoms(&self.inner)
}
pub fn num_bridgehead_atoms(&self) -> usize {
chematic_chem::num_bridgehead_atoms(&self.inner)
}
pub fn depict_svg(&self) -> String {
chematic_depict::depict_svg(&self.inner)
}
pub fn depict_svg_opts(&self, opts: &DepictOptions) -> String {
chematic_depict::depict_svg_opts(&self.inner, &opts.to_render_options())
}
pub fn depict_png(&self) -> Vec<u8> {
Vec::new()
}
pub fn wiener_index(&self) -> f64 {
chematic_chem::wiener_index(&self.inner)
}
pub fn kappa1(&self) -> f64 {
chematic_chem::kappa1(&self.inner)
}
pub fn kappa2(&self) -> f64 {
chematic_chem::kappa2(&self.inner)
}
pub fn kappa3(&self) -> f64 {
chematic_chem::kappa3(&self.inner)
}
pub fn chi0(&self) -> f64 {
chematic_chem::chi0(&self.inner)
}
pub fn chi1(&self) -> f64 {
chematic_chem::chi1(&self.inner)
}
pub fn chi2(&self) -> f64 {
chematic_chem::chi2(&self.inner)
}
pub fn chi3(&self) -> f64 {
chematic_chem::chi3(&self.inner)
}
pub fn chi4(&self) -> f64 {
chematic_chem::chi4(&self.inner)
}
pub fn chi0v(&self) -> f64 {
chematic_chem::chi0v(&self.inner)
}
pub fn chi1v(&self) -> f64 {
chematic_chem::chi1v(&self.inner)
}
pub fn chi2v(&self) -> f64 {
chematic_chem::chi2v(&self.inner)
}
pub fn chi3v(&self) -> f64 {
chematic_chem::chi3v(&self.inner)
}
pub fn chi4v(&self) -> f64 {
chematic_chem::chi4v(&self.inner)
}
pub fn bertz_ct(&self) -> f64 {
chematic_chem::bertz_ct(&self.inner)
}
pub fn labute_asa(&self) -> f64 {
chematic_chem::labute_asa(&self.inner)
}
pub fn morgan_fp_counts_json(&self, radius: u32) -> String {
let counts = chematic_fp::morgan_fp_counts(&self.inner, radius);
let mut pairs: Vec<(u64, u32)> = counts.into_iter().collect();
pairs.sort_by_key(|(k, _)| *k);
let entries: Vec<String> = pairs.iter().map(|(k, v)| format!("\"{k}\": {v}")).collect();
format!("{{{}}}", entries.join(", "))
}
pub fn sum_estate(&self) -> f64 {
chematic_chem::sum_estate(&self.inner)
}
pub fn max_estate(&self) -> f64 {
chematic_chem::max_estate(&self.inner)
}
pub fn min_estate(&self) -> f64 {
chematic_chem::min_estate(&self.inner)
}
pub fn num_aliphatic_rings(&self) -> usize {
chematic_chem::num_aliphatic_rings(&self.inner)
}
pub fn num_saturated_rings(&self) -> usize {
chematic_chem::num_saturated_rings(&self.inner)
}
pub fn num_unspecified_stereocenters(&self) -> usize {
chematic_chem::num_unspecified_stereocenters(&self.inner)
}
pub fn to_inchi(&self) -> String {
chematic_inchi::inchi(&self.inner)
}
pub fn to_inchikey(&self) -> String {
let inchi_str = chematic_inchi::inchi(&self.inner);
chematic_inchi::inchi_key(&inchi_str)
}
pub fn logd_at_ph(&self, ph: f64) -> f64 {
chematic_chem::logd_simple(&self.inner, ph)
}
pub fn pka_acid_value(&self) -> f64 {
chematic_chem::pka_acid(&self.inner).unwrap_or(f64::NAN)
}
pub fn pka_base_value(&self) -> f64 {
chematic_chem::pka_base(&self.inner).unwrap_or(f64::NAN)
}
pub fn bbb_score(&self) -> f64 {
chematic_chem::bbb_score(&self.inner)
}
pub fn bbb_passes(&self) -> bool {
chematic_chem::bbb_passes(&self.inner)
}
pub fn caco2_permeability(&self) -> f64 {
chematic_chem::caco2_permeability(&self.inner)
}
pub fn herg_risk_score(&self) -> f64 {
chematic_chem::herg_risk_score(&self.inner)
}
pub fn cyp3a4_inhibition_risk(&self) -> f64 {
chematic_chem::cyp3a4_inhibition_risk(&self.inner)
}
pub fn logd_profile_json(&self, ph_start: f64, ph_end: f64, steps: usize) -> String {
let profile = chematic_chem::logd_profile(&self.inner, ph_start, ph_end, steps);
let items: Vec<String> = profile
.iter()
.map(|(ph, ld)| format!(r#"{{"ph":{ph:.2},"logd":{ld:.4}}}"#))
.collect();
format!("[{}]", items.join(","))
}
pub fn isotope_distribution_json(&self, resolution: f64) -> String {
let dist = chematic_chem::isotope_distribution(&self.inner, resolution);
let items: Vec<String> = dist
.iter()
.map(|(mass, abund)| format!(r#"{{"mass":{mass:.4},"abundance":{abund:.6}}}"#))
.collect();
format!("[{}]", items.join(","))
}
pub fn randic_index(&self) -> f64 {
chematic_chem::randic_index(&self.inner)
}
pub fn zagreb_index_m1(&self) -> u32 {
chematic_chem::zagreb_index_m1(&self.inner)
}
pub fn iupac_name(&self) -> String {
chematic_iupac::name(&self.inner).unwrap_or_default()
}
pub fn assign_cip_json(&self) -> String {
use chematic_chem::assign_cip;
use chematic_core::CipCode;
let assignment = assign_cip(&self.inner);
let centers: Vec<String> = assignment
.assignments
.iter()
.map(|(idx, code)| {
let code_str = match code {
CipCode::R => "R",
CipCode::S => "S",
CipCode::E => "E",
CipCode::Z => "Z",
};
format!(r#"{{"atom":{},"code":"{}"}}"#, idx.0, code_str)
})
.collect();
format!(r#"{{"centers":[{}]}}"#, centers.join(","))
}
}
#[wasm_bindgen]
pub fn is_valid_smiles(s: &str) -> bool {
chematic_smiles::parse(s).is_ok()
}
#[wasm_bindgen]
pub struct DepictOptions {
inner: chematic_depict::RenderOptions,
highlight_atoms: Vec<u32>,
highlight_bonds: Vec<u32>,
atom_color_entries: Vec<(u32, String)>,
}
impl Default for DepictOptions {
fn default() -> Self {
Self::new()
}
}
#[wasm_bindgen]
impl DepictOptions {
#[wasm_bindgen(constructor)]
pub fn new() -> Self {
Self {
inner: chematic_depict::RenderOptions::default(),
highlight_atoms: vec![],
highlight_bonds: vec![],
atom_color_entries: vec![],
}
}
pub fn set_width(&mut self, w: u32) {
self.inner.width = Some(w);
}
pub fn set_height(&mut self, h: u32) {
self.inner.height = Some(h);
}
pub fn set_padding(&mut self, p: f64) {
self.inner.padding = p;
}
pub fn set_background(&mut self, bg: String) {
self.inner.background = bg;
}
pub fn set_dark(&mut self, dark: bool) {
self.inner.dark = dark;
}
pub fn set_highlight_atoms(&mut self, atoms: Vec<u32>) {
self.highlight_atoms = atoms;
}
pub fn set_highlight_bonds(&mut self, bonds: Vec<u32>) {
self.highlight_bonds = bonds;
}
pub fn set_highlight_color(&mut self, color: String) {
self.inner.highlight_color = color;
}
pub fn set_atom_color(&mut self, idx: u32, color: String) {
self.atom_color_entries.push((idx, color));
}
pub fn set_atom_ids(&mut self, v: bool) {
self.inner.atom_ids = v;
}
pub fn set_show_atom_indices(&mut self, v: bool) {
self.inner.show_atom_indices = v;
}
pub fn set_kekulize(&mut self, v: bool) {
self.inner.kekulize = v;
}
pub(crate) fn to_render_options(&self) -> chematic_depict::RenderOptions {
let mut ro = self.inner.clone();
ro.highlight_atoms = self
.highlight_atoms
.iter()
.map(|&i| chematic_core::AtomIdx(i))
.collect();
ro.highlight_bonds = self
.highlight_bonds
.iter()
.map(|&i| chematic_core::BondIdx(i))
.collect();
ro.atom_color_map = self
.atom_color_entries
.iter()
.map(|(i, c)| (chematic_core::AtomIdx(*i), c.clone()))
.collect(); ro
}
}
fn bond_in_ring(
mol: &chematic_core::Molecule,
a: chematic_core::AtomIdx,
b: chematic_core::AtomIdx,
) -> bool {
let rings = chematic_perception::find_sssr(mol);
for ring in rings.rings() {
let n = ring.len();
for i in 0..n {
if (ring[i] == a && ring[(i + 1) % n] == b) || (ring[i] == b && ring[(i + 1) % n] == a)
{
return true;
}
}
}
false
}
fn molecular_formula(mol: &chematic_core::Molecule) -> String {
use chematic_core::{Element, implicit_hcount};
use std::collections::BTreeMap;
let mut counts: BTreeMap<u8, u32> = BTreeMap::new();
for (idx, atom) in mol.atoms() {
let an = atom.element.atomic_number();
if an != 1 {
*counts.entry(an).or_insert(0) += 1;
let h = implicit_hcount(mol, idx) as u32;
if h > 0 {
*counts.entry(1).or_insert(0) += h;
}
} else {
*counts.entry(1).or_insert(0) += 1;
}
}
let mut result = String::new();
let append = |symbol: &str, count: u32, out: &mut String| {
out.push_str(symbol);
if count > 1 {
out.push_str(&count.to_string());
}
};
if let Some(&c_count) = counts.get(&6) {
append("C", c_count, &mut result);
}
if let Some(&h_count) = counts.get(&1) {
append("H", h_count, &mut result);
}
for (&an, &count) in &counts {
if an == 1 || an == 6 {
continue;
}
let elem = Element::from_atomic_number(an).unwrap();
append(elem.symbol(), count, &mut result);
}
result
}
fn escape_json_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for ch in s.chars() {
match ch {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", c as u32)),
c => out.push(c),
}
}
out
}
fn json_option_string_array(values: &[Option<String>]) -> String {
let items = values
.iter()
.map(|value| match value {
Some(s) => format!(r#""{}""#, escape_json_string(s)),
None => "null".to_string(),
})
.collect::<Vec<_>>()
.join(",");
format!("[{items}]")
}
fn json_option_u8_array(values: &[Option<u8>]) -> String {
let items = values
.iter()
.map(|value| value.map_or_else(|| "null".to_string(), |v| v.to_string()))
.collect::<Vec<_>>()
.join(",");
format!("[{items}]")
}
fn enforce_wasm_input_len(label: &str, input: &str) -> Result<(), JsValue> {
if input.len() > WASM_MAX_INPUT_BYTES {
return Err(JsValue::from_str(&format!(
"{label} exceeds maximum input size ({} > {} bytes)",
input.len(),
WASM_MAX_INPUT_BYTES
)));
}
Ok(())
}
fn enforce_wasm_molecule_size(mol: &chematic_core::Molecule) -> Result<(), JsValue> {
if mol.atom_count() > WASM_MAX_ATOMS {
return Err(JsValue::from_str(&format!(
"Molecule exceeds maximum atom count ({} > {})",
mol.atom_count(),
WASM_MAX_ATOMS
)));
}
Ok(())
}
fn parse_wasm_string_json_array(json: &str, label: &str) -> Result<Vec<String>, JsValue> {
enforce_wasm_input_len(label, json)?;
let values: Vec<String> = serde_json::from_str(json)
.map_err(|e| JsValue::from_str(&format!("{label} must be a JSON array of strings: {e}")))?;
if values.len() > WASM_MAX_BATCH_ITEMS {
return Err(JsValue::from_str(&format!(
"{label} exceeds maximum item count ({} > {})",
values.len(),
WASM_MAX_BATCH_ITEMS
)));
}
if let Some((idx, value)) = values
.iter()
.enumerate()
.find(|(_, value)| value.len() > WASM_MAX_JSON_STRING_BYTES)
{
return Err(JsValue::from_str(&format!(
"{label}[{idx}] exceeds maximum string size ({} > {} bytes)",
value.len(),
WASM_MAX_JSON_STRING_BYTES
)));
}
Ok(values)
}
fn parse_smiles_json_array(json: &str) -> Result<Vec<String>, JsValue> {
parse_wasm_string_json_array(json, "smiles_json")
}
fn rgroup_fragment_smiles(
mol: &chematic_core::Molecule,
start: chematic_core::AtomIdx,
core_atoms: &std::collections::HashSet<chematic_core::AtomIdx>,
) -> String {
use chematic_core::{Atom, AtomIdx, MoleculeBuilder};
use std::collections::{HashMap, HashSet, VecDeque};
let mut fragment: Vec<AtomIdx> = Vec::new();
let mut visited: HashSet<AtomIdx> = HashSet::new();
let mut queue: VecDeque<AtomIdx> = VecDeque::new();
queue.push_back(start);
while let Some(idx) = queue.pop_front() {
if visited.contains(&idx) || core_atoms.contains(&idx) {
continue;
}
visited.insert(idx);
fragment.push(idx);
for (neighbor, _) in mol.neighbors(idx) {
if !visited.contains(&neighbor) {
queue.push_back(neighbor);
}
}
}
if fragment.is_empty() {
return String::new();
}
let fragment_set: HashSet<AtomIdx> = fragment.iter().copied().collect();
let mut builder = MoleculeBuilder::new();
let mut idx_map: HashMap<AtomIdx, AtomIdx> = HashMap::new();
for &orig in &fragment {
let atom = mol.atom(orig);
let mut a = Atom::new(atom.element);
a.charge = atom.charge;
a.isotope = atom.isotope;
a.aromatic = atom.aromatic;
a.chirality = atom.chirality;
a.hydrogen_count = atom.hydrogen_count;
a.atom_map = atom.atom_map;
let new_idx = builder.add_atom(a);
idx_map.insert(orig, new_idx);
}
for (_, bond) in mol.bonds() {
if fragment_set.contains(&bond.atom1) && fragment_set.contains(&bond.atom2) {
if let (Some(&n1), Some(&n2)) = (idx_map.get(&bond.atom1), idx_map.get(&bond.atom2)) {
let _ = builder.add_bond(n1, n2, bond.order);
}
}
}
chematic_smiles::canonical_smiles(&builder.build())
}