Initial commit
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
//! Chemical dataset management
|
||||
|
||||
pub struct ChemicalDatabase;
|
||||
pub struct MolecularLoader;
|
||||
pub struct PropertyDataset;
|
||||
@@ -0,0 +1,29 @@
|
||||
//! Drug discovery and molecular optimization
|
||||
|
||||
use crate::chemistry::Molecule;
|
||||
|
||||
pub struct DrugDiscovery {
|
||||
pub target_protein: String,
|
||||
}
|
||||
|
||||
pub struct MolecularOptimization {
|
||||
pub objective: OptimizationObjective,
|
||||
}
|
||||
|
||||
pub struct LeadOptimization {
|
||||
pub lead_compound: Molecule,
|
||||
}
|
||||
|
||||
pub enum OptimizationObjective {
|
||||
Potency,
|
||||
Selectivity,
|
||||
ADMET,
|
||||
MultiObjective(Vec<String>),
|
||||
}
|
||||
|
||||
impl DrugDiscovery {
|
||||
#[must_use]
|
||||
pub fn new(target_protein: String) -> Self {
|
||||
Self { target_protein }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Molecular feature extraction
|
||||
|
||||
pub struct FeatureExtractor;
|
||||
pub struct MolecularDescriptors;
|
||||
pub struct Fingerprints;
|
||||
@@ -0,0 +1,124 @@
|
||||
//! Graph Neural Networks for Molecular Property Prediction
|
||||
|
||||
use crate::chemistry::Molecule;
|
||||
use crate::error::Result;
|
||||
use rtx_tensor::{Device, Tensor};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Molecular Graph Neural Network
|
||||
#[derive(Debug)]
|
||||
pub struct MolecularGNN {
|
||||
pub device: Device,
|
||||
pub node_features: usize,
|
||||
pub edge_features: usize,
|
||||
pub hidden_dim: usize,
|
||||
pub output_dim: usize,
|
||||
}
|
||||
|
||||
/// Message passing layer
|
||||
#[derive(Debug)]
|
||||
pub struct MessagePassing {
|
||||
pub aggregation: AggregationType,
|
||||
}
|
||||
|
||||
/// Graph convolution layer
|
||||
#[derive(Debug)]
|
||||
pub struct GraphConvolution {
|
||||
pub conv_type: ConvolutionType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum AggregationType {
|
||||
Mean,
|
||||
Sum,
|
||||
Max,
|
||||
Attention,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ConvolutionType {
|
||||
GCN,
|
||||
GraphSage,
|
||||
GAT,
|
||||
MPNN,
|
||||
}
|
||||
|
||||
impl MolecularGNN {
|
||||
#[must_use]
|
||||
pub fn builder() -> MolecularGNNBuilder {
|
||||
MolecularGNNBuilder::new()
|
||||
}
|
||||
|
||||
pub async fn forward(&self, molecule: &Molecule) -> Result<Tensor> {
|
||||
// Placeholder implementation
|
||||
let _features = molecule.to_feature_matrix()?;
|
||||
Ok(Tensor::from_slice(&[0.5], &[1], &self.device)?)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct MolecularGNNBuilder {
|
||||
device: Option<Device>,
|
||||
node_features: usize,
|
||||
edge_features: usize,
|
||||
hidden_dim: usize,
|
||||
output_dim: usize,
|
||||
}
|
||||
|
||||
impl Default for MolecularGNNBuilder {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl MolecularGNNBuilder {
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
device: None,
|
||||
node_features: 74,
|
||||
edge_features: 12,
|
||||
hidden_dim: 128,
|
||||
output_dim: 1,
|
||||
}
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn device(mut self, device: &Device) -> Self {
|
||||
self.device = Some(device.clone());
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn node_features(mut self, features: usize) -> Self {
|
||||
self.node_features = features;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn edge_features(mut self, features: usize) -> Self {
|
||||
self.edge_features = features;
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn message_passing_layers(self, _layers: usize) -> Self {
|
||||
// Configuration for message passing layers
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn readout_layers(self, _layers: Vec<usize>) -> Self {
|
||||
// Configuration for readout layers
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> Result<MolecularGNN> {
|
||||
Ok(MolecularGNN {
|
||||
device: self.device.unwrap_or(Device::Cuda(0)),
|
||||
node_features: self.node_features,
|
||||
edge_features: self.edge_features,
|
||||
hidden_dim: self.hidden_dim,
|
||||
output_dim: self.output_dim,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//! Chemistry Applications Module
|
||||
//!
|
||||
//! This module provides comprehensive chemistry applications including molecular
|
||||
//! property prediction, drug discovery, molecular optimization, and reaction prediction.
|
||||
|
||||
pub mod datasets;
|
||||
pub mod drug_discovery;
|
||||
pub mod features;
|
||||
pub mod gnn;
|
||||
pub mod molecular;
|
||||
pub mod properties;
|
||||
pub mod reactions;
|
||||
pub mod transformer;
|
||||
|
||||
// Re-export main types
|
||||
pub use datasets::{ChemicalDatabase, MolecularLoader, PropertyDataset};
|
||||
pub use drug_discovery::{DrugDiscovery, LeadOptimization, MolecularOptimization};
|
||||
pub use features::{FeatureExtractor, Fingerprints, MolecularDescriptors};
|
||||
pub use gnn::{GraphConvolution, MessagePassing, MolecularGNN};
|
||||
pub use molecular::{AtomicFeatures, BondFeatures, MolecularDataset, Molecule};
|
||||
pub use properties::{ADMET, Lipinski, PropertyPredictor, Solubility, Toxicity};
|
||||
pub use reactions::{CatalysisOptimizer, ReactionPredictor, RetrosynthesisPlanner};
|
||||
pub use transformer::{MolecularTransformer, PositionalEncoding, SelfAttention};
|
||||
@@ -0,0 +1,969 @@
|
||||
//! Molecular data structures and basic operations
|
||||
//!
|
||||
//! This module provides core molecular representations including atoms, bonds,
|
||||
//! and complete molecular structures with their associated features.
|
||||
|
||||
use crate::error::{Result, ScienceError};
|
||||
use nalgebra::{DMatrix, Vector3};
|
||||
use petgraph::Undirected;
|
||||
use petgraph::graph::{EdgeIndex, Graph, NodeIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Atomic number to element symbol mapping
|
||||
const PERIODIC_TABLE: &[&str] = &[
|
||||
"H", "He", "Li", "Be", "B", "C", "N", "O", "F", "Ne", "Na", "Mg", "Al", "Si", "P", "S", "Cl",
|
||||
"Ar", "K", "Ca",
|
||||
// ... truncated for brevity
|
||||
];
|
||||
|
||||
/// Molecular graph representation
|
||||
pub type MolecularGraph = Graph<Atom, Bond, Undirected>;
|
||||
|
||||
/// Complete molecular structure
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Molecule {
|
||||
/// Unique molecular identifier
|
||||
pub id: String,
|
||||
/// SMILES representation
|
||||
pub smiles: Option<String>,
|
||||
/// `InChI` representation
|
||||
pub inchi: Option<String>,
|
||||
/// Molecular graph
|
||||
pub graph: MolecularGraph,
|
||||
/// 3D coordinates (if available)
|
||||
pub coordinates: Option<Vec<Vector3<f64>>>,
|
||||
/// Computed properties
|
||||
pub properties: HashMap<String, f64>,
|
||||
/// Experimental data
|
||||
pub experimental_data: HashMap<String, f64>,
|
||||
/// Additional metadata
|
||||
pub metadata: HashMap<String, String>,
|
||||
}
|
||||
|
||||
/// Individual atom in a molecule
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Atom {
|
||||
/// Atomic number (1=H, 6=C, etc.)
|
||||
pub atomic_number: u8,
|
||||
/// Element symbol
|
||||
pub symbol: String,
|
||||
/// Formal charge
|
||||
pub formal_charge: i8,
|
||||
/// Number of implicit hydrogens
|
||||
pub implicit_hydrogens: u8,
|
||||
/// Hybridization state
|
||||
pub hybridization: HybridizationType,
|
||||
/// Aromaticity flag
|
||||
pub is_aromatic: bool,
|
||||
/// Ring membership
|
||||
pub ring_info: RingInfo,
|
||||
/// Partial atomic charge (if computed)
|
||||
pub partial_charge: Option<f64>,
|
||||
/// Van der Waals radius
|
||||
pub vdw_radius: f64,
|
||||
/// Atomic features for ML
|
||||
pub features: AtomicFeatures,
|
||||
}
|
||||
|
||||
/// Chemical bond between atoms
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Bond {
|
||||
/// Bond order (1=single, 2=double, 3=triple, 4=aromatic)
|
||||
pub order: f64,
|
||||
/// Bond type
|
||||
pub bond_type: BondType,
|
||||
/// Bond length (in Angstroms, if available)
|
||||
pub length: Option<f64>,
|
||||
/// Conjugation flag
|
||||
pub is_conjugated: bool,
|
||||
/// Ring membership
|
||||
pub in_ring: bool,
|
||||
/// Stereochemistry information
|
||||
pub stereo: BondStereo,
|
||||
/// Bond features for ML
|
||||
pub features: BondFeatures,
|
||||
}
|
||||
|
||||
/// Hybridization types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum HybridizationType {
|
||||
/// sp3 hybridization
|
||||
SP3,
|
||||
/// sp2 hybridization
|
||||
SP2,
|
||||
/// sp hybridization
|
||||
SP,
|
||||
/// sp3d hybridization
|
||||
SP3D,
|
||||
/// sp3d2 hybridization
|
||||
SP3D2,
|
||||
/// Unknown/unassigned
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Bond types
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum BondType {
|
||||
/// Single bond
|
||||
Single,
|
||||
/// Double bond
|
||||
Double,
|
||||
/// Triple bond
|
||||
Triple,
|
||||
/// Aromatic bond
|
||||
Aromatic,
|
||||
/// Coordinate covalent bond
|
||||
Coordinate,
|
||||
/// Hydrogen bond
|
||||
Hydrogen,
|
||||
/// Unknown bond type
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Bond stereochemistry
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub enum BondStereo {
|
||||
/// No stereochemistry
|
||||
None,
|
||||
/// E (trans) configuration
|
||||
E,
|
||||
/// Z (cis) configuration
|
||||
Z,
|
||||
/// Unknown configuration
|
||||
Unknown,
|
||||
}
|
||||
|
||||
/// Ring information for atoms
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RingInfo {
|
||||
/// Number of rings the atom belongs to
|
||||
pub ring_count: usize,
|
||||
/// Sizes of rings the atom belongs to
|
||||
pub ring_sizes: Vec<usize>,
|
||||
/// Is the atom in an aromatic ring
|
||||
pub is_aromatic_ring: bool,
|
||||
/// Smallest ring size
|
||||
pub smallest_ring: Option<usize>,
|
||||
}
|
||||
|
||||
/// Atomic features for machine learning models
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct AtomicFeatures {
|
||||
/// One-hot encoded atomic number
|
||||
pub atomic_number_onehot: Vec<f64>,
|
||||
/// Degree (number of connections)
|
||||
pub degree: f64,
|
||||
/// Formal charge
|
||||
pub formal_charge: f64,
|
||||
/// Number of radical electrons
|
||||
pub radical_electrons: f64,
|
||||
/// Hybridization one-hot
|
||||
pub hybridization_onehot: Vec<f64>,
|
||||
/// Aromaticity flag
|
||||
pub is_aromatic: f64,
|
||||
/// Ring membership features
|
||||
pub ring_features: Vec<f64>,
|
||||
/// Chirality features
|
||||
pub chirality_features: Vec<f64>,
|
||||
/// Additional computed features
|
||||
pub additional_features: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Bond features for machine learning models
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BondFeatures {
|
||||
/// Bond type one-hot encoding
|
||||
pub bond_type_onehot: Vec<f64>,
|
||||
/// Bond order
|
||||
pub bond_order: f64,
|
||||
/// Conjugation flag
|
||||
pub is_conjugated: f64,
|
||||
/// Ring membership flag
|
||||
pub is_in_ring: f64,
|
||||
/// Stereo configuration one-hot
|
||||
pub stereo_onehot: Vec<f64>,
|
||||
/// Bond length (normalized, if available)
|
||||
pub bond_length: f64,
|
||||
/// Additional computed features
|
||||
pub additional_features: Vec<f64>,
|
||||
}
|
||||
|
||||
/// Molecular dataset container
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MolecularDataset {
|
||||
/// Dataset name/identifier
|
||||
pub name: String,
|
||||
/// Collection of molecules
|
||||
pub molecules: Vec<Molecule>,
|
||||
/// Target properties for supervised learning
|
||||
pub targets: HashMap<String, Vec<f64>>,
|
||||
/// Dataset splits (train/validation/test indices)
|
||||
pub splits: HashMap<String, Vec<usize>>,
|
||||
/// Dataset statistics
|
||||
pub statistics: DatasetStatistics,
|
||||
/// Preprocessing configuration
|
||||
pub preprocessing_config: PreprocessingConfig,
|
||||
}
|
||||
|
||||
/// Dataset statistics
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct DatasetStatistics {
|
||||
/// Number of molecules
|
||||
pub num_molecules: usize,
|
||||
/// Average molecular weight
|
||||
pub avg_molecular_weight: f64,
|
||||
/// Molecular weight distribution percentiles
|
||||
pub mw_percentiles: Vec<f64>,
|
||||
/// Average number of atoms
|
||||
pub avg_num_atoms: f64,
|
||||
/// Average number of bonds
|
||||
pub avg_num_bonds: f64,
|
||||
/// Element count distribution
|
||||
pub element_counts: HashMap<String, usize>,
|
||||
/// Target property statistics
|
||||
pub target_stats: HashMap<String, PropertyStatistics>,
|
||||
}
|
||||
|
||||
/// Property statistics
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PropertyStatistics {
|
||||
/// Mean value
|
||||
pub mean: f64,
|
||||
/// Standard deviation
|
||||
pub std_dev: f64,
|
||||
/// Minimum value
|
||||
pub min: f64,
|
||||
/// Maximum value
|
||||
pub max: f64,
|
||||
/// Percentiles [10, 25, 50, 75, 90]
|
||||
pub percentiles: Vec<f64>,
|
||||
/// Number of valid (non-NaN) values
|
||||
pub valid_count: usize,
|
||||
}
|
||||
|
||||
/// Preprocessing configuration
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct PreprocessingConfig {
|
||||
/// Normalize molecular features
|
||||
pub normalize_features: bool,
|
||||
/// Add implicit hydrogens
|
||||
pub add_hydrogens: bool,
|
||||
/// Canonicalize SMILES
|
||||
pub canonicalize_smiles: bool,
|
||||
/// Remove salts and solvents
|
||||
pub remove_salts: bool,
|
||||
/// Filter by molecular weight range
|
||||
pub mw_filter: Option<(f64, f64)>,
|
||||
/// Maximum number of atoms
|
||||
pub max_atoms: Option<usize>,
|
||||
/// Feature scaling method
|
||||
pub scaling_method: ScalingMethod,
|
||||
}
|
||||
|
||||
/// Feature scaling methods
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ScalingMethod {
|
||||
/// No scaling
|
||||
None,
|
||||
/// Standard normalization (z-score)
|
||||
StandardNormalization,
|
||||
/// Min-max scaling to [0, 1]
|
||||
MinMaxScaling,
|
||||
/// Robust scaling using median and IQR
|
||||
RobustScaling,
|
||||
}
|
||||
|
||||
impl Molecule {
|
||||
/// Create a new empty molecule
|
||||
#[must_use]
|
||||
pub fn new(id: String) -> Self {
|
||||
Self {
|
||||
id,
|
||||
smiles: None,
|
||||
inchi: None,
|
||||
graph: Graph::new_undirected(),
|
||||
coordinates: None,
|
||||
properties: HashMap::new(),
|
||||
experimental_data: HashMap::new(),
|
||||
metadata: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create molecule from SMILES string
|
||||
pub fn from_smiles(id: String, smiles: &str) -> Result<Self> {
|
||||
// In a full implementation, this would parse SMILES
|
||||
// For now, create a minimal structure
|
||||
let mut mol = Self::new(id);
|
||||
mol.smiles = Some(smiles.to_string());
|
||||
|
||||
// Add some dummy atoms for demonstration
|
||||
if smiles.contains('C') {
|
||||
mol.add_carbon_atom()?;
|
||||
}
|
||||
if smiles.contains('O') {
|
||||
mol.add_oxygen_atom()?;
|
||||
}
|
||||
|
||||
Ok(mol)
|
||||
}
|
||||
|
||||
/// Add a carbon atom to the molecule
|
||||
pub fn add_carbon_atom(&mut self) -> Result<NodeIndex> {
|
||||
let carbon = Atom::new_carbon();
|
||||
let node_idx = self.graph.add_node(carbon);
|
||||
Ok(node_idx)
|
||||
}
|
||||
|
||||
/// Add an oxygen atom to the molecule
|
||||
pub fn add_oxygen_atom(&mut self) -> Result<NodeIndex> {
|
||||
let oxygen = Atom::new_oxygen();
|
||||
let node_idx = self.graph.add_node(oxygen);
|
||||
Ok(node_idx)
|
||||
}
|
||||
|
||||
/// Add a bond between two atoms
|
||||
pub fn add_bond(
|
||||
&mut self,
|
||||
atom1: NodeIndex,
|
||||
atom2: NodeIndex,
|
||||
bond_type: BondType,
|
||||
) -> Result<EdgeIndex> {
|
||||
let bond = Bond::new(bond_type);
|
||||
let edge_idx = self.graph.add_edge(atom1, atom2, bond);
|
||||
Ok(edge_idx)
|
||||
}
|
||||
|
||||
/// Get molecular formula
|
||||
#[must_use]
|
||||
pub fn molecular_formula(&self) -> String {
|
||||
let mut element_counts: HashMap<String, usize> = HashMap::new();
|
||||
|
||||
for node_idx in self.graph.node_indices() {
|
||||
if let Some(atom) = self.graph.node_weight(node_idx) {
|
||||
*element_counts.entry(atom.symbol.clone()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Format as molecular formula (C first, then H, then alphabetical)
|
||||
let mut formula = String::new();
|
||||
|
||||
// Carbon first
|
||||
if let Some(&count) = element_counts.get("C") {
|
||||
formula.push('C');
|
||||
if count > 1 {
|
||||
formula.push_str(&count.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Hydrogen second
|
||||
if let Some(&count) = element_counts.get("H") {
|
||||
formula.push('H');
|
||||
if count > 1 {
|
||||
formula.push_str(&count.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Other elements alphabetically
|
||||
let mut other_elements: Vec<_> = element_counts
|
||||
.iter()
|
||||
.filter(|(symbol, _)| *symbol != "C" && *symbol != "H")
|
||||
.collect();
|
||||
other_elements.sort_by_key(|(symbol, _)| *symbol);
|
||||
|
||||
for (symbol, &count) in other_elements {
|
||||
formula.push_str(symbol);
|
||||
if count > 1 {
|
||||
formula.push_str(&count.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
if formula.is_empty() {
|
||||
formula = "Unknown".to_string();
|
||||
}
|
||||
|
||||
formula
|
||||
}
|
||||
|
||||
/// Calculate molecular weight
|
||||
#[must_use]
|
||||
pub fn molecular_weight(&self) -> f64 {
|
||||
const ATOMIC_WEIGHTS: &[f64] = &[
|
||||
1.008, 4.003, 6.94, 9.012, 10.81, 12.01, 14.007, 15.999, 18.998, 20.18, 22.99, 24.305,
|
||||
26.982, 28.085, 30.974, 32.06,
|
||||
// ... truncated for brevity
|
||||
];
|
||||
|
||||
self.graph
|
||||
.node_weights()
|
||||
.map(|atom| {
|
||||
let idx = (atom.atomic_number as usize).saturating_sub(1);
|
||||
ATOMIC_WEIGHTS.get(idx).copied().unwrap_or(0.0)
|
||||
})
|
||||
.sum()
|
||||
}
|
||||
|
||||
/// Count atoms of specific element
|
||||
#[must_use]
|
||||
pub fn count_atoms(&self, element: &str) -> usize {
|
||||
self.graph
|
||||
.node_weights()
|
||||
.filter(|atom| atom.symbol == element)
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Get number of rings using SSSR (Smallest Set of Smallest Rings)
|
||||
#[must_use]
|
||||
pub fn num_rings(&self) -> usize {
|
||||
// Simplified ring detection - in practice would use proper algorithm
|
||||
let num_edges = self.graph.edge_count();
|
||||
let num_nodes = self.graph.node_count();
|
||||
|
||||
// For connected graph: rings = edges - nodes + 1
|
||||
if num_nodes > 0 && num_edges >= num_nodes {
|
||||
num_edges - num_nodes + 1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if molecule satisfies Lipinski's Rule of Five
|
||||
#[must_use]
|
||||
pub fn lipinski_compliance(&self) -> LipinskiResult {
|
||||
let mw = self.molecular_weight();
|
||||
let logp = self.properties.get("LogP").copied().unwrap_or(0.0);
|
||||
let hbd = self.count_atoms("H"); // Simplified H-bond donors
|
||||
let hba = self.count_atoms("O") + self.count_atoms("N"); // Simplified H-bond acceptors
|
||||
|
||||
LipinskiResult {
|
||||
molecular_weight: mw,
|
||||
mw_compliant: mw <= 500.0,
|
||||
logp,
|
||||
logp_compliant: logp <= 5.0,
|
||||
hbd: hbd as f64,
|
||||
hbd_compliant: hbd <= 5,
|
||||
hba: hba as f64,
|
||||
hba_compliant: hba <= 10,
|
||||
overall_compliant: mw <= 500.0 && logp <= 5.0 && hbd <= 5 && hba <= 10,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert molecule to feature matrix for ML
|
||||
pub fn to_feature_matrix(&self) -> Result<DMatrix<f64>> {
|
||||
let num_atoms = self.graph.node_count();
|
||||
if num_atoms == 0 {
|
||||
return Err(ScienceError::chemistry(
|
||||
"Cannot create feature matrix for empty molecule",
|
||||
Some(self.id.clone()),
|
||||
));
|
||||
}
|
||||
|
||||
// Each atom contributes atomic features
|
||||
let feature_dim = 74; // Standard atomic feature dimension
|
||||
let mut features = DMatrix::zeros(num_atoms, feature_dim);
|
||||
|
||||
for (i, node_idx) in self.graph.node_indices().enumerate() {
|
||||
if let Some(atom) = self.graph.node_weight(node_idx) {
|
||||
let atom_features = atom.to_feature_vector();
|
||||
for (j, &feature) in atom_features.iter().enumerate() {
|
||||
if j < feature_dim {
|
||||
features[(i, j)] = feature;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(features)
|
||||
}
|
||||
|
||||
/// Get adjacency matrix
|
||||
#[must_use]
|
||||
pub fn adjacency_matrix(&self) -> DMatrix<f64> {
|
||||
let num_atoms = self.graph.node_count();
|
||||
let mut adj = DMatrix::zeros(num_atoms, num_atoms);
|
||||
|
||||
let node_indices: Vec<_> = self.graph.node_indices().collect();
|
||||
|
||||
for edge_idx in self.graph.edge_indices() {
|
||||
if let Some((node1, node2)) = self.graph.edge_endpoints(edge_idx)
|
||||
&& let (Some(i), Some(j)) = (
|
||||
node_indices.iter().position(|&x| x == node1),
|
||||
node_indices.iter().position(|&x| x == node2),
|
||||
)
|
||||
{
|
||||
let bond = self.graph.edge_weight(edge_idx).unwrap();
|
||||
adj[(i, j)] = bond.order;
|
||||
adj[(j, i)] = bond.order; // Symmetric for undirected graph
|
||||
}
|
||||
}
|
||||
|
||||
adj
|
||||
}
|
||||
}
|
||||
|
||||
/// Lipinski Rule of Five compliance result
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct LipinskiResult {
|
||||
/// Molecular weight
|
||||
pub molecular_weight: f64,
|
||||
/// MW ≤ 500 Da
|
||||
pub mw_compliant: bool,
|
||||
/// Octanol-water partition coefficient
|
||||
pub logp: f64,
|
||||
/// `LogP` ≤ 5
|
||||
pub logp_compliant: bool,
|
||||
/// Number of hydrogen bond donors
|
||||
pub hbd: f64,
|
||||
/// HBD ≤ 5
|
||||
pub hbd_compliant: bool,
|
||||
/// Number of hydrogen bond acceptors
|
||||
pub hba: f64,
|
||||
/// HBA ≤ 10
|
||||
pub hba_compliant: bool,
|
||||
/// Overall Lipinski compliance
|
||||
pub overall_compliant: bool,
|
||||
}
|
||||
|
||||
impl Atom {
|
||||
/// Create a new carbon atom
|
||||
#[must_use]
|
||||
pub fn new_carbon() -> Self {
|
||||
Self {
|
||||
atomic_number: 6,
|
||||
symbol: "C".to_string(),
|
||||
formal_charge: 0,
|
||||
implicit_hydrogens: 0,
|
||||
hybridization: HybridizationType::SP3,
|
||||
is_aromatic: false,
|
||||
ring_info: RingInfo::new(),
|
||||
partial_charge: None,
|
||||
vdw_radius: 1.70, // Van der Waals radius for carbon
|
||||
features: AtomicFeatures::new_carbon(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new oxygen atom
|
||||
#[must_use]
|
||||
pub fn new_oxygen() -> Self {
|
||||
Self {
|
||||
atomic_number: 8,
|
||||
symbol: "O".to_string(),
|
||||
formal_charge: 0,
|
||||
implicit_hydrogens: 0,
|
||||
hybridization: HybridizationType::SP3,
|
||||
is_aromatic: false,
|
||||
ring_info: RingInfo::new(),
|
||||
partial_charge: None,
|
||||
vdw_radius: 1.52, // Van der Waals radius for oxygen
|
||||
features: AtomicFeatures::new_oxygen(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert atom to feature vector
|
||||
#[must_use]
|
||||
pub fn to_feature_vector(&self) -> Vec<f64> {
|
||||
let mut features = Vec::new();
|
||||
|
||||
// Atomic number one-hot (first 118 elements)
|
||||
features.extend(self.features.atomic_number_onehot.clone());
|
||||
|
||||
// Other atomic features
|
||||
features.push(self.features.degree);
|
||||
features.push(self.features.formal_charge);
|
||||
features.push(self.features.radical_electrons);
|
||||
features.extend(self.features.hybridization_onehot.clone());
|
||||
features.push(self.features.is_aromatic);
|
||||
features.extend(self.features.ring_features.clone());
|
||||
features.extend(self.features.chirality_features.clone());
|
||||
|
||||
features
|
||||
}
|
||||
}
|
||||
|
||||
impl Bond {
|
||||
/// Create a new bond
|
||||
#[must_use]
|
||||
pub fn new(bond_type: BondType) -> Self {
|
||||
let order = match &bond_type {
|
||||
BondType::Single => 1.0,
|
||||
BondType::Double => 2.0,
|
||||
BondType::Triple => 3.0,
|
||||
BondType::Aromatic => 1.5,
|
||||
_ => 1.0,
|
||||
};
|
||||
|
||||
Self {
|
||||
order,
|
||||
bond_type: bond_type.clone(),
|
||||
length: None,
|
||||
is_conjugated: false,
|
||||
in_ring: false,
|
||||
stereo: BondStereo::None,
|
||||
features: BondFeatures::new(&bond_type),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RingInfo {
|
||||
/// Create new empty ring info
|
||||
#[must_use]
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
ring_count: 0,
|
||||
ring_sizes: Vec::new(),
|
||||
is_aromatic_ring: false,
|
||||
smallest_ring: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AtomicFeatures {
|
||||
/// Create features for carbon atom
|
||||
#[must_use]
|
||||
pub fn new_carbon() -> Self {
|
||||
let mut atomic_onehot = vec![0.0; 118];
|
||||
atomic_onehot[5] = 1.0; // Carbon is element 6 (0-indexed)
|
||||
|
||||
let mut hybr_onehot = vec![0.0; 6];
|
||||
hybr_onehot[0] = 1.0; // SP3
|
||||
|
||||
Self {
|
||||
atomic_number_onehot: atomic_onehot,
|
||||
degree: 0.0,
|
||||
formal_charge: 0.0,
|
||||
radical_electrons: 0.0,
|
||||
hybridization_onehot: hybr_onehot,
|
||||
is_aromatic: 0.0,
|
||||
ring_features: vec![0.0; 6], // Ring size features
|
||||
chirality_features: vec![0.0; 4], // Chirality features
|
||||
additional_features: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create features for oxygen atom
|
||||
#[must_use]
|
||||
pub fn new_oxygen() -> Self {
|
||||
let mut atomic_onehot = vec![0.0; 118];
|
||||
atomic_onehot[7] = 1.0; // Oxygen is element 8 (0-indexed)
|
||||
|
||||
let mut hybr_onehot = vec![0.0; 6];
|
||||
hybr_onehot[0] = 1.0; // SP3
|
||||
|
||||
Self {
|
||||
atomic_number_onehot: atomic_onehot,
|
||||
degree: 0.0,
|
||||
formal_charge: 0.0,
|
||||
radical_electrons: 0.0,
|
||||
hybridization_onehot: hybr_onehot,
|
||||
is_aromatic: 0.0,
|
||||
ring_features: vec![0.0; 6],
|
||||
chirality_features: vec![0.0; 4],
|
||||
additional_features: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BondFeatures {
|
||||
/// Create bond features
|
||||
#[must_use]
|
||||
pub fn new(bond_type: &BondType) -> Self {
|
||||
let mut type_onehot = vec![0.0; 6];
|
||||
let type_idx = match bond_type {
|
||||
BondType::Single => 0,
|
||||
BondType::Double => 1,
|
||||
BondType::Triple => 2,
|
||||
BondType::Aromatic => 3,
|
||||
BondType::Coordinate => 4,
|
||||
_ => 5,
|
||||
};
|
||||
type_onehot[type_idx] = 1.0;
|
||||
|
||||
let order = match bond_type {
|
||||
BondType::Single => 1.0,
|
||||
BondType::Double => 2.0,
|
||||
BondType::Triple => 3.0,
|
||||
BondType::Aromatic => 1.5,
|
||||
_ => 1.0,
|
||||
};
|
||||
|
||||
Self {
|
||||
bond_type_onehot: type_onehot,
|
||||
bond_order: order,
|
||||
is_conjugated: 0.0,
|
||||
is_in_ring: 0.0,
|
||||
stereo_onehot: vec![0.0; 4], // Stereo configuration
|
||||
bond_length: 0.0,
|
||||
additional_features: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MolecularDataset {
|
||||
/// Create a new empty dataset
|
||||
#[must_use]
|
||||
pub fn new(name: String) -> Self {
|
||||
Self {
|
||||
name,
|
||||
molecules: Vec::new(),
|
||||
targets: HashMap::new(),
|
||||
splits: HashMap::new(),
|
||||
statistics: DatasetStatistics::default(),
|
||||
preprocessing_config: PreprocessingConfig::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load dataset from file
|
||||
pub fn load(filename: &str) -> Result<Self> {
|
||||
// In practice, would load from CSV, SDF, or other formats
|
||||
let mut dataset = Self::new(filename.to_string());
|
||||
|
||||
// Add some dummy molecules for demonstration
|
||||
let mol1 = Molecule::from_smiles("mol_1".to_string(), "CCO")?;
|
||||
let mol2 = Molecule::from_smiles("mol_2".to_string(), "CC(=O)O")?;
|
||||
|
||||
dataset.molecules.push(mol1);
|
||||
dataset.molecules.push(mol2);
|
||||
|
||||
// Add dummy targets
|
||||
dataset
|
||||
.targets
|
||||
.insert("solubility".to_string(), vec![-2.5, -1.8]);
|
||||
dataset
|
||||
.targets
|
||||
.insert("toxicity".to_string(), vec![0.1, 0.3]);
|
||||
|
||||
dataset.compute_statistics()?;
|
||||
|
||||
Ok(dataset)
|
||||
}
|
||||
|
||||
/// Compute dataset statistics
|
||||
pub fn compute_statistics(&mut self) -> Result<()> {
|
||||
if self.molecules.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let num_molecules = self.molecules.len();
|
||||
let total_mw: f64 = self.molecules.iter().map(Molecule::molecular_weight).sum();
|
||||
let avg_mw = total_mw / num_molecules as f64;
|
||||
|
||||
let total_atoms: usize = self.molecules.iter().map(|m| m.graph.node_count()).sum();
|
||||
let avg_atoms = total_atoms as f64 / num_molecules as f64;
|
||||
|
||||
let total_bonds: usize = self.molecules.iter().map(|m| m.graph.edge_count()).sum();
|
||||
let avg_bonds = total_bonds as f64 / num_molecules as f64;
|
||||
|
||||
// Compute molecular weight percentiles
|
||||
let mut mws: Vec<f64> = self
|
||||
.molecules
|
||||
.iter()
|
||||
.map(Molecule::molecular_weight)
|
||||
.collect();
|
||||
mws.sort_by(f64::total_cmp);
|
||||
|
||||
let mw_percentiles = vec![
|
||||
percentile(&mws, 0.1),
|
||||
percentile(&mws, 0.25),
|
||||
percentile(&mws, 0.5),
|
||||
percentile(&mws, 0.75),
|
||||
percentile(&mws, 0.9),
|
||||
];
|
||||
|
||||
// Element counts
|
||||
let mut element_counts = HashMap::new();
|
||||
for molecule in &self.molecules {
|
||||
for node_idx in molecule.graph.node_indices() {
|
||||
if let Some(atom) = molecule.graph.node_weight(node_idx) {
|
||||
*element_counts.entry(atom.symbol.clone()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Target property statistics
|
||||
let mut target_stats = HashMap::new();
|
||||
for (prop_name, values) in &self.targets {
|
||||
target_stats.insert(prop_name.clone(), PropertyStatistics::compute(values));
|
||||
}
|
||||
|
||||
self.statistics = DatasetStatistics {
|
||||
num_molecules,
|
||||
avg_molecular_weight: avg_mw,
|
||||
mw_percentiles,
|
||||
avg_num_atoms: avg_atoms,
|
||||
avg_num_bonds: avg_bonds,
|
||||
element_counts,
|
||||
target_stats,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Create train/validation/test splits
|
||||
pub fn create_splits(
|
||||
&mut self,
|
||||
train_ratio: f64,
|
||||
val_ratio: f64,
|
||||
test_ratio: f64,
|
||||
) -> Result<()> {
|
||||
if (train_ratio + val_ratio + test_ratio - 1.0).abs() > 1e-6 {
|
||||
return Err(ScienceError::data_validation(
|
||||
"Split ratios must sum to 1.0",
|
||||
"split_ratios",
|
||||
"sum = 1.0",
|
||||
format!("sum = {}", train_ratio + val_ratio + test_ratio),
|
||||
));
|
||||
}
|
||||
|
||||
let n = self.molecules.len();
|
||||
let n_train = (n as f64 * train_ratio) as usize;
|
||||
let n_val = (n as f64 * val_ratio) as usize;
|
||||
|
||||
let indices: Vec<usize> = (0..n).collect();
|
||||
// In practice, would shuffle randomly
|
||||
|
||||
let train_indices = indices[..n_train].to_vec();
|
||||
let val_indices = indices[n_train..n_train + n_val].to_vec();
|
||||
let test_indices = indices[n_train + n_val..].to_vec();
|
||||
|
||||
self.splits.insert("train".to_string(), train_indices);
|
||||
self.splits.insert("validation".to_string(), val_indices);
|
||||
self.splits.insert("test".to_string(), test_indices);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl PropertyStatistics {
|
||||
/// Compute statistics for a property
|
||||
#[must_use]
|
||||
pub fn compute(values: &[f64]) -> Self {
|
||||
let valid_values: Vec<f64> = values.iter().filter(|&&x| !x.is_nan()).copied().collect();
|
||||
|
||||
if valid_values.is_empty() {
|
||||
return Self {
|
||||
mean: 0.0,
|
||||
std_dev: 0.0,
|
||||
min: 0.0,
|
||||
max: 0.0,
|
||||
percentiles: vec![0.0; 5],
|
||||
valid_count: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let mean = valid_values.iter().sum::<f64>() / valid_values.len() as f64;
|
||||
let variance = valid_values
|
||||
.iter()
|
||||
.map(|&x| (x - mean).powi(2))
|
||||
.sum::<f64>()
|
||||
/ valid_values.len() as f64;
|
||||
let std_dev = variance.sqrt();
|
||||
|
||||
let mut sorted = valid_values.clone();
|
||||
sorted.sort_by(f64::total_cmp);
|
||||
|
||||
let percentiles = vec![
|
||||
percentile(&sorted, 0.1),
|
||||
percentile(&sorted, 0.25),
|
||||
percentile(&sorted, 0.5),
|
||||
percentile(&sorted, 0.75),
|
||||
percentile(&sorted, 0.9),
|
||||
];
|
||||
|
||||
Self {
|
||||
mean,
|
||||
std_dev,
|
||||
min: sorted[0],
|
||||
max: sorted[sorted.len() - 1],
|
||||
percentiles,
|
||||
valid_count: valid_values.len(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DatasetStatistics {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
num_molecules: 0,
|
||||
avg_molecular_weight: 0.0,
|
||||
mw_percentiles: vec![0.0; 5],
|
||||
avg_num_atoms: 0.0,
|
||||
avg_num_bonds: 0.0,
|
||||
element_counts: HashMap::new(),
|
||||
target_stats: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PreprocessingConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
normalize_features: true,
|
||||
add_hydrogens: false,
|
||||
canonicalize_smiles: true,
|
||||
remove_salts: true,
|
||||
mw_filter: Some((50.0, 1000.0)),
|
||||
max_atoms: Some(100),
|
||||
scaling_method: ScalingMethod::StandardNormalization,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Calculate percentile of sorted data
|
||||
fn percentile(sorted_data: &[f64], p: f64) -> f64 {
|
||||
if sorted_data.is_empty() {
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
let index = (p * (sorted_data.len() - 1) as f64).round() as usize;
|
||||
sorted_data[index.min(sorted_data.len() - 1)]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_molecule_creation() -> Result<()> {
|
||||
let mol = Molecule::from_smiles("test_mol".to_string(), "CCO")?;
|
||||
assert_eq!(mol.id, "test_mol");
|
||||
assert!(mol.smiles.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_molecular_weight() -> Result<()> {
|
||||
let mol = Molecule::from_smiles("ethanol".to_string(), "CCO")?;
|
||||
let mw = mol.molecular_weight();
|
||||
assert!(mw > 0.0);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_molecular_formula() -> Result<()> {
|
||||
let mut mol = Molecule::new("test".to_string());
|
||||
mol.add_carbon_atom()?;
|
||||
mol.add_carbon_atom()?;
|
||||
mol.add_oxygen_atom()?;
|
||||
|
||||
let formula = mol.molecular_formula();
|
||||
assert!(formula.contains('C'));
|
||||
assert!(formula.contains('O'));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_dataset_loading() -> Result<()> {
|
||||
let dataset = MolecularDataset::load("test_dataset.csv")?;
|
||||
assert!(!dataset.molecules.is_empty());
|
||||
assert!(!dataset.targets.is_empty());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lipinski_compliance() -> Result<()> {
|
||||
let mol = Molecule::from_smiles("aspirin".to_string(), "CC(=O)OC1=CC=CC=C1C(=O)O")?;
|
||||
let lipinski = mol.lipinski_compliance();
|
||||
assert!(lipinski.molecular_weight > 0.0);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
//! Molecular property prediction models
|
||||
|
||||
use crate::chemistry::Molecule;
|
||||
use crate::error::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Property prediction model
|
||||
pub struct PropertyPredictor {
|
||||
pub model_type: ModelType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub enum ModelType {
|
||||
GNN,
|
||||
Transformer,
|
||||
RandomForest,
|
||||
XGBoost,
|
||||
}
|
||||
|
||||
/// ADMET (Absorption, Distribution, Metabolism, Excretion, Toxicity) properties
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ADMET {
|
||||
pub absorption: f64,
|
||||
pub distribution: f64,
|
||||
pub metabolism: f64,
|
||||
pub excretion: f64,
|
||||
pub toxicity: f64,
|
||||
}
|
||||
|
||||
/// Lipinski's Rule of Five
|
||||
pub struct Lipinski;
|
||||
|
||||
/// Solubility prediction
|
||||
pub struct Solubility {
|
||||
pub log_s: f64,
|
||||
}
|
||||
|
||||
/// Toxicity prediction
|
||||
pub struct Toxicity {
|
||||
pub ld50: f64,
|
||||
pub mutagenicity: bool,
|
||||
}
|
||||
|
||||
impl PropertyPredictor {
|
||||
#[must_use]
|
||||
pub fn new(model_type: ModelType) -> Self {
|
||||
Self { model_type }
|
||||
}
|
||||
|
||||
pub async fn predict(&self, molecule: &Molecule) -> Result<f64> {
|
||||
// Placeholder implementation
|
||||
Ok(molecule.molecular_weight() / 1000.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Lipinski {
|
||||
pub fn evaluate(molecule: &Molecule) -> Result<bool> {
|
||||
let compliance = molecule.lipinski_compliance();
|
||||
Ok(compliance.overall_compliant)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Chemical reaction prediction and retrosynthesis
|
||||
|
||||
pub struct ReactionPredictor;
|
||||
pub struct RetrosynthesisPlanner;
|
||||
pub struct CatalysisOptimizer;
|
||||
@@ -0,0 +1,34 @@
|
||||
//! Transformer-based molecular models
|
||||
|
||||
use rtx_tensor::Device;
|
||||
|
||||
pub struct MolecularTransformer {
|
||||
pub device: Device,
|
||||
pub vocab_size: usize,
|
||||
pub hidden_dim: usize,
|
||||
pub num_heads: usize,
|
||||
pub num_layers: usize,
|
||||
}
|
||||
|
||||
pub struct SelfAttention {
|
||||
pub num_heads: usize,
|
||||
pub head_dim: usize,
|
||||
}
|
||||
|
||||
pub struct PositionalEncoding {
|
||||
pub max_len: usize,
|
||||
pub d_model: usize,
|
||||
}
|
||||
|
||||
impl MolecularTransformer {
|
||||
#[must_use]
|
||||
pub fn new(device: Device, vocab_size: usize) -> Self {
|
||||
Self {
|
||||
device,
|
||||
vocab_size,
|
||||
hidden_dim: 512,
|
||||
num_heads: 8,
|
||||
num_layers: 6,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user