quantrs2_tytan/applications/
drug_discovery.rs

1//! Drug discovery applications: Molecular design and optimization.
2//!
3//! This module provides quantum optimization tools for drug discovery
4//! including molecular design, lead optimization, and virtual screening.
5
6// Sampler types available for drug discovery applications
7#![allow(dead_code)]
8
9use scirs2_core::ndarray::Array2;
10use std::collections::{HashMap, HashSet};
11
12/// Molecular design optimizer
13pub struct MolecularDesignOptimizer {
14    /// Target properties
15    target_properties: TargetProperties,
16    /// Fragment library
17    fragment_library: FragmentLibrary,
18    /// Scoring function
19    scoring_function: ScoringFunction,
20    /// Design constraints
21    constraints: DesignConstraints,
22    /// Optimization strategy
23    strategy: OptimizationStrategy,
24}
25
26#[derive(Debug, Clone)]
27pub struct TargetProperties {
28    /// Target molecular weight
29    pub molecular_weight: Option<(f64, f64)>, // (min, max)
30    /// LogP range
31    pub logp: Option<(f64, f64)>,
32    /// LogS range
33    pub logs: Option<(f64, f64)>,
34    /// H-bond donors
35    pub hbd: Option<(usize, usize)>,
36    /// H-bond acceptors
37    pub hba: Option<(usize, usize)>,
38    /// Rotatable bonds
39    pub rotatable_bonds: Option<(usize, usize)>,
40    /// TPSA range
41    pub tpsa: Option<(f64, f64)>,
42    /// Custom descriptors
43    pub custom_descriptors: HashMap<String, (f64, f64)>,
44}
45
46#[derive(Debug, Clone)]
47pub struct FragmentLibrary {
48    /// Available fragments
49    pub fragments: Vec<MolecularFragment>,
50    /// Connection rules
51    pub connection_rules: ConnectionRules,
52    /// Fragment frequencies in known drugs
53    pub fragment_scores: HashMap<usize, f64>,
54    /// Privileged scaffolds
55    pub privileged_scaffolds: Vec<usize>,
56}
57
58#[derive(Debug, Clone)]
59pub struct MolecularFragment {
60    /// Fragment ID
61    pub id: usize,
62    /// SMILES representation
63    pub smiles: String,
64    /// Attachment points
65    pub attachment_points: Vec<AttachmentPoint>,
66    /// Fragment properties
67    pub properties: FragmentProperties,
68    /// Pharmacophore features
69    pub pharmacophores: Vec<PharmacophoreFeature>,
70}
71
72#[derive(Debug, Clone)]
73pub struct AttachmentPoint {
74    /// Atom index
75    pub atom_idx: usize,
76    /// Bond type allowed
77    pub bond_types: Vec<BondType>,
78    /// Directionality
79    pub direction: Vec3D,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum BondType {
84    Single,
85    Double,
86    Triple,
87    Aromatic,
88}
89
90#[derive(Debug, Clone)]
91pub struct Vec3D {
92    pub x: f64,
93    pub y: f64,
94    pub z: f64,
95}
96
97#[derive(Debug, Clone)]
98pub struct FragmentProperties {
99    /// Molecular weight contribution
100    pub mw_contribution: f64,
101    /// LogP contribution
102    pub logp_contribution: f64,
103    /// H-bond donors
104    pub hbd_count: usize,
105    /// H-bond acceptors
106    pub hba_count: usize,
107    /// Rotatable bonds
108    pub rotatable_count: usize,
109    /// TPSA contribution
110    pub tpsa_contribution: f64,
111}
112
113#[derive(Debug, Clone)]
114pub struct PharmacophoreFeature {
115    /// Feature type
116    pub feature_type: PharmacophoreType,
117    /// Position relative to fragment
118    pub position: Vec3D,
119    /// Tolerance radius
120    pub tolerance: f64,
121}
122
123#[derive(Debug, Clone)]
124pub enum PharmacophoreType {
125    HBondDonor,
126    HBondAcceptor,
127    Hydrophobic,
128    Aromatic,
129    PositiveCharge,
130    NegativeCharge,
131    MetalCoordination,
132}
133
134#[derive(Debug, Clone)]
135pub struct ConnectionRules {
136    /// Compatible fragment pairs
137    pub compatible_pairs: HashMap<(usize, usize), f64>,
138    /// Forbidden connections
139    pub forbidden_connections: HashSet<(usize, usize)>,
140    /// Reaction templates
141    pub reaction_templates: Vec<ReactionTemplate>,
142}
143
144#[derive(Debug, Clone)]
145pub struct ReactionTemplate {
146    /// Template name
147    pub name: String,
148    /// Required functional groups
149    pub reactants: Vec<FunctionalGroup>,
150    /// Product pattern
151    pub product_pattern: String,
152    /// Reaction feasibility score
153    pub feasibility: f64,
154}
155
156#[derive(Debug, Clone)]
157pub struct FunctionalGroup {
158    /// SMARTS pattern
159    pub smarts: String,
160    /// Required count
161    pub count: usize,
162}
163
164#[derive(Debug, Clone)]
165pub enum ScoringFunction {
166    /// Simple additive scoring
167    Additive { weights: HashMap<String, f64> },
168    /// Machine learning based
169    MLBased { model_path: String },
170    /// Docking score
171    DockingBased { receptor: ProteinStructure },
172    /// Multi-objective
173    MultiObjective { objectives: Vec<ObjectiveFunction> },
174    /// Pharmacophore matching
175    PharmacophoreMatching { reference: PharmacophoreModel },
176}
177
178#[derive(Debug, Clone)]
179pub struct ProteinStructure {
180    /// PDB ID or path
181    pub pdb_id: String,
182    /// Active site residues
183    pub active_site: Vec<usize>,
184    /// Grid box for docking
185    pub grid_box: GridBox,
186}
187
188#[derive(Debug, Clone)]
189pub struct GridBox {
190    pub center: Vec3D,
191    pub dimensions: Vec3D,
192    pub spacing: f64,
193}
194
195#[derive(Debug, Clone)]
196pub struct PharmacophoreModel {
197    /// Required features
198    pub features: Vec<PharmacophoreFeature>,
199    /// Distance constraints
200    pub distance_constraints: Vec<DistanceConstraint>,
201    /// Angle constraints
202    pub angle_constraints: Vec<AngleConstraint>,
203}
204
205#[derive(Debug, Clone)]
206pub struct DistanceConstraint {
207    pub feature1: usize,
208    pub feature2: usize,
209    pub min_distance: f64,
210    pub max_distance: f64,
211}
212
213#[derive(Debug, Clone)]
214pub struct AngleConstraint {
215    pub feature1: usize,
216    pub feature2: usize,
217    pub feature3: usize,
218    pub min_angle: f64,
219    pub max_angle: f64,
220}
221
222#[derive(Debug, Clone)]
223pub enum ObjectiveFunction {
224    /// Binding affinity
225    BindingAffinity { weight: f64 },
226    /// Synthetic accessibility
227    SyntheticAccessibility { weight: f64 },
228    /// ADMET properties
229    ADMET {
230        property: ADMETProperty,
231        weight: f64,
232    },
233    /// Novelty
234    Novelty {
235        reference_set: Vec<String>,
236        weight: f64,
237    },
238    /// Diversity
239    Diversity { weight: f64 },
240}
241
242#[derive(Debug, Clone)]
243pub enum ADMETProperty {
244    Absorption,
245    Distribution,
246    Metabolism,
247    Excretion,
248    Toxicity,
249    Solubility,
250    Permeability,
251    Stability,
252}
253
254#[derive(Debug, Clone)]
255pub struct DesignConstraints {
256    /// Maximum molecular weight
257    pub max_mw: f64,
258    /// Lipinski's rule of five
259    pub lipinski: bool,
260    /// Veber's rules
261    pub veber: bool,
262    /// PAINS filters
263    pub pains_filter: bool,
264    /// Synthetic accessibility threshold
265    pub max_sa_score: f64,
266    /// Minimum QED score
267    pub min_qed: f64,
268    /// Custom SMARTS filters
269    pub smarts_filters: Vec<String>,
270}
271
272#[derive(Debug, Clone)]
273pub enum OptimizationStrategy {
274    /// Fragment growing
275    FragmentGrowing { core: MolecularFragment },
276    /// Fragment linking
277    FragmentLinking { fragments: Vec<MolecularFragment> },
278    /// Fragment hopping
279    FragmentHopping { scaffold: MolecularFragment },
280    /// De novo design
281    DeNovo,
282    /// Lead optimization
283    LeadOptimization { lead: String },
284}
285
286impl MolecularDesignOptimizer {
287    /// Create new molecular design optimizer
288    pub fn new(target_properties: TargetProperties, fragment_library: FragmentLibrary) -> Self {
289        Self {
290            target_properties,
291            fragment_library,
292            scoring_function: ScoringFunction::Additive {
293                weights: Self::default_weights(),
294            },
295            constraints: DesignConstraints::default(),
296            strategy: OptimizationStrategy::DeNovo,
297        }
298    }
299
300    /// Default scoring weights
301    fn default_weights() -> HashMap<String, f64> {
302        let mut weights = HashMap::new();
303        weights.insert("mw_penalty".to_string(), -0.1);
304        weights.insert("logp_penalty".to_string(), -0.2);
305        weights.insert("hbd_penalty".to_string(), -0.1);
306        weights.insert("hba_penalty".to_string(), -0.1);
307        weights.insert("rotatable_penalty".to_string(), -0.05);
308        weights.insert("tpsa_penalty".to_string(), -0.1);
309        weights.insert("fragment_score".to_string(), 1.0);
310        weights
311    }
312
313    /// Set scoring function
314    pub fn with_scoring(mut self, scoring: ScoringFunction) -> Self {
315        self.scoring_function = scoring;
316        self
317    }
318
319    /// Set constraints
320    pub fn with_constraints(mut self, constraints: DesignConstraints) -> Self {
321        self.constraints = constraints;
322        self
323    }
324
325    /// Set optimization strategy
326    pub fn with_strategy(mut self, strategy: OptimizationStrategy) -> Self {
327        self.strategy = strategy;
328        self
329    }
330
331    /// Build QUBO for molecular design
332    pub fn build_qubo(&self) -> Result<(Array2<f64>, HashMap<String, usize>), String> {
333        match &self.strategy {
334            OptimizationStrategy::FragmentGrowing { core } => {
335                self.build_fragment_growing_qubo(core)
336            }
337            OptimizationStrategy::DeNovo => self.build_de_novo_qubo(),
338            _ => Err("Strategy not yet implemented".to_string()),
339        }
340    }
341
342    /// Build QUBO for fragment growing
343    fn build_fragment_growing_qubo(
344        &self,
345        core: &MolecularFragment,
346    ) -> Result<(Array2<f64>, HashMap<String, usize>), String> {
347        // Variables: x_{f,p} = 1 if fragment f is attached at position p
348        let positions = core.attachment_points.len();
349        let fragments = self.fragment_library.fragments.len();
350        let n_vars = positions * fragments;
351
352        let mut qubo = Array2::zeros((n_vars, n_vars));
353        let mut var_map = HashMap::new();
354
355        // Create variable mapping
356        for p in 0..positions {
357            for f in 0..fragments {
358                let var_name = format!("x_{f}_{p}");
359                var_map.insert(var_name, p * fragments + f);
360            }
361        }
362
363        // Add scoring terms
364        self.add_fragment_scores(&mut qubo, &var_map, core)?;
365
366        // Add property constraints
367        self.add_property_constraints(&mut qubo, &var_map, core)?;
368
369        // Add connection compatibility
370        self.add_connection_compatibility(&mut qubo, &var_map, core)?;
371
372        // At most one fragment per position
373        self.add_uniqueness_constraints(&mut qubo, &var_map, positions, fragments)?;
374
375        Ok((qubo, var_map))
376    }
377
378    /// Build QUBO for de novo design
379    fn build_de_novo_qubo(&self) -> Result<(Array2<f64>, HashMap<String, usize>), String> {
380        // Variables: x_{f,i} = 1 if fragment f is at position i in molecule
381        let max_positions = 10; // Maximum molecule size
382        let fragments = self.fragment_library.fragments.len();
383
384        // Additional variables for connections
385        // y_{i,j} = 1 if position i connects to position j
386
387        let position_vars = max_positions * fragments;
388        let connection_vars = max_positions * (max_positions - 1) / 2;
389        let n_vars = position_vars + connection_vars;
390
391        let mut qubo = Array2::zeros((n_vars, n_vars));
392        let mut var_map = HashMap::new();
393
394        // Position variables
395        for i in 0..max_positions {
396            for f in 0..fragments {
397                let var_name = format!("x_{f}_{i}");
398                var_map.insert(var_name, i * fragments + f);
399            }
400        }
401
402        // Connection variables
403        let mut var_idx = position_vars;
404        for i in 0..max_positions {
405            for j in i + 1..max_positions {
406                let var_name = format!("y_{i}_{j}");
407                var_map.insert(var_name, var_idx);
408                var_idx += 1;
409            }
410        }
411
412        // Add de novo specific terms
413        self.add_de_novo_objective(&mut qubo, &var_map, max_positions)?;
414
415        // Connectivity constraints
416        self.add_connectivity_constraints(&mut qubo, &var_map, max_positions)?;
417
418        // Property constraints
419        self.add_global_property_constraints(&mut qubo, &var_map, max_positions)?;
420
421        Ok((qubo, var_map))
422    }
423
424    /// Add fragment scoring terms
425    fn add_fragment_scores(
426        &self,
427        qubo: &mut Array2<f64>,
428        var_map: &HashMap<String, usize>,
429        core: &MolecularFragment,
430    ) -> Result<(), String> {
431        let positions = core.attachment_points.len();
432
433        for p in 0..positions {
434            for f in 0..self.fragment_library.fragments.len() {
435                let var_name = format!("x_{f}_{p}");
436                if let Some(&var_idx) = var_map.get(&var_name) {
437                    // Fragment score
438                    let score = self
439                        .fragment_library
440                        .fragment_scores
441                        .get(&f)
442                        .unwrap_or(&0.0);
443
444                    // Compatibility with attachment point
445                    let compatibility = self.compute_attachment_compatibility(
446                        &core.attachment_points[p],
447                        &self.fragment_library.fragments[f],
448                    );
449
450                    qubo[[var_idx, var_idx]] -= score * compatibility;
451                }
452            }
453        }
454
455        Ok(())
456    }
457
458    /// Compute attachment compatibility
459    fn compute_attachment_compatibility(
460        &self,
461        attachment: &AttachmentPoint,
462        fragment: &MolecularFragment,
463    ) -> f64 {
464        // Check if fragment has compatible attachment points
465        let compatible = fragment.attachment_points.iter().any(|frag_attach| {
466            attachment
467                .bond_types
468                .iter()
469                .any(|bt| frag_attach.bond_types.contains(bt))
470        });
471
472        if compatible {
473            1.0
474        } else {
475            0.0
476        }
477    }
478
479    /// Add property constraints
480    fn add_property_constraints(
481        &self,
482        qubo: &mut Array2<f64>,
483        var_map: &HashMap<String, usize>,
484        core: &MolecularFragment,
485    ) -> Result<(), String> {
486        let penalty = 100.0;
487
488        // Molecular weight constraint
489        if let Some((min_mw, max_mw)) = self.target_properties.molecular_weight {
490            let core_mw = core.properties.mw_contribution;
491
492            for f in 0..self.fragment_library.fragments.len() {
493                let frag_mw = self.fragment_library.fragments[f]
494                    .properties
495                    .mw_contribution;
496                let total_mw = core_mw + frag_mw;
497
498                if total_mw < min_mw || total_mw > max_mw {
499                    // Penalize out-of-range combinations
500                    for p in 0..core.attachment_points.len() {
501                        let var_name = format!("x_{f}_{p}");
502                        if let Some(&var_idx) = var_map.get(&var_name) {
503                            qubo[[var_idx, var_idx]] += penalty;
504                        }
505                    }
506                }
507            }
508        }
509
510        // Similar constraints for other properties
511        self.add_logp_constraints(qubo, var_map, core, penalty)?;
512        self.add_hbond_constraints(qubo, var_map, core, penalty)?;
513
514        Ok(())
515    }
516
517    /// Add LogP constraints
518    fn add_logp_constraints(
519        &self,
520        qubo: &mut Array2<f64>,
521        var_map: &HashMap<String, usize>,
522        core: &MolecularFragment,
523        penalty: f64,
524    ) -> Result<(), String> {
525        if let Some((min_logp, max_logp)) = self.target_properties.logp {
526            let core_logp = core.properties.logp_contribution;
527
528            for f in 0..self.fragment_library.fragments.len() {
529                let frag_logp = self.fragment_library.fragments[f]
530                    .properties
531                    .logp_contribution;
532                let total_logp = core_logp + frag_logp;
533
534                if total_logp < min_logp || total_logp > max_logp {
535                    for p in 0..core.attachment_points.len() {
536                        let var_name = format!("x_{f}_{p}");
537                        if let Some(&var_idx) = var_map.get(&var_name) {
538                            qubo[[var_idx, var_idx]] += penalty * 0.5;
539                        }
540                    }
541                }
542            }
543        }
544
545        Ok(())
546    }
547
548    /// Add H-bond constraints
549    fn add_hbond_constraints(
550        &self,
551        qubo: &mut Array2<f64>,
552        var_map: &HashMap<String, usize>,
553        core: &MolecularFragment,
554        penalty: f64,
555    ) -> Result<(), String> {
556        // H-bond donor constraints
557        if let Some((min_hbd, max_hbd)) = self.target_properties.hbd {
558            let core_hbd = core.properties.hbd_count;
559
560            for f in 0..self.fragment_library.fragments.len() {
561                let frag_hbd = self.fragment_library.fragments[f].properties.hbd_count;
562                let total_hbd = core_hbd + frag_hbd;
563
564                if total_hbd < min_hbd || total_hbd > max_hbd {
565                    for p in 0..core.attachment_points.len() {
566                        let var_name = format!("x_{f}_{p}");
567                        if let Some(&var_idx) = var_map.get(&var_name) {
568                            qubo[[var_idx, var_idx]] += penalty * 0.3;
569                        }
570                    }
571                }
572            }
573        }
574
575        Ok(())
576    }
577
578    /// Add connection compatibility
579    fn add_connection_compatibility(
580        &self,
581        qubo: &mut Array2<f64>,
582        var_map: &HashMap<String, usize>,
583        core: &MolecularFragment,
584    ) -> Result<(), String> {
585        let positions = core.attachment_points.len();
586
587        // Penalize incompatible fragment pairs at different positions
588        for p1 in 0..positions {
589            for p2 in p1 + 1..positions {
590                for f1 in 0..self.fragment_library.fragments.len() {
591                    for f2 in 0..self.fragment_library.fragments.len() {
592                        let var1 = format!("x_{f1}_{p1}");
593                        let var2 = format!("x_{f2}_{p2}");
594
595                        if let (Some(&idx1), Some(&idx2)) = (var_map.get(&var1), var_map.get(&var2))
596                        {
597                            // Check compatibility
598                            if self
599                                .fragment_library
600                                .connection_rules
601                                .forbidden_connections
602                                .contains(&(f1, f2))
603                            {
604                                qubo[[idx1, idx2]] += 1000.0;
605                            } else if let Some(&score) = self
606                                .fragment_library
607                                .connection_rules
608                                .compatible_pairs
609                                .get(&(f1, f2))
610                            {
611                                qubo[[idx1, idx2]] -= score;
612                            }
613                        }
614                    }
615                }
616            }
617        }
618
619        Ok(())
620    }
621
622    /// Add uniqueness constraints
623    fn add_uniqueness_constraints(
624        &self,
625        qubo: &mut Array2<f64>,
626        var_map: &HashMap<String, usize>,
627        positions: usize,
628        fragments: usize,
629    ) -> Result<(), String> {
630        let penalty = 100.0;
631
632        // At most one fragment per position
633        for p in 0..positions {
634            // (sum_f x_{f,p} - 1)^2 if we want exactly one
635            // or just penalize multiple selections
636            for f1 in 0..fragments {
637                for f2 in f1 + 1..fragments {
638                    let var1 = format!("x_{f1}_{p}");
639                    let var2 = format!("x_{f2}_{p}");
640
641                    if let (Some(&idx1), Some(&idx2)) = (var_map.get(&var1), var_map.get(&var2)) {
642                        qubo[[idx1, idx2]] += penalty;
643                    }
644                }
645            }
646        }
647
648        Ok(())
649    }
650
651    /// Add de novo objective
652    fn add_de_novo_objective(
653        &self,
654        qubo: &mut Array2<f64>,
655        var_map: &HashMap<String, usize>,
656        max_positions: usize,
657    ) -> Result<(), String> {
658        // Favor molecules with good fragment scores
659        for i in 0..max_positions {
660            for f in 0..self.fragment_library.fragments.len() {
661                let var_name = format!("x_{f}_{i}");
662                if let Some(&var_idx) = var_map.get(&var_name) {
663                    let score = self
664                        .fragment_library
665                        .fragment_scores
666                        .get(&f)
667                        .unwrap_or(&0.0);
668                    qubo[[var_idx, var_idx]] -= score;
669
670                    // Privileged scaffolds get bonus
671                    if self.fragment_library.privileged_scaffolds.contains(&f) {
672                        qubo[[var_idx, var_idx]] -= 2.0;
673                    }
674                }
675            }
676        }
677
678        // Favor connected molecules
679        for i in 0..max_positions {
680            for j in i + 1..max_positions {
681                let conn_var = format!("y_{i}_{j}");
682                if let Some(&conn_idx) = var_map.get(&conn_var) {
683                    // Small penalty for connections (want some but not too many)
684                    qubo[[conn_idx, conn_idx]] += 0.1;
685                }
686            }
687        }
688
689        Ok(())
690    }
691
692    /// Add connectivity constraints
693    fn add_connectivity_constraints(
694        &self,
695        qubo: &mut Array2<f64>,
696        var_map: &HashMap<String, usize>,
697        max_positions: usize,
698    ) -> Result<(), String> {
699        let penalty = 100.0;
700
701        // If position i has a fragment, it should connect to at least one other
702        for i in 0..max_positions {
703            // Connection indicator for position i
704            for f in 0..self.fragment_library.fragments.len() {
705                let frag_var = format!("x_{f}_{i}");
706                if let Some(&frag_idx) = var_map.get(&frag_var) {
707                    // Must have at least one connection if fragment present
708                    let mut _has_connection = false;
709                    for j in 0..max_positions {
710                        if i != j {
711                            let conn_var = if i < j {
712                                format!("y_{i}_{j}")
713                            } else {
714                                format!("y_{j}_{i}")
715                            };
716
717                            if let Some(&conn_idx) = var_map.get(&conn_var) {
718                                // Fragment at i but no connections is penalized
719                                qubo[[frag_idx, frag_idx]] += penalty;
720                                qubo[[frag_idx, conn_idx]] -= penalty;
721                                _has_connection = true;
722                            }
723                        }
724                    }
725                }
726            }
727        }
728
729        // Connection compatibility
730        self.add_connection_compatibility_de_novo(qubo, var_map, max_positions)?;
731
732        Ok(())
733    }
734
735    /// Add connection compatibility for de novo
736    fn add_connection_compatibility_de_novo(
737        &self,
738        qubo: &mut Array2<f64>,
739        var_map: &HashMap<String, usize>,
740        max_positions: usize,
741    ) -> Result<(), String> {
742        // If positions i and j are connected, fragments must be compatible
743        for i in 0..max_positions {
744            for j in i + 1..max_positions {
745                let conn_var = format!("y_{i}_{j}");
746                if let Some(&conn_idx) = var_map.get(&conn_var) {
747                    for f1 in 0..self.fragment_library.fragments.len() {
748                        for f2 in 0..self.fragment_library.fragments.len() {
749                            let var1 = format!("x_{f1}_{i}");
750                            let var2 = format!("x_{f2}_{j}");
751
752                            if let (Some(&idx1), Some(&idx2)) =
753                                (var_map.get(&var1), var_map.get(&var2))
754                            {
755                                if self
756                                    .fragment_library
757                                    .connection_rules
758                                    .forbidden_connections
759                                    .contains(&(f1, f2))
760                                {
761                                    // Penalize: connection + incompatible fragments
762                                    // This is a 3-way interaction, approximate with 2-way
763                                    qubo[[conn_idx, idx1]] += 50.0;
764                                    qubo[[conn_idx, idx2]] += 50.0;
765                                }
766                            }
767                        }
768                    }
769                }
770            }
771        }
772
773        Ok(())
774    }
775
776    /// Add global property constraints
777    fn add_global_property_constraints(
778        &self,
779        qubo: &mut Array2<f64>,
780        var_map: &HashMap<String, usize>,
781        max_positions: usize,
782    ) -> Result<(), String> {
783        // This is challenging as properties are additive over all selected fragments
784        // Use penalty approximation
785
786        let penalty = 10.0;
787
788        // Approximate molecular weight constraint
789        if let Some((_min_mw, max_mw)) = self.target_properties.molecular_weight {
790            for i in 0..max_positions {
791                for f in 0..self.fragment_library.fragments.len() {
792                    let var_name = format!("x_{f}_{i}");
793                    if let Some(&var_idx) = var_map.get(&var_name) {
794                        let mw = self.fragment_library.fragments[f]
795                            .properties
796                            .mw_contribution;
797
798                        // Penalize if single fragment already exceeds limits
799                        if mw > max_mw {
800                            qubo[[var_idx, var_idx]] += penalty * 10.0;
801                        }
802
803                        // Soft penalty based on contribution
804                        let mw_penalty = if mw > max_mw / max_positions as f64 {
805                            (mw - max_mw / max_positions as f64) * penalty
806                        } else {
807                            0.0
808                        };
809
810                        qubo[[var_idx, var_idx]] += mw_penalty;
811                    }
812                }
813            }
814        }
815
816        Ok(())
817    }
818
819    /// Decode solution to molecule
820    pub fn decode_solution(
821        &self,
822        solution: &HashMap<String, bool>,
823    ) -> Result<DesignedMolecule, String> {
824        match &self.strategy {
825            OptimizationStrategy::FragmentGrowing { core } => {
826                self.decode_fragment_growing(solution, core)
827            }
828            OptimizationStrategy::DeNovo => self.decode_de_novo(solution),
829            _ => Err("Decoding not implemented for this strategy".to_string()),
830        }
831    }
832
833    /// Decode fragment growing solution
834    fn decode_fragment_growing(
835        &self,
836        solution: &HashMap<String, bool>,
837        core: &MolecularFragment,
838    ) -> Result<DesignedMolecule, String> {
839        let mut fragments = vec![core.clone()];
840        let mut connections = Vec::new();
841
842        // Find attached fragments
843        for (var_name, &value) in solution {
844            if value && var_name.starts_with("x_") {
845                let parts: Vec<&str> = var_name[2..].split('_').collect();
846                if parts.len() == 2 {
847                    let frag_idx: usize = parts[0].parse().unwrap_or(0);
848                    let pos_idx: usize = parts[1].parse().unwrap_or(0);
849
850                    if frag_idx < self.fragment_library.fragments.len() {
851                        fragments.push(self.fragment_library.fragments[frag_idx].clone());
852                        connections.push(Connection {
853                            from_fragment: 0,
854                            from_attachment: pos_idx,
855                            to_fragment: fragments.len() - 1,
856                            to_attachment: 0,
857                            bond_type: BondType::Single,
858                        });
859                    }
860                }
861            }
862        }
863
864        let properties = self.calculate_properties(&fragments);
865        let score = self.calculate_score(&fragments, &connections);
866
867        Ok(DesignedMolecule {
868            fragments,
869            connections,
870            properties,
871            score,
872            smiles: None, // Would need to construct SMILES
873        })
874    }
875
876    /// Decode de novo solution
877    fn decode_de_novo(&self, solution: &HashMap<String, bool>) -> Result<DesignedMolecule, String> {
878        let mut fragment_positions: HashMap<usize, usize> = HashMap::new();
879        let mut connections = Vec::new();
880
881        // Find fragment positions
882        for (var_name, &value) in solution {
883            if value && var_name.starts_with("x_") {
884                let parts: Vec<&str> = var_name[2..].split('_').collect();
885                if parts.len() == 2 {
886                    let frag_idx: usize = parts[0].parse().unwrap_or(0);
887                    let pos_idx: usize = parts[1].parse().unwrap_or(0);
888                    fragment_positions.insert(pos_idx, frag_idx);
889                }
890            }
891        }
892
893        // Find connections
894        for (var_name, &value) in solution {
895            if value && var_name.starts_with("y_") {
896                let parts: Vec<&str> = var_name[2..].split('_').collect();
897                if parts.len() == 2 {
898                    let pos1: usize = parts[0].parse().unwrap_or(0);
899                    let pos2: usize = parts[1].parse().unwrap_or(0);
900
901                    if fragment_positions.contains_key(&pos1)
902                        && fragment_positions.contains_key(&pos2)
903                    {
904                        connections.push(Connection {
905                            from_fragment: pos1,
906                            from_attachment: 0,
907                            to_fragment: pos2,
908                            to_attachment: 0,
909                            bond_type: BondType::Single,
910                        });
911                    }
912                }
913            }
914        }
915
916        // Build fragment list
917        let fragments: Vec<_> = fragment_positions
918            .iter()
919            .map(|(_, &frag_idx)| self.fragment_library.fragments[frag_idx].clone())
920            .collect();
921
922        Ok(DesignedMolecule {
923            fragments,
924            connections,
925            properties: MolecularProperties::default(),
926            score: 0.0,
927            smiles: None,
928        })
929    }
930
931    /// Calculate molecular properties
932    fn calculate_properties(&self, fragments: &[MolecularFragment]) -> MolecularProperties {
933        let mut props = MolecularProperties::default();
934
935        for fragment in fragments {
936            props.molecular_weight += fragment.properties.mw_contribution;
937            props.logp += fragment.properties.logp_contribution;
938            props.hbd += fragment.properties.hbd_count;
939            props.hba += fragment.properties.hba_count;
940            props.rotatable_bonds += fragment.properties.rotatable_count;
941            props.tpsa += fragment.properties.tpsa_contribution;
942        }
943
944        props
945    }
946
947    /// Calculate molecule score
948    fn calculate_score(&self, fragments: &[MolecularFragment], _connections: &[Connection]) -> f64 {
949        match &self.scoring_function {
950            ScoringFunction::Additive { weights } => {
951                let mut score = 0.0;
952                let props = self.calculate_properties(fragments);
953
954                // Property penalties
955                if let Some((min, max)) = self.target_properties.molecular_weight {
956                    if props.molecular_weight < min || props.molecular_weight > max {
957                        score += weights.get("mw_penalty").unwrap_or(&0.0)
958                            * (props.molecular_weight - f64::midpoint(min, max)).abs();
959                    }
960                }
961
962                // Fragment scores
963                for fragment in fragments {
964                    if let Some(&frag_score) =
965                        self.fragment_library.fragment_scores.get(&fragment.id)
966                    {
967                        score += weights.get("fragment_score").unwrap_or(&1.0) * frag_score;
968                    }
969                }
970
971                score
972            }
973            _ => 0.0,
974        }
975    }
976}
977
978impl Default for DesignConstraints {
979    fn default() -> Self {
980        Self {
981            max_mw: 500.0,
982            lipinski: true,
983            veber: true,
984            pains_filter: true,
985            max_sa_score: 6.0,
986            min_qed: 0.3,
987            smarts_filters: Vec::new(),
988        }
989    }
990}
991
992#[derive(Debug, Clone)]
993pub struct Connection {
994    pub from_fragment: usize,
995    pub from_attachment: usize,
996    pub to_fragment: usize,
997    pub to_attachment: usize,
998    pub bond_type: BondType,
999}
1000
1001#[derive(Debug, Clone)]
1002pub struct DesignedMolecule {
1003    pub fragments: Vec<MolecularFragment>,
1004    pub connections: Vec<Connection>,
1005    pub properties: MolecularProperties,
1006    pub score: f64,
1007    pub smiles: Option<String>,
1008}
1009
1010#[derive(Debug, Clone, Default)]
1011pub struct MolecularProperties {
1012    pub molecular_weight: f64,
1013    pub logp: f64,
1014    pub logs: f64,
1015    pub hbd: usize,
1016    pub hba: usize,
1017    pub rotatable_bonds: usize,
1018    pub tpsa: f64,
1019    pub sa_score: f64,
1020    pub qed_score: f64,
1021}
1022
1023/// Lead optimization
1024pub struct LeadOptimizer {
1025    /// Starting lead compound
1026    lead_compound: String,
1027    /// Optimization objectives
1028    objectives: Vec<OptimizationObjective>,
1029    /// Allowed modifications
1030    modifications: AllowedModifications,
1031    /// ADMET predictor
1032    admet_predictor: ADMETPredictor,
1033}
1034
1035#[derive(Debug, Clone)]
1036pub enum OptimizationObjective {
1037    /// Improve potency
1038    Potency { target_ic50: f64 },
1039    /// Improve selectivity
1040    Selectivity { off_targets: Vec<String> },
1041    /// Improve ADMET
1042    ADMET { properties: Vec<ADMETProperty> },
1043    /// Reduce molecular weight
1044    ReduceMW { target_mw: f64 },
1045    /// Improve solubility
1046    Solubility { target_logs: f64 },
1047}
1048
1049#[derive(Debug, Clone)]
1050pub struct AllowedModifications {
1051    /// Bioisosteric replacements
1052    pub bioisosteres: HashMap<String, Vec<String>>,
1053    /// Allowed R-group modifications
1054    pub r_groups: Vec<RGroupModification>,
1055    /// Scaffold hopping allowed
1056    pub scaffold_hopping: bool,
1057    /// Maximum similarity to lead
1058    pub max_similarity: f64,
1059}
1060
1061#[derive(Debug, Clone)]
1062pub struct RGroupModification {
1063    /// Position in molecule
1064    pub position: String,
1065    /// Allowed substituents
1066    pub substituents: Vec<String>,
1067    /// Preferred properties
1068    pub preferred_properties: HashMap<String, f64>,
1069}
1070
1071#[derive(Debug, Clone)]
1072pub struct ADMETPredictor {
1073    /// Prediction models
1074    pub models: HashMap<ADMETProperty, PredictionModel>,
1075    /// Experimental data
1076    pub experimental_data: HashMap<String, ADMETProfile>,
1077}
1078
1079#[derive(Debug, Clone)]
1080pub struct PredictionModel {
1081    /// Model type
1082    pub model_type: ModelType,
1083    /// Model parameters
1084    pub parameters: Vec<f64>,
1085    /// Accuracy metrics
1086    pub accuracy: f64,
1087}
1088
1089#[derive(Debug, Clone)]
1090pub enum ModelType {
1091    /// Random forest
1092    RandomForest,
1093    /// Neural network
1094    NeuralNetwork,
1095    /// Support vector machine
1096    SVM,
1097    /// Physics-based
1098    PhysicsBased,
1099}
1100
1101#[derive(Debug, Clone)]
1102pub struct ADMETProfile {
1103    /// Absorption properties
1104    pub absorption: AbsorptionProfile,
1105    /// Distribution properties
1106    pub distribution: DistributionProfile,
1107    /// Metabolism properties
1108    pub metabolism: MetabolismProfile,
1109    /// Excretion properties
1110    pub excretion: ExcretionProfile,
1111    /// Toxicity properties
1112    pub toxicity: ToxicityProfile,
1113}
1114
1115#[derive(Debug, Clone)]
1116pub struct AbsorptionProfile {
1117    pub caco2_permeability: f64,
1118    pub pgp_substrate: bool,
1119    pub pgp_inhibitor: bool,
1120    pub oral_bioavailability: f64,
1121}
1122
1123#[derive(Debug, Clone)]
1124pub struct DistributionProfile {
1125    pub plasma_protein_binding: f64,
1126    pub vd: f64, // Volume of distribution
1127    pub bbb_penetration: bool,
1128    pub tissue_distribution: HashMap<String, f64>,
1129}
1130
1131#[derive(Debug, Clone)]
1132pub struct MetabolismProfile {
1133    pub cyp_substrate: HashMap<String, bool>,
1134    pub cyp_inhibitor: HashMap<String, bool>,
1135    pub metabolic_stability: f64,
1136    pub major_metabolites: Vec<String>,
1137}
1138
1139#[derive(Debug, Clone)]
1140pub struct ExcretionProfile {
1141    pub renal_clearance: f64,
1142    pub hepatic_clearance: f64,
1143    pub half_life: f64,
1144}
1145
1146#[derive(Debug, Clone)]
1147pub struct ToxicityProfile {
1148    pub ld50: f64,
1149    pub mutagenicity: bool,
1150    pub hepatotoxicity: bool,
1151    pub cardiotoxicity: bool,
1152    pub herg_inhibition: f64,
1153}
1154
1155/// Virtual screening engine
1156pub struct VirtualScreeningEngine {
1157    /// Compound library
1158    library: CompoundLibrary,
1159    /// Screening protocol
1160    protocol: ScreeningProtocol,
1161    /// Hit selection criteria
1162    hit_criteria: HitSelectionCriteria,
1163}
1164
1165#[derive(Debug, Clone)]
1166pub struct CompoundLibrary {
1167    /// Library source
1168    pub source: LibrarySource,
1169    /// Number of compounds
1170    pub size: usize,
1171    /// Diversity metrics
1172    pub diversity: DiversityMetrics,
1173    /// Filters applied
1174    pub filters: Vec<LibraryFilter>,
1175}
1176
1177#[derive(Debug, Clone)]
1178pub enum LibrarySource {
1179    /// Commercial vendor
1180    Commercial { vendor: String },
1181    /// FDA approved drugs
1182    FDAApproved,
1183    /// Natural products
1184    NaturalProducts,
1185    /// Fragment library
1186    Fragments,
1187    /// Virtual enumeration
1188    Virtual { rules: Vec<EnumerationRule> },
1189}
1190
1191#[derive(Debug, Clone)]
1192pub struct DiversityMetrics {
1193    pub scaffold_diversity: f64,
1194    pub property_coverage: f64,
1195    pub pharmacophore_coverage: f64,
1196}
1197
1198#[derive(Debug, Clone)]
1199pub enum LibraryFilter {
1200    /// Molecular weight range
1201    MolecularWeight { min: f64, max: f64 },
1202    /// Lipinski compliance
1203    Lipinski,
1204    /// PAINS removal
1205    PAINS,
1206    /// Custom SMARTS
1207    SMARTS { pattern: String, exclude: bool },
1208}
1209
1210#[derive(Debug, Clone)]
1211pub struct EnumerationRule {
1212    /// Core scaffold
1213    pub scaffold: String,
1214    /// Variation points
1215    pub variation_points: Vec<VariationPoint>,
1216    /// Enumeration strategy
1217    pub strategy: EnumerationStrategy,
1218}
1219
1220#[derive(Debug, Clone)]
1221pub struct VariationPoint {
1222    /// Position in scaffold
1223    pub position: String,
1224    /// Available building blocks
1225    pub building_blocks: Vec<String>,
1226}
1227
1228#[derive(Debug, Clone)]
1229pub enum EnumerationStrategy {
1230    /// Exhaustive enumeration
1231    Exhaustive,
1232    /// Random sampling
1233    RandomSampling { size: usize },
1234    /// Focused enumeration
1235    Focused { criteria: Vec<String> },
1236}
1237
1238#[derive(Debug, Clone)]
1239pub enum ScreeningProtocol {
1240    /// Structure-based
1241    StructureBased {
1242        receptor: ProteinStructure,
1243        docking_program: DockingProgram,
1244        scoring_function: String,
1245    },
1246    /// Ligand-based
1247    LigandBased {
1248        reference_ligands: Vec<String>,
1249        similarity_metric: SimilarityMetric,
1250        threshold: f64,
1251    },
1252    /// Pharmacophore-based
1253    PharmacaophoreBased {
1254        pharmacophore: PharmacophoreModel,
1255        tolerance: f64,
1256    },
1257    /// Machine learning
1258    MachineLearning {
1259        model: String,
1260        features: Vec<String>,
1261    },
1262}
1263
1264#[derive(Debug, Clone)]
1265pub enum DockingProgram {
1266    AutoDock,
1267    Glide,
1268    FlexX,
1269    GOLD,
1270    Vina,
1271}
1272
1273#[derive(Debug, Clone)]
1274pub enum SimilarityMetric {
1275    Tanimoto,
1276    Dice,
1277    Cosine,
1278    Euclidean,
1279}
1280
1281#[derive(Debug, Clone)]
1282pub struct HitSelectionCriteria {
1283    /// Score threshold
1284    pub score_threshold: f64,
1285    /// Top N compounds
1286    pub top_n: Option<usize>,
1287    /// Diversity selection
1288    pub diversity_selection: bool,
1289    /// Visual inspection required
1290    pub manual_inspection: bool,
1291}
1292
1293#[cfg(test)]
1294mod tests {
1295    use super::*;
1296
1297    #[test]
1298    fn test_molecular_design() {
1299        let target = TargetProperties {
1300            molecular_weight: Some((300.0, 500.0)),
1301            logp: Some((2.0, 5.0)),
1302            logs: None,
1303            hbd: Some((0, 5)),
1304            hba: Some((0, 10)),
1305            rotatable_bonds: Some((0, 10)),
1306            tpsa: Some((40.0, 140.0)),
1307            custom_descriptors: HashMap::new(),
1308        };
1309
1310        let mut fragments = Vec::new();
1311        for i in 0..5 {
1312            fragments.push(MolecularFragment {
1313                id: i,
1314                smiles: format!("C{}O", "C".repeat(i)),
1315                attachment_points: vec![AttachmentPoint {
1316                    atom_idx: 0,
1317                    bond_types: vec![BondType::Single],
1318                    direction: Vec3D {
1319                        x: 1.0,
1320                        y: 0.0,
1321                        z: 0.0,
1322                    },
1323                }],
1324                properties: FragmentProperties {
1325                    mw_contribution: (i as f64).mul_add(14.0, 50.0),
1326                    logp_contribution: (i as f64).mul_add(0.5, 0.5),
1327                    hbd_count: 1,
1328                    hba_count: 1,
1329                    rotatable_count: i,
1330                    tpsa_contribution: 20.0,
1331                },
1332                pharmacophores: vec![],
1333            });
1334        }
1335
1336        let library = FragmentLibrary {
1337            fragments,
1338            connection_rules: ConnectionRules {
1339                compatible_pairs: HashMap::new(),
1340                forbidden_connections: HashSet::new(),
1341                reaction_templates: vec![],
1342            },
1343            fragment_scores: HashMap::new(),
1344            privileged_scaffolds: vec![],
1345        };
1346
1347        let optimizer = MolecularDesignOptimizer::new(target, library);
1348        let mut result = optimizer.build_qubo();
1349        assert!(result.is_ok());
1350    }
1351
1352    #[test]
1353    fn test_fragment_growing() {
1354        let core = MolecularFragment {
1355            id: 999,
1356            smiles: "c1ccccc1".to_string(),
1357            attachment_points: vec![
1358                AttachmentPoint {
1359                    atom_idx: 0,
1360                    bond_types: vec![BondType::Single, BondType::Aromatic],
1361                    direction: Vec3D {
1362                        x: 1.0,
1363                        y: 0.0,
1364                        z: 0.0,
1365                    },
1366                },
1367                AttachmentPoint {
1368                    atom_idx: 3,
1369                    bond_types: vec![BondType::Single, BondType::Aromatic],
1370                    direction: Vec3D {
1371                        x: -1.0,
1372                        y: 0.0,
1373                        z: 0.0,
1374                    },
1375                },
1376            ],
1377            properties: FragmentProperties {
1378                mw_contribution: 78.0,
1379                logp_contribution: 2.0,
1380                hbd_count: 0,
1381                hba_count: 0,
1382                rotatable_count: 0,
1383                tpsa_contribution: 0.0,
1384            },
1385            pharmacophores: vec![PharmacophoreFeature {
1386                feature_type: PharmacophoreType::Aromatic,
1387                position: Vec3D {
1388                    x: 0.0,
1389                    y: 0.0,
1390                    z: 0.0,
1391                },
1392                tolerance: 1.0,
1393            }],
1394        };
1395
1396        let target = TargetProperties {
1397            molecular_weight: Some((200.0, 400.0)),
1398            logp: Some((1.0, 4.0)),
1399            logs: None,
1400            hbd: Some((0, 3)),
1401            hba: Some((0, 6)),
1402            rotatable_bonds: None,
1403            tpsa: None,
1404            custom_descriptors: HashMap::new(),
1405        };
1406
1407        let library = FragmentLibrary {
1408            fragments: vec![MolecularFragment {
1409                id: 0,
1410                smiles: "CCO".to_string(),
1411                attachment_points: vec![AttachmentPoint {
1412                    atom_idx: 0,
1413                    bond_types: vec![BondType::Single],
1414                    direction: Vec3D {
1415                        x: 1.0,
1416                        y: 0.0,
1417                        z: 0.0,
1418                    },
1419                }],
1420                properties: FragmentProperties {
1421                    mw_contribution: 45.0,
1422                    logp_contribution: 0.2,
1423                    hbd_count: 1,
1424                    hba_count: 1,
1425                    rotatable_count: 1,
1426                    tpsa_contribution: 20.0,
1427                },
1428                pharmacophores: vec![],
1429            }],
1430            connection_rules: ConnectionRules {
1431                compatible_pairs: HashMap::new(),
1432                forbidden_connections: HashSet::new(),
1433                reaction_templates: vec![],
1434            },
1435            fragment_scores: {
1436                let mut scores = HashMap::new();
1437                scores.insert(0, 1.0);
1438                scores
1439            },
1440            privileged_scaffolds: vec![],
1441        };
1442
1443        let optimizer = MolecularDesignOptimizer::new(target, library)
1444            .with_strategy(OptimizationStrategy::FragmentGrowing { core });
1445
1446        let mut result = optimizer.build_qubo();
1447        assert!(result.is_ok());
1448
1449        let (_qubo, var_map) = result.expect("QUBO building should succeed after is_ok check");
1450        assert!(!var_map.is_empty());
1451    }
1452}