smiles-rs 0.2.1

A SMILES parser producing molecular graphs, with no_std support
Documentation
use alloc::vec::Vec;

use geometric_traits::traits::SparseValuedMatrixRef;

use super::{Smiles, maybe_collapse_atom_to_organic_subset};
use crate::{
    atom::{Atom, bracketed::chirality::Chirality},
    bond::Bond,
    smiles::{BondMatrixBuilder, StereoNeighbor, render_plan::preserve_raw_directional_single},
};

pub(super) mod chirality;
mod directional_bonds;
mod preparation;

use self::{
    chirality::{atom_with_chirality, canonical_stereo_neighbors_row, normalized_stereo_chirality},
    directional_bonds::{AtomBasedDoubleBondNormalization, atom_based_override_bond},
};

impl<AtomPolicy: crate::smiles::SmilesAtomPolicy> Smiles<AtomPolicy> {
    pub(crate) fn semantic_tetrahedral_chirality(&self, node_id: usize) -> Option<Chirality> {
        let chirality = self.node_by_id(node_id)?.chirality()?;
        let parsed_neighbors = self.parsed_stereo_neighbors_row(node_id);
        let preparation = self.stereo_normalization_preparation();
        let normalized_neighbors = canonical_stereo_neighbors_row(
            self,
            node_id,
            Some(chirality),
            parsed_neighbors,
            &preparation.new_index_of_old_node,
            &preparation.rooted_classes,
            &preparation.refined_classes,
        );

        match normalized_stereo_chirality(
            self,
            node_id,
            Some(chirality),
            parsed_neighbors,
            &normalized_neighbors,
            &preparation.rooted_classes,
            &preparation.refined_classes,
        ) {
            Some(chirality @ (Chirality::TH(_) | Chirality::AL(_))) => Some(chirality),
            _ => None,
        }
    }

    pub(super) fn stereo_normal_form(&self) -> Self {
        if self.nodes().is_empty() {
            return self.clone();
        }
        if !self.has_stereo_markup_for_normalization() {
            return self.clone();
        }

        let radical_electrons = self.radical_electron_counts();

        let preparation = self.stereo_normalization_preparation();
        let new_index_of_old_node = &preparation.new_index_of_old_node;
        let refined_classes = &preparation.refined_classes;
        let rooted_classes = &preparation.rooted_classes;
        let directional_overrides = self.projected_directional_bond_overrides_with_classes(
            new_index_of_old_node,
            rooted_classes,
            refined_classes,
        );
        let atom_based_double_bond_normalization = self.atom_based_double_bond_normalization(
            new_index_of_old_node,
            rooted_classes,
            refined_classes,
        );
        let normalized_atom_rows = self.stereo_normalized_atom_rows(
            new_index_of_old_node,
            rooted_classes,
            refined_classes,
            &atom_based_double_bond_normalization,
            &radical_electrons,
        );

        let atom_nodes = normalized_atom_rows.iter().map(|(atom, _)| *atom).collect::<Vec<_>>();
        let parsed_stereo_neighbors = normalized_atom_rows
            .into_iter()
            .map(|(_atom, neighbors)| neighbors)
            .collect::<Vec<_>>();

        let mut builder = BondMatrixBuilder::with_capacity(self.number_of_bonds());
        for ((row, column), entry) in self.bond_matrix().sparse_entries() {
            if row >= column {
                continue;
            }

            let mut bond = entry.bond();
            if matches!(bond, Bond::Up | Bond::Down)
                && (!preserve_raw_directional_single(self, &directional_overrides, row, column)
                    || atom_based_double_bond_normalization.semantic_endpoints[row]
                    || atom_based_double_bond_normalization.semantic_endpoints[column])
            {
                bond = Bond::Single;
            }
            if let Some(override_bond) = directional_overrides.get(row, column) {
                bond = override_bond;
            }
            if let Some(override_bond) = atom_based_override_bond(
                &atom_based_double_bond_normalization.override_rows,
                row,
                column,
            ) {
                bond = override_bond;
            }

            builder
                .push_edge_with_descriptor(row, column, entry.descriptor().with_bond(bond), None)
                .unwrap_or_else(|_| unreachable!("stereo normalization preserves a simple graph"));
        }

        Self::from_bond_matrix_parts_with_parsed_stereo_and_radicals(
            atom_nodes,
            builder.finish(self.nodes().len()),
            parsed_stereo_neighbors,
            Some(radical_electrons.to_vec()),
        )
    }

    fn stereo_normalized_atom_rows(
        &self,
        new_index_of_old_node: &[usize],
        rooted_classes: &[usize],
        refined_classes: &[usize],
        atom_based_double_bond_normalization: &AtomBasedDoubleBondNormalization,
        radical_electrons: &[u8],
    ) -> Vec<(Atom, Vec<StereoNeighbor>)> {
        self.atom_nodes
            .iter()
            .copied()
            .enumerate()
            .map(|(node_id, atom)| {
                let chirality = atom.chirality();
                let parsed_neighbors = self.parsed_stereo_neighbors_row(node_id);
                if atom_based_double_bond_normalization.clear_chirality[node_id] {
                    let cleared = atom_with_chirality(atom, None);
                    return (
                        maybe_collapse_atom_to_organic_subset(
                            self,
                            node_id,
                            cleared,
                            radical_electrons,
                        ),
                        Vec::new(),
                    );
                }
                if chirality.is_none() && parsed_neighbors.is_empty() {
                    return (atom, Vec::new());
                }
                let normalized_neighbors = canonical_stereo_neighbors_row(
                    self,
                    node_id,
                    chirality,
                    parsed_neighbors,
                    new_index_of_old_node,
                    rooted_classes,
                    refined_classes,
                );
                let normalized_chirality = normalized_stereo_chirality(
                    self,
                    node_id,
                    chirality,
                    parsed_neighbors,
                    &normalized_neighbors,
                    rooted_classes,
                    refined_classes,
                );
                let normalized_atom = atom_with_chirality(atom, normalized_chirality);
                (
                    if normalized_chirality.is_none() {
                        maybe_collapse_atom_to_organic_subset(
                            self,
                            node_id,
                            normalized_atom,
                            radical_electrons,
                        )
                    } else {
                        normalized_atom
                    },
                    normalized_chirality.map_or_else(Vec::new, |_| normalized_neighbors),
                )
            })
            .collect::<Vec<_>>()
    }
}