Initial commit

This commit is contained in:
redclawsystems
2026-03-04 00:08:42 +00:00
commit 4d88dc0584
4449 changed files with 1556714 additions and 0 deletions
@@ -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,
}
}
}