quantrs2_anneal/applications/
drug_discovery.rs

1//! Drug Discovery Molecular Optimization with Advanced Quantum Algorithms
2//!
3//! This module implements cutting-edge drug discovery optimization using quantum annealing
4//! with integrated quantum error correction and advanced algorithms. It addresses fundamental
5//! pharmaceutical challenges including molecular design, drug-target interaction optimization,
6//! ADMET property prediction, and multi-objective drug development.
7//!
8//! Key Features:
9//! - Molecular graph representation and SMILES encoding
10//! - Drug-target binding affinity optimization
11//! - Multi-objective optimization (efficacy, safety, synthesizability)
12//! - ADMET property prediction and optimization
13//! - Fragment-based drug design
14//! - Lead compound optimization with quantum algorithms
15//! - Structure-Activity Relationship (SAR) modeling
16//! - Quantum molecular descriptors and property calculation
17//! - Advanced quantum error correction for molecular simulations
18
19use std::collections::{HashMap, HashSet, VecDeque};
20use std::fmt;
21
22use crate::advanced_quantum_algorithms::{
23    AdiabaticShortcutsOptimizer, AdvancedAlgorithmConfig, AdvancedQuantumAlgorithms,
24    AlgorithmSelectionStrategy, InfiniteDepthQAOA, InfiniteQAOAConfig, QuantumZenoAnnealer,
25    ShortcutsConfig, ZenoConfig,
26};
27use crate::applications::{
28    ApplicationError, ApplicationResult, IndustrySolution, OptimizationProblem,
29};
30use crate::bayesian_hyperopt::{optimize_annealing_parameters, BayesianHyperoptimizer};
31use crate::ising::{IsingModel, QuboModel};
32use crate::neural_annealing_schedules::{NeuralAnnealingScheduler, NeuralSchedulerConfig};
33use crate::quantum_error_correction::{
34    ErrorCorrectionCode, ErrorMitigationConfig, ErrorMitigationManager, LogicalAnnealingEncoder,
35    NoiseResilientAnnealingProtocol, SyndromeDetector,
36};
37use crate::simulator::{AnnealingParams, AnnealingResult, QuantumAnnealingSimulator};
38use std::fmt::Write;
39
40/// Chemical elements for molecular representation
41#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
42pub enum AtomType {
43    Hydrogen,
44    Carbon,
45    Nitrogen,
46    Oxygen,
47    Phosphorus,
48    Sulfur,
49    Fluorine,
50    Chlorine,
51    Bromine,
52    Iodine,
53    Custom(u8), // For other elements
54}
55
56impl AtomType {
57    #[must_use]
58    pub const fn atomic_number(&self) -> u8 {
59        match self {
60            Self::Hydrogen => 1,
61            Self::Carbon => 6,
62            Self::Nitrogen => 7,
63            Self::Oxygen => 8,
64            Self::Phosphorus => 15,
65            Self::Sulfur => 16,
66            Self::Fluorine => 9,
67            Self::Chlorine => 17,
68            Self::Bromine => 35,
69            Self::Iodine => 53,
70            Self::Custom(n) => *n,
71        }
72    }
73
74    #[must_use]
75    pub const fn symbol(&self) -> &'static str {
76        match self {
77            Self::Hydrogen => "H",
78            Self::Carbon => "C",
79            Self::Nitrogen => "N",
80            Self::Oxygen => "O",
81            Self::Phosphorus => "P",
82            Self::Sulfur => "S",
83            Self::Fluorine => "F",
84            Self::Chlorine => "Cl",
85            Self::Bromine => "Br",
86            Self::Iodine => "I",
87            Self::Custom(_) => "X",
88        }
89    }
90
91    #[must_use]
92    pub const fn valence(&self) -> u8 {
93        match self {
94            Self::Hydrogen | Self::Fluorine | Self::Chlorine => 1,
95            Self::Carbon => 4,
96            Self::Nitrogen => 3,
97            Self::Oxygen => 2,
98            Self::Phosphorus => 5,
99            Self::Sulfur => 6,
100            Self::Bromine => 1,
101            Self::Iodine => 1,
102            Self::Custom(_) => 4, // Default
103        }
104    }
105}
106
107/// Atomic hybridization states
108#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
109pub enum Hybridization {
110    SP,
111    SP2,
112    SP3,
113    SP3D,
114    SP3D2,
115}
116
117/// 3D position coordinates
118#[derive(Debug, Clone, Copy, PartialEq)]
119pub struct Position3D {
120    pub x: f64,
121    pub y: f64,
122    pub z: f64,
123}
124
125impl Position3D {
126    #[must_use]
127    pub const fn new(x: f64, y: f64, z: f64) -> Self {
128        Self { x, y, z }
129    }
130
131    #[must_use]
132    pub fn distance(&self, other: &Self) -> f64 {
133        (self.z - other.z)
134            .mul_add(
135                self.z - other.z,
136                (self.y - other.y).mul_add(self.y - other.y, (self.x - other.x).powi(2)),
137            )
138            .sqrt()
139    }
140}
141
142/// Bond types in molecular graphs
143#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
144pub enum BondType {
145    Single,
146    Double,
147    Triple,
148    Aromatic,
149}
150
151impl BondType {
152    #[must_use]
153    pub const fn bond_order(&self) -> f64 {
154        match self {
155            Self::Single => 1.0,
156            Self::Double => 2.0,
157            Self::Triple => 3.0,
158            Self::Aromatic => 1.5,
159        }
160    }
161}
162
163/// Molecular atom with properties
164#[derive(Debug, Clone)]
165pub struct Atom {
166    pub id: usize,
167    pub atom_type: AtomType,
168    pub formal_charge: i8,
169    pub hybridization: Option<String>,
170    pub aromatic: bool,
171    pub coordinates: Option<[f64; 3]>, // 3D coordinates if available
172}
173
174impl Atom {
175    #[must_use]
176    pub const fn new(id: usize, atom_type: AtomType) -> Self {
177        Self {
178            id,
179            atom_type,
180            formal_charge: 0,
181            hybridization: None,
182            aromatic: false,
183            coordinates: None,
184        }
185    }
186
187    #[must_use]
188    pub const fn with_charge(mut self, charge: i8) -> Self {
189        self.formal_charge = charge;
190        self
191    }
192
193    #[must_use]
194    pub const fn with_coordinates(mut self, coords: [f64; 3]) -> Self {
195        self.coordinates = Some(coords);
196        self
197    }
198
199    #[must_use]
200    pub const fn set_aromatic(mut self, aromatic: bool) -> Self {
201        self.aromatic = aromatic;
202        self
203    }
204}
205
206/// Chemical bond between atoms
207#[derive(Debug, Clone)]
208pub struct Bond {
209    pub atom1: usize,
210    pub atom2: usize,
211    pub bond_type: BondType,
212    pub aromatic: bool,
213    pub in_ring: bool,
214}
215
216impl Bond {
217    #[must_use]
218    pub const fn new(atom1: usize, atom2: usize, bond_type: BondType) -> Self {
219        Self {
220            atom1,
221            atom2,
222            bond_type,
223            aromatic: false,
224            in_ring: false,
225        }
226    }
227
228    #[must_use]
229    pub const fn set_aromatic(mut self, aromatic: bool) -> Self {
230        self.aromatic = aromatic;
231        self
232    }
233
234    #[must_use]
235    pub const fn set_in_ring(mut self, in_ring: bool) -> Self {
236        self.in_ring = in_ring;
237        self
238    }
239}
240
241/// Molecular graph representation
242#[derive(Debug, Clone)]
243pub struct Molecule {
244    pub id: String,
245    pub atoms: Vec<Atom>,
246    pub bonds: Vec<Bond>,
247    pub smiles: Option<String>,
248    pub properties: HashMap<String, f64>,
249}
250
251impl Molecule {
252    #[must_use]
253    pub fn new(id: String) -> Self {
254        Self {
255            id,
256            atoms: Vec::new(),
257            bonds: Vec::new(),
258            smiles: None,
259            properties: HashMap::new(),
260        }
261    }
262
263    pub fn add_atom(&mut self, atom: Atom) -> usize {
264        let id = self.atoms.len();
265        self.atoms.push(atom);
266        id
267    }
268
269    pub fn add_bond(&mut self, bond: Bond) {
270        self.bonds.push(bond);
271    }
272
273    pub fn set_smiles(&mut self, smiles: String) {
274        self.smiles = Some(smiles);
275    }
276
277    pub fn set_property(&mut self, name: String, value: f64) {
278        self.properties.insert(name, value);
279    }
280
281    /// Calculate molecular weight
282    #[must_use]
283    pub fn molecular_weight(&self) -> f64 {
284        self.atoms
285            .iter()
286            .map(|atom| {
287                match atom.atom_type {
288                    AtomType::Hydrogen => 1.008,
289                    AtomType::Carbon => 12.011,
290                    AtomType::Nitrogen => 14.007,
291                    AtomType::Oxygen => 15.999,
292                    AtomType::Phosphorus => 30.974,
293                    AtomType::Sulfur => 32.065,
294                    AtomType::Fluorine => 18.998,
295                    AtomType::Chlorine => 35.453,
296                    AtomType::Bromine => 79.904,
297                    AtomType::Iodine => 126.904,
298                    AtomType::Custom(_) => 12.0, // Default
299                }
300            })
301            .sum()
302    }
303
304    /// Calculate `LogP` (lipophilicity) approximation
305    #[must_use]
306    pub fn logp_approximation(&self) -> f64 {
307        let mut logp = 0.0;
308
309        // Simplified LogP calculation based on atom contributions
310        for atom in &self.atoms {
311            match atom.atom_type {
312                AtomType::Carbon => logp += 0.5,
313                AtomType::Nitrogen => logp -= 0.5,
314                AtomType::Oxygen => logp -= 1.0,
315                AtomType::Sulfur => logp += 0.2,
316                AtomType::Fluorine => logp += 0.1,
317                AtomType::Chlorine => logp += 0.7,
318                AtomType::Bromine => logp += 1.0,
319                _ => {}
320            }
321        }
322
323        logp
324    }
325
326    /// Calculate topological polar surface area (TPSA) approximation
327    #[must_use]
328    pub fn tpsa_approximation(&self) -> f64 {
329        let mut tpsa = 0.0;
330
331        for atom in &self.atoms {
332            match atom.atom_type {
333                AtomType::Nitrogen => tpsa += 23.79,
334                AtomType::Oxygen => tpsa += 23.06,
335                _ => {}
336            }
337        }
338
339        tpsa
340    }
341
342    /// Count rotatable bonds
343    #[must_use]
344    pub fn rotatable_bonds(&self) -> usize {
345        self.bonds
346            .iter()
347            .filter(|bond| {
348                bond.bond_type == BondType::Single && !bond.in_ring && !self.is_terminal_bond(bond)
349            })
350            .count()
351    }
352
353    fn is_terminal_bond(&self, bond: &Bond) -> bool {
354        let atom1_degree = self
355            .bonds
356            .iter()
357            .filter(|b| b.atom1 == bond.atom1 || b.atom2 == bond.atom2)
358            .count();
359        let atom2_degree = self
360            .bonds
361            .iter()
362            .filter(|b| b.atom1 == bond.atom1 || b.atom2 == bond.atom2)
363            .count();
364        atom1_degree == 1 || atom2_degree == 1
365    }
366
367    /// Calculate drug-likeness score (Lipinski's Rule of Five compliance)
368    #[must_use]
369    pub fn drug_likeness_score(&self) -> f64 {
370        let mw = self.molecular_weight();
371        let logp = self.logp_approximation();
372        let tpsa = self.tpsa_approximation();
373        let rot_bonds = self.rotatable_bonds() as f64;
374
375        let mut score = 1.0;
376
377        // Molecular weight penalty
378        if mw > 500.0 {
379            score *= 0.5;
380        }
381
382        // LogP penalty
383        if logp > 5.0 || logp < -2.0 {
384            score *= 0.5;
385        }
386
387        // TPSA penalty
388        if tpsa > 140.0 {
389            score *= 0.7;
390        }
391
392        // Rotatable bonds penalty
393        if rot_bonds > 10.0 {
394            score *= 0.8;
395        }
396
397        score
398    }
399}
400
401/// Drug-target interaction representation
402#[derive(Debug, Clone)]
403pub struct DrugTargetInteraction {
404    pub drug_molecule: Molecule,
405    pub target_id: String,
406    pub target_type: TargetType,
407    pub binding_affinity: Option<f64>,     // pKd or pIC50
408    pub selectivity: HashMap<String, f64>, // Off-target interactions
409    pub admet_properties: AdmetProperties,
410}
411
412/// Types of drug targets
413#[derive(Debug, Clone, PartialEq, Eq)]
414pub enum TargetType {
415    Enzyme,
416    Receptor,
417    IonChannel,
418    Transporter,
419    StructuralProtein,
420    Other(String),
421}
422
423/// ADMET (Absorption, Distribution, Metabolism, Excretion, Toxicity) properties
424#[derive(Debug, Clone)]
425pub struct AdmetProperties {
426    pub absorption: AdsorptionProperties,
427    pub distribution: DistributionProperties,
428    pub metabolism: MetabolismProperties,
429    pub excretion: ExcretionProperties,
430    pub toxicity: ToxicityProperties,
431}
432
433#[derive(Debug, Clone)]
434pub struct AdsorptionProperties {
435    pub bioavailability: Option<f64>,
436    pub permeability: Option<f64>,
437    pub solubility: Option<f64>,
438}
439
440#[derive(Debug, Clone)]
441pub struct DistributionProperties {
442    pub volume_distribution: Option<f64>,
443    pub protein_binding: Option<f64>,
444    pub blood_brain_barrier: Option<f64>,
445}
446
447#[derive(Debug, Clone)]
448pub struct MetabolismProperties {
449    pub clearance: Option<f64>,
450    pub half_life: Option<f64>,
451    pub cyp_inhibition: HashMap<String, f64>,
452}
453
454#[derive(Debug, Clone)]
455pub struct ExcretionProperties {
456    pub renal_clearance: Option<f64>,
457    pub biliary_excretion: Option<f64>,
458}
459
460#[derive(Debug, Clone)]
461pub struct ToxicityProperties {
462    pub cytotoxicity: Option<f64>,
463    pub hepatotoxicity: Option<f64>,
464    pub cardiotoxicity: Option<f64>,
465    pub mutagenicity: Option<f64>,
466}
467
468impl Default for AdmetProperties {
469    fn default() -> Self {
470        Self {
471            absorption: AdsorptionProperties {
472                bioavailability: None,
473                permeability: None,
474                solubility: None,
475            },
476            distribution: DistributionProperties {
477                volume_distribution: None,
478                protein_binding: None,
479                blood_brain_barrier: None,
480            },
481            metabolism: MetabolismProperties {
482                clearance: None,
483                half_life: None,
484                cyp_inhibition: HashMap::new(),
485            },
486            excretion: ExcretionProperties {
487                renal_clearance: None,
488                biliary_excretion: None,
489            },
490            toxicity: ToxicityProperties {
491                cytotoxicity: None,
492                hepatotoxicity: None,
493                cardiotoxicity: None,
494                mutagenicity: None,
495            },
496        }
497    }
498}
499
500/// Drug discovery optimization objectives
501#[derive(Debug, Clone)]
502pub enum DrugOptimizationObjective {
503    /// Maximize binding affinity to target
504    MaximizeAffinity,
505    /// Minimize off-target effects
506    MinimizeOffTarget,
507    /// Maximize drug-likeness
508    MaximizeDrugLikeness,
509    /// Minimize toxicity
510    MinimizeToxicity,
511    /// Maximize synthesizability
512    MaximizeSynthesizability,
513    /// Optimize ADMET properties
514    OptimizeAdmet,
515    /// Multi-objective combination
516    MultiObjective(Vec<(Self, f64)>),
517}
518
519/// Drug discovery optimization problem
520#[derive(Debug, Clone)]
521pub struct DrugDiscoveryProblem {
522    /// Target molecule or interaction
523    pub target_interaction: DrugTargetInteraction,
524    /// Optimization objectives
525    pub objectives: Vec<DrugOptimizationObjective>,
526    /// Molecular constraints
527    pub constraints: Vec<MolecularConstraint>,
528    /// Quantum error correction framework
529    pub qec_framework: Option<String>,
530    /// Advanced algorithm configuration
531    pub advanced_config: AdvancedAlgorithmConfig,
532    /// Neural scheduling configuration
533    pub neural_config: Option<NeuralSchedulerConfig>,
534}
535
536/// Molecular design constraints
537#[derive(Debug, Clone)]
538pub enum MolecularConstraint {
539    /// Molecular weight range
540    MolecularWeightRange { min: f64, max: f64 },
541    /// `LogP` range for lipophilicity
542    LogPRange { min: f64, max: f64 },
543    /// TPSA constraint
544    TpsaLimit(f64),
545    /// Maximum rotatable bonds
546    MaxRotatableBonds(usize),
547    /// Required functional groups
548    RequiredGroups(Vec<String>),
549    /// Forbidden substructures
550    ForbiddenSubstructures(Vec<String>),
551    /// Synthesizability score minimum
552    MinSynthesizability(f64),
553    /// Maximum heavy atoms
554    MaxHeavyAtoms(usize),
555}
556
557impl DrugDiscoveryProblem {
558    #[must_use]
559    pub fn new(target_interaction: DrugTargetInteraction) -> Self {
560        Self {
561            target_interaction,
562            objectives: vec![DrugOptimizationObjective::MaximizeAffinity],
563            constraints: vec![],
564            qec_framework: None,
565            advanced_config: AdvancedAlgorithmConfig {
566                enable_infinite_qaoa: true,
567                enable_zeno_annealing: true,
568                enable_adiabatic_shortcuts: true,
569                enable_counterdiabatic: true,
570                selection_strategy: AlgorithmSelectionStrategy::ProblemSpecific,
571                track_performance: true,
572            },
573            neural_config: None,
574        }
575    }
576
577    #[must_use]
578    pub fn with_quantum_error_correction(mut self, config: String) -> Self {
579        self.qec_framework = Some(config);
580        self
581    }
582
583    #[must_use]
584    pub fn with_neural_annealing(mut self, config: NeuralSchedulerConfig) -> Self {
585        self.neural_config = Some(config);
586        self
587    }
588
589    #[must_use]
590    pub fn add_objective(mut self, objective: DrugOptimizationObjective) -> Self {
591        self.objectives.push(objective);
592        self
593    }
594
595    #[must_use]
596    pub fn add_constraint(mut self, constraint: MolecularConstraint) -> Self {
597        self.constraints.push(constraint);
598        self
599    }
600
601    /// Solve using infinite-depth QAOA
602    pub fn solve_with_infinite_qaoa(&self) -> ApplicationResult<Molecule> {
603        println!("Starting drug discovery optimization with infinite-depth QAOA");
604
605        let (ising_model, variable_map) = self.to_ising_model()?;
606
607        let qaoa_config = InfiniteQAOAConfig {
608            max_depth: 50,
609            initial_depth: 3,
610            optimization_tolerance: 1e-8,
611            ..Default::default()
612        };
613
614        let mut qaoa = InfiniteDepthQAOA::new(qaoa_config);
615        let result = qaoa.solve(&ising_model).map_err(|e| {
616            ApplicationError::OptimizationError(format!("Infinite QAOA failed: {e:?}"))
617        })?;
618
619        let solution = result
620            .map_err(|e| ApplicationError::OptimizationError(format!("QAOA solver error: {e}")))?;
621
622        self.solution_to_molecule(&solution, &variable_map)
623    }
624
625    /// Solve using Quantum Zeno annealing
626    pub fn solve_with_zeno_annealing(&self) -> ApplicationResult<Molecule> {
627        println!("Starting drug discovery optimization with Quantum Zeno annealing");
628
629        let (ising_model, variable_map) = self.to_ising_model()?;
630
631        let zeno_config = ZenoConfig {
632            measurement_frequency: 2.0,
633            total_evolution_time: 20.0,
634            evolution_time_step: 0.05,
635            ..Default::default()
636        };
637
638        let mut zeno_annealer = QuantumZenoAnnealer::new(zeno_config);
639        let result = zeno_annealer.solve(&ising_model).map_err(|e| {
640            ApplicationError::OptimizationError(format!("Zeno annealing failed: {e:?}"))
641        })?;
642
643        let solution = result
644            .map_err(|e| ApplicationError::OptimizationError(format!("Zeno solver error: {e}")))?;
645
646        self.solution_to_molecule(&solution, &variable_map)
647    }
648
649    /// Solve using adiabatic shortcuts
650    pub fn solve_with_adiabatic_shortcuts(&self) -> ApplicationResult<Molecule> {
651        println!("Starting drug discovery optimization with adiabatic shortcuts");
652
653        let (ising_model, variable_map) = self.to_ising_model()?;
654
655        let shortcuts_config = ShortcutsConfig::default();
656        let mut shortcuts_optimizer = AdiabaticShortcutsOptimizer::new(shortcuts_config);
657
658        let result = shortcuts_optimizer.solve(&ising_model).map_err(|e| {
659            ApplicationError::OptimizationError(format!("Adiabatic shortcuts failed: {e:?}"))
660        })?;
661
662        let solution = result.map_err(|e| {
663            ApplicationError::OptimizationError(format!("Shortcuts solver error: {e}"))
664        })?;
665
666        self.solution_to_molecule(&solution, &variable_map)
667    }
668
669    /// Solve with quantum error correction
670    pub fn solve_with_error_correction(&self) -> ApplicationResult<Molecule> {
671        if let Some(ref qec_framework) = self.qec_framework {
672            println!("Starting noise-resilient drug discovery optimization");
673
674            let (ising_model, variable_map) = self.to_ising_model()?;
675
676            // Use error mitigation for molecular optimization
677            let error_config = ErrorMitigationConfig::default();
678            let mut error_manager = ErrorMitigationManager::new(error_config).map_err(|e| {
679                ApplicationError::OptimizationError(format!(
680                    "Failed to create error manager: {e:?}"
681                ))
682            })?;
683
684            // First perform standard annealing
685            let params = AnnealingParams::default();
686            let annealer = QuantumAnnealingSimulator::new(params.clone()).map_err(|e| {
687                ApplicationError::OptimizationError(format!("Failed to create annealer: {e:?}"))
688            })?;
689            let annealing_result = annealer.solve(&ising_model).map_err(|e| {
690                ApplicationError::OptimizationError(format!("Annealing failed: {e:?}"))
691            })?;
692
693            // Convert simulator result to error mitigation format
694            let error_mitigation_result =
695                crate::quantum_error_correction::error_mitigation::AnnealingResult {
696                    solution: annealing_result
697                        .best_spins
698                        .iter()
699                        .map(|&x| i32::from(x))
700                        .collect(),
701                    energy: annealing_result.best_energy,
702                    num_occurrences: 1,
703                    chain_break_fraction: 0.0,
704                    timing: std::collections::HashMap::new(),
705                    info: std::collections::HashMap::new(),
706                };
707
708            // Apply error mitigation to improve the result
709            let mitigation_result = error_manager
710                .apply_mitigation(&ising_model, error_mitigation_result, &params)
711                .map_err(|e| {
712                    ApplicationError::OptimizationError(format!("Error mitigation failed: {e:?}"))
713                })?;
714
715            let solution = &mitigation_result.mitigated_result.solution;
716
717            self.solution_to_molecule(solution, &variable_map)
718        } else {
719            Err(ApplicationError::InvalidConfiguration(
720                "Quantum error correction not enabled".to_string(),
721            ))
722        }
723    }
724
725    /// Optimize molecular parameters using Bayesian optimization
726    pub fn optimize_molecular_parameters(&self) -> ApplicationResult<HashMap<String, f64>> {
727        println!("Optimizing molecular parameters with Bayesian optimization");
728
729        let objective = |params: &[f64]| -> f64 {
730            // params[0] = molecular weight target, params[1] = logP target, params[2] = TPSA target
731            let mw_target = params[0];
732            let logp_target = params[1];
733            let tpsa_target = params[2];
734
735            let current_mw = self.target_interaction.drug_molecule.molecular_weight();
736            let current_logp = self.target_interaction.drug_molecule.logp_approximation();
737            let current_tpsa = self.target_interaction.drug_molecule.tpsa_approximation();
738
739            // Calculate optimization score based on distance from targets
740            let mw_score = -((current_mw - mw_target) / 100.0).powi(2);
741            let logp_score = -((current_logp - logp_target) / 2.0).powi(2);
742            let tpsa_score = -((current_tpsa - tpsa_target) / 50.0).powi(2);
743
744            // Combined score with drug-likeness
745            let drug_likeness = self.target_interaction.drug_molecule.drug_likeness_score();
746
747            mw_score + logp_score + tpsa_score + drug_likeness
748        };
749
750        let best_params = optimize_annealing_parameters(objective, Some(40)).map_err(|e| {
751            ApplicationError::OptimizationError(format!("Bayesian optimization failed: {e:?}"))
752        })?;
753
754        let mut result = HashMap::new();
755        result.insert("optimal_molecular_weight".to_string(), best_params[0]);
756        result.insert("optimal_logp".to_string(), best_params[1]);
757        result.insert("optimal_tpsa".to_string(), best_params[2]);
758
759        Ok(result)
760    }
761
762    /// Convert to Ising model representation
763    fn to_ising_model(&self) -> ApplicationResult<(IsingModel, HashMap<String, usize>)> {
764        let num_atoms = self.target_interaction.drug_molecule.atoms.len();
765        let num_variables = num_atoms * 8; // 8 bits per atom for type encoding
766
767        let mut ising = IsingModel::new(num_variables);
768        let mut variable_map = HashMap::new();
769
770        // Map molecular variables to Ising variables
771        for (i, atom) in self
772            .target_interaction
773            .drug_molecule
774            .atoms
775            .iter()
776            .enumerate()
777        {
778            for bit in 0..8 {
779                variable_map.insert(format!("atom_{i}_bit_{bit}"), i * 8 + bit);
780            }
781        }
782
783        // Add objective-based bias terms
784        for (i, atom) in self
785            .target_interaction
786            .drug_molecule
787            .atoms
788            .iter()
789            .enumerate()
790        {
791            for bit in 0..8 {
792                let var_idx = i * 8 + bit;
793                let mut bias = 0.0;
794
795                // Add objective-specific bias
796                for objective in &self.objectives {
797                    match objective {
798                        DrugOptimizationObjective::MaximizeAffinity => {
799                            // Encourage beneficial atom types for binding
800                            match atom.atom_type {
801                                AtomType::Nitrogen | AtomType::Oxygen => bias -= 0.5,
802                                AtomType::Carbon => bias -= 0.2,
803                                _ => bias += 0.1,
804                            }
805                        }
806                        DrugOptimizationObjective::MaximizeDrugLikeness => {
807                            // Encourage drug-like properties
808                            bias -=
809                                self.target_interaction.drug_molecule.drug_likeness_score() * 0.1;
810                        }
811                        DrugOptimizationObjective::MinimizeToxicity => {
812                            // Penalize potentially toxic groups
813                            if matches!(atom.atom_type, AtomType::Bromine | AtomType::Iodine) {
814                                bias += 1.0;
815                            }
816                        }
817                        _ => {
818                            bias += 0.05; // Default small bias
819                        }
820                    }
821                }
822
823                ising
824                    .set_bias(var_idx, bias)
825                    .map_err(|e| ApplicationError::OptimizationError(e.to_string()))?;
826            }
827        }
828
829        // Add coupling terms for molecular connectivity
830        for bond in &self.target_interaction.drug_molecule.bonds {
831            let atom1_base = bond.atom1 * 8;
832            let atom2_base = bond.atom2 * 8;
833
834            for bit1 in 0..8 {
835                for bit2 in 0..8 {
836                    let var1 = atom1_base + bit1;
837                    let var2 = atom2_base + bit2;
838
839                    if var1 < var2 && var1 < num_variables && var2 < num_variables {
840                        let coupling = bond.bond_type.bond_order() * 0.1;
841                        ising
842                            .set_coupling(var1, var2, -coupling)
843                            .map_err(|e| ApplicationError::OptimizationError(e.to_string()))?;
844                        // Negative for stability
845                    }
846                }
847            }
848        }
849
850        Ok((ising, variable_map))
851    }
852
853    /// Convert Ising solution back to molecular structure
854    fn solution_to_molecule(
855        &self,
856        solution: &[i32],
857        variable_map: &HashMap<String, usize>,
858    ) -> ApplicationResult<Molecule> {
859        let mut optimized_molecule = self.target_interaction.drug_molecule.clone();
860
861        // Update molecular structure based on solution
862        for (i, atom) in optimized_molecule.atoms.iter_mut().enumerate() {
863            // Decode atom type from binary solution
864            let mut atom_encoding = 0u8;
865
866            for bit in 0..8 {
867                if let Some(&var_index) = variable_map.get(&format!("atom_{i}_bit_{bit}")) {
868                    if var_index < solution.len() && solution[var_index] > 0 {
869                        atom_encoding |= 1 << bit;
870                    }
871                }
872            }
873
874            // Update atom type based on encoding
875            atom.atom_type = match atom_encoding % 10 {
876                0 => AtomType::Carbon,
877                1 => AtomType::Nitrogen,
878                2 => AtomType::Oxygen,
879                3 => AtomType::Sulfur,
880                4 => AtomType::Phosphorus,
881                5 => AtomType::Fluorine,
882                6 => AtomType::Chlorine,
883                7 => AtomType::Hydrogen,
884                _ => atom.atom_type, // Keep original
885            };
886        }
887
888        // Recalculate molecular properties
889        optimized_molecule.set_property(
890            "molecular_weight".to_string(),
891            optimized_molecule.molecular_weight(),
892        );
893        optimized_molecule
894            .set_property("logp".to_string(), optimized_molecule.logp_approximation());
895        optimized_molecule
896            .set_property("tpsa".to_string(), optimized_molecule.tpsa_approximation());
897        optimized_molecule.set_property(
898            "drug_likeness".to_string(),
899            optimized_molecule.drug_likeness_score(),
900        );
901
902        Ok(optimized_molecule)
903    }
904}
905
906impl OptimizationProblem for DrugDiscoveryProblem {
907    type Solution = Molecule;
908    type ObjectiveValue = f64;
909
910    fn description(&self) -> String {
911        format!(
912            "Drug discovery optimization for {} targeting {} (MW: {:.2})",
913            self.target_interaction.drug_molecule.id,
914            self.target_interaction.target_id,
915            self.target_interaction.drug_molecule.molecular_weight()
916        )
917    }
918
919    fn size_metrics(&self) -> HashMap<String, usize> {
920        let mut metrics = HashMap::new();
921        metrics.insert(
922            "num_atoms".to_string(),
923            self.target_interaction.drug_molecule.atoms.len(),
924        );
925        metrics.insert(
926            "num_bonds".to_string(),
927            self.target_interaction.drug_molecule.bonds.len(),
928        );
929        metrics.insert(
930            "rotatable_bonds".to_string(),
931            self.target_interaction.drug_molecule.rotatable_bonds(),
932        );
933        metrics.insert(
934            "variables".to_string(),
935            self.target_interaction.drug_molecule.atoms.len() * 8,
936        );
937        metrics
938    }
939
940    fn validate(&self) -> ApplicationResult<()> {
941        if self.target_interaction.drug_molecule.atoms.is_empty() {
942            return Err(ApplicationError::DataValidationError(
943                "Molecule must have at least one atom".to_string(),
944            ));
945        }
946
947        if self.target_interaction.drug_molecule.atoms.len() > 100 {
948            return Err(ApplicationError::ResourceLimitExceeded(
949                "Molecule too large for current implementation".to_string(),
950            ));
951        }
952
953        // Validate molecular constraints
954        for constraint in &self.constraints {
955            match constraint {
956                MolecularConstraint::MolecularWeightRange { min, max } => {
957                    let mw = self.target_interaction.drug_molecule.molecular_weight();
958                    if mw < *min || mw > *max {
959                        return Err(ApplicationError::ConstraintViolation(format!(
960                            "Molecular weight {mw} outside range [{min}, {max}]"
961                        )));
962                    }
963                }
964                MolecularConstraint::LogPRange { min, max } => {
965                    let logp = self.target_interaction.drug_molecule.logp_approximation();
966                    if logp < *min || logp > *max {
967                        return Err(ApplicationError::ConstraintViolation(format!(
968                            "LogP {logp} outside range [{min}, {max}]"
969                        )));
970                    }
971                }
972                MolecularConstraint::MaxHeavyAtoms(max_atoms) => {
973                    let heavy_atoms = self
974                        .target_interaction
975                        .drug_molecule
976                        .atoms
977                        .iter()
978                        .filter(|atom| atom.atom_type != AtomType::Hydrogen)
979                        .count();
980                    if heavy_atoms > *max_atoms {
981                        return Err(ApplicationError::ConstraintViolation(format!(
982                            "Heavy atom count {heavy_atoms} exceeds maximum {max_atoms}"
983                        )));
984                    }
985                }
986                _ => {
987                    // Other constraints would be validated here
988                }
989            }
990        }
991
992        Ok(())
993    }
994
995    fn to_qubo(&self) -> ApplicationResult<(QuboModel, HashMap<String, usize>)> {
996        // Convert Ising model to QUBO
997        let (ising, variable_map) = self.to_ising_model()?;
998        let qubo = ising.to_qubo();
999        Ok((qubo, variable_map))
1000    }
1001
1002    fn evaluate_solution(
1003        &self,
1004        solution: &Self::Solution,
1005    ) -> ApplicationResult<Self::ObjectiveValue> {
1006        let mut total_score = 0.0;
1007
1008        // Evaluate based on objectives
1009        for objective in &self.objectives {
1010            match objective {
1011                DrugOptimizationObjective::MaximizeAffinity => {
1012                    // Simplified affinity score based on molecular properties
1013                    total_score += solution.drug_likeness_score() * 10.0;
1014                }
1015                DrugOptimizationObjective::MaximizeDrugLikeness => {
1016                    total_score += solution.drug_likeness_score() * 5.0;
1017                }
1018                DrugOptimizationObjective::MinimizeToxicity => {
1019                    // Penalize potentially toxic elements
1020                    let toxic_penalty = solution
1021                        .atoms
1022                        .iter()
1023                        .filter(|atom| {
1024                            matches!(atom.atom_type, AtomType::Bromine | AtomType::Iodine)
1025                        })
1026                        .count() as f64;
1027                    total_score -= toxic_penalty * 2.0;
1028                }
1029                _ => {
1030                    total_score += 1.0; // Default score
1031                }
1032            }
1033        }
1034
1035        Ok(total_score)
1036    }
1037
1038    fn is_feasible(&self, solution: &Self::Solution) -> bool {
1039        // Check basic feasibility constraints
1040        for constraint in &self.constraints {
1041            match constraint {
1042                MolecularConstraint::MolecularWeightRange { min, max } => {
1043                    let mw = solution.molecular_weight();
1044                    if mw < *min || mw > *max {
1045                        return false;
1046                    }
1047                }
1048                MolecularConstraint::LogPRange { min, max } => {
1049                    let logp = solution.logp_approximation();
1050                    if logp < *min || logp > *max {
1051                        return false;
1052                    }
1053                }
1054                MolecularConstraint::MaxHeavyAtoms(max_atoms) => {
1055                    let heavy_atoms = solution
1056                        .atoms
1057                        .iter()
1058                        .filter(|atom| atom.atom_type != AtomType::Hydrogen)
1059                        .count();
1060                    if heavy_atoms > *max_atoms {
1061                        return false;
1062                    }
1063                }
1064                _ => {
1065                    // Other constraints
1066                }
1067            }
1068        }
1069
1070        true
1071    }
1072}
1073
1074impl IndustrySolution for Molecule {
1075    type Problem = DrugDiscoveryProblem;
1076
1077    fn from_binary(problem: &Self::Problem, binary_solution: &[i8]) -> ApplicationResult<Self> {
1078        let solution_i32: Vec<i32> = binary_solution.iter().map(|&x| i32::from(x)).collect();
1079        let variable_map = HashMap::new(); // Simplified
1080        problem.solution_to_molecule(&solution_i32, &variable_map)
1081    }
1082
1083    fn summary(&self) -> HashMap<String, String> {
1084        let mut summary = HashMap::new();
1085        summary.insert("molecule_id".to_string(), self.id.clone());
1086        summary.insert("num_atoms".to_string(), self.atoms.len().to_string());
1087        summary.insert("num_bonds".to_string(), self.bonds.len().to_string());
1088        summary.insert(
1089            "molecular_weight".to_string(),
1090            format!("{:.2}", self.molecular_weight()),
1091        );
1092        summary.insert(
1093            "logp".to_string(),
1094            format!("{:.2}", self.logp_approximation()),
1095        );
1096        summary.insert(
1097            "tpsa".to_string(),
1098            format!("{:.2}", self.tpsa_approximation()),
1099        );
1100        summary.insert(
1101            "rotatable_bonds".to_string(),
1102            self.rotatable_bonds().to_string(),
1103        );
1104        summary.insert(
1105            "drug_likeness".to_string(),
1106            format!("{:.3}", self.drug_likeness_score()),
1107        );
1108
1109        if let Some(ref smiles) = self.smiles {
1110            summary.insert("smiles".to_string(), smiles.clone());
1111        }
1112
1113        summary
1114    }
1115
1116    fn metrics(&self) -> HashMap<String, f64> {
1117        let mut metrics = HashMap::new();
1118        metrics.insert("molecular_weight".to_string(), self.molecular_weight());
1119        metrics.insert("logp".to_string(), self.logp_approximation());
1120        metrics.insert("tpsa".to_string(), self.tpsa_approximation());
1121        metrics.insert("rotatable_bonds".to_string(), self.rotatable_bonds() as f64);
1122        metrics.insert(
1123            "drug_likeness_score".to_string(),
1124            self.drug_likeness_score(),
1125        );
1126
1127        let heavy_atom_count = self
1128            .atoms
1129            .iter()
1130            .filter(|atom| atom.atom_type != AtomType::Hydrogen)
1131            .count() as f64;
1132        metrics.insert("heavy_atom_count".to_string(), heavy_atom_count);
1133
1134        // Add any stored properties
1135        for (key, value) in &self.properties {
1136            metrics.insert(key.clone(), *value);
1137        }
1138
1139        metrics
1140    }
1141
1142    fn export_format(&self) -> ApplicationResult<String> {
1143        let mut output = String::new();
1144
1145        output.push_str("# Drug Discovery Molecular Result\n");
1146        writeln!(output, "Molecule ID: {}", self.id).expect("Writing to String should not fail");
1147        write!(
1148            output,
1149            "Molecular Weight: {:.2} Da\n",
1150            self.molecular_weight()
1151        )
1152        .expect("Writing to String should not fail");
1153        write!(
1154            output,
1155            "LogP (Lipophilicity): {:.2}\n",
1156            self.logp_approximation()
1157        )
1158        .expect("Writing to String should not fail");
1159        write!(output, "TPSA: {:.2} Ų\n", self.tpsa_approximation())
1160            .expect("Writing to String should not fail");
1161        write!(output, "Rotatable Bonds: {}\n", self.rotatable_bonds())
1162            .expect("Writing to String should not fail");
1163        write!(
1164            output,
1165            "Drug-likeness Score: {:.3}\n",
1166            self.drug_likeness_score()
1167        )
1168        .expect("Writing to String should not fail");
1169
1170        if let Some(ref smiles) = self.smiles {
1171            writeln!(output, "SMILES: {smiles}").expect("Writing to String should not fail");
1172        }
1173
1174        output.push_str("\n# Atoms\n");
1175        for (i, atom) in self.atoms.iter().enumerate() {
1176            write!(
1177                output,
1178                "Atom {}: {} ({})",
1179                i,
1180                atom.atom_type.symbol(),
1181                atom.atom_type.atomic_number()
1182            )
1183            .expect("Writing to String should not fail");
1184            if atom.formal_charge != 0 {
1185                write!(output, " charge={}", atom.formal_charge)
1186                    .expect("Writing to String should not fail");
1187            }
1188            if atom.aromatic {
1189                output.push_str(" aromatic");
1190            }
1191            if let Some(coords) = atom.coordinates {
1192                write!(
1193                    output,
1194                    " coords=({:.3}, {:.3}, {:.3})",
1195                    coords[0], coords[1], coords[2]
1196                )
1197                .expect("Writing to String should not fail");
1198            }
1199            output.push('\n');
1200        }
1201
1202        output.push_str("\n# Bonds\n");
1203        for (i, bond) in self.bonds.iter().enumerate() {
1204            write!(
1205                output,
1206                "Bond {}: {} - {} ({:?})",
1207                i, bond.atom1, bond.atom2, bond.bond_type
1208            )
1209            .expect("Writing to String should not fail");
1210            if bond.aromatic {
1211                output.push_str(" aromatic");
1212            }
1213            if bond.in_ring {
1214                output.push_str(" in_ring");
1215            }
1216            output.push('\n');
1217        }
1218
1219        if !self.properties.is_empty() {
1220            output.push_str("\n# Properties\n");
1221            for (key, value) in &self.properties {
1222                writeln!(output, "{key}: {value:.6}").expect("Writing to String should not fail");
1223            }
1224        }
1225
1226        Ok(output)
1227    }
1228}
1229
1230/// Create benchmark drug discovery problems
1231pub fn create_benchmark_problems(
1232    size: usize,
1233) -> ApplicationResult<Vec<Box<dyn OptimizationProblem<Solution = Molecule, ObjectiveValue = f64>>>>
1234{
1235    let mut problems = Vec::new();
1236
1237    // Create molecules of different complexity
1238    let molecule_specs = match size {
1239        s if s <= 10 => {
1240            vec![
1241                (
1242                    "aspirin",
1243                    vec![
1244                        (AtomType::Carbon, 9),
1245                        (AtomType::Hydrogen, 8),
1246                        (AtomType::Oxygen, 4),
1247                    ],
1248                ),
1249                (
1250                    "caffeine",
1251                    vec![
1252                        (AtomType::Carbon, 8),
1253                        (AtomType::Hydrogen, 10),
1254                        (AtomType::Nitrogen, 4),
1255                        (AtomType::Oxygen, 2),
1256                    ],
1257                ),
1258            ]
1259        }
1260        s if s <= 25 => {
1261            vec![
1262                (
1263                    "ibuprofen",
1264                    vec![
1265                        (AtomType::Carbon, 13),
1266                        (AtomType::Hydrogen, 18),
1267                        (AtomType::Oxygen, 2),
1268                    ],
1269                ),
1270                (
1271                    "acetaminophen",
1272                    vec![
1273                        (AtomType::Carbon, 8),
1274                        (AtomType::Hydrogen, 9),
1275                        (AtomType::Nitrogen, 1),
1276                        (AtomType::Oxygen, 2),
1277                    ],
1278                ),
1279                (
1280                    "penicillin",
1281                    vec![
1282                        (AtomType::Carbon, 16),
1283                        (AtomType::Hydrogen, 18),
1284                        (AtomType::Nitrogen, 2),
1285                        (AtomType::Oxygen, 4),
1286                        (AtomType::Sulfur, 1),
1287                    ],
1288                ),
1289            ]
1290        }
1291        _ => {
1292            vec![
1293                (
1294                    "vancomycin",
1295                    vec![
1296                        (AtomType::Carbon, 66),
1297                        (AtomType::Hydrogen, 75),
1298                        (AtomType::Chlorine, 2),
1299                        (AtomType::Nitrogen, 9),
1300                        (AtomType::Oxygen, 24),
1301                    ],
1302                ),
1303                (
1304                    "doxorubicin",
1305                    vec![
1306                        (AtomType::Carbon, 27),
1307                        (AtomType::Hydrogen, 29),
1308                        (AtomType::Nitrogen, 1),
1309                        (AtomType::Oxygen, 11),
1310                    ],
1311                ),
1312            ]
1313        }
1314    };
1315
1316    for (i, (name, atom_composition)) in molecule_specs.iter().enumerate() {
1317        let mut molecule = Molecule::new(format!("benchmark_{name}"));
1318
1319        // Add atoms based on composition
1320        let mut atom_id = 0;
1321        for (atom_type, count) in atom_composition {
1322            for _ in 0..*count {
1323                let atom = Atom::new(atom_id, *atom_type);
1324                molecule.add_atom(atom);
1325                atom_id += 1;
1326            }
1327        }
1328
1329        // Add some basic bonds (simplified)
1330        for j in 0..(molecule.atoms.len() - 1) {
1331            let bond = Bond::new(j, j + 1, BondType::Single);
1332            molecule.add_bond(bond);
1333        }
1334
1335        // Create drug-target interaction
1336        let target_interaction = DrugTargetInteraction {
1337            drug_molecule: molecule,
1338            target_id: format!("target_{i}"),
1339            target_type: TargetType::Enzyme,
1340            binding_affinity: Some(6.5), // Example pIC50
1341            selectivity: HashMap::new(),
1342            admet_properties: AdmetProperties::default(),
1343        };
1344
1345        let mut problem = DrugDiscoveryProblem::new(target_interaction);
1346
1347        // Add different objectives for different problems
1348        match i % 3 {
1349            0 => problem = problem.add_objective(DrugOptimizationObjective::MaximizeAffinity),
1350            1 => problem = problem.add_objective(DrugOptimizationObjective::MaximizeDrugLikeness),
1351            2 => problem = problem.add_objective(DrugOptimizationObjective::MinimizeToxicity),
1352            _ => problem = problem.add_objective(DrugOptimizationObjective::OptimizeAdmet),
1353        }
1354
1355        // Add molecular constraints
1356        problem = problem.add_constraint(MolecularConstraint::MolecularWeightRange {
1357            min: 100.0,
1358            max: 800.0,
1359        });
1360        problem = problem.add_constraint(MolecularConstraint::LogPRange {
1361            min: -2.0,
1362            max: 5.0,
1363        });
1364
1365        // Note: Simplified for trait object compatibility
1366        problems.push(Box::new(problem)
1367            as Box<
1368                dyn OptimizationProblem<Solution = Molecule, ObjectiveValue = f64>,
1369            >);
1370    }
1371
1372    Ok(problems)
1373}
1374
1375#[cfg(test)]
1376mod tests {
1377    use super::*;
1378
1379    #[test]
1380    fn test_atom_creation() {
1381        let atom = Atom::new(0, AtomType::Carbon)
1382            .with_charge(-1)
1383            .with_coordinates([1.0, 2.0, 3.0])
1384            .set_aromatic(true);
1385
1386        assert_eq!(atom.id, 0);
1387        assert_eq!(atom.atom_type, AtomType::Carbon);
1388        assert_eq!(atom.formal_charge, -1);
1389        assert_eq!(atom.coordinates, Some([1.0, 2.0, 3.0]));
1390        assert!(atom.aromatic);
1391    }
1392
1393    #[test]
1394    fn test_molecule_properties() {
1395        let mut molecule = Molecule::new("test".to_string());
1396
1397        // Add carbon and hydrogen atoms
1398        molecule.add_atom(Atom::new(0, AtomType::Carbon));
1399        molecule.add_atom(Atom::new(1, AtomType::Hydrogen));
1400        molecule.add_atom(Atom::new(2, AtomType::Oxygen));
1401
1402        assert_eq!(molecule.atoms.len(), 3);
1403        assert!(molecule.molecular_weight() > 0.0);
1404
1405        let drug_likeness = molecule.drug_likeness_score();
1406        assert!(drug_likeness > 0.0 && drug_likeness <= 1.0);
1407    }
1408
1409    #[test]
1410    fn test_atom_type_properties() {
1411        assert_eq!(AtomType::Carbon.atomic_number(), 6);
1412        assert_eq!(AtomType::Carbon.symbol(), "C");
1413        assert_eq!(AtomType::Carbon.valence(), 4);
1414
1415        assert_eq!(AtomType::Nitrogen.atomic_number(), 7);
1416        assert_eq!(AtomType::Oxygen.valence(), 2);
1417    }
1418
1419    #[test]
1420    fn test_bond_properties() {
1421        let bond = Bond::new(0, 1, BondType::Double)
1422            .set_aromatic(true)
1423            .set_in_ring(true);
1424
1425        assert_eq!(bond.atom1, 0);
1426        assert_eq!(bond.atom2, 1);
1427        assert_eq!(bond.bond_type, BondType::Double);
1428        assert_eq!(bond.bond_type.bond_order(), 2.0);
1429        assert!(bond.aromatic);
1430        assert!(bond.in_ring);
1431    }
1432
1433    #[test]
1434    fn test_molecular_calculations() {
1435        let mut molecule = Molecule::new("ethanol".to_string());
1436
1437        // Create ethanol: C2H6O
1438        molecule.add_atom(Atom::new(0, AtomType::Carbon));
1439        molecule.add_atom(Atom::new(1, AtomType::Carbon));
1440        molecule.add_atom(Atom::new(2, AtomType::Oxygen));
1441        for i in 3..9 {
1442            molecule.add_atom(Atom::new(i, AtomType::Hydrogen));
1443        }
1444
1445        // Add bonds
1446        molecule.add_bond(Bond::new(0, 1, BondType::Single));
1447        molecule.add_bond(Bond::new(1, 2, BondType::Single));
1448
1449        let mw = molecule.molecular_weight();
1450        assert!(mw > 40.0 && mw < 50.0); // Approximately 46 Da
1451
1452        let logp = molecule.logp_approximation();
1453        assert!(logp > -2.0 && logp < 2.0); // Reasonable LogP for ethanol
1454    }
1455
1456    #[test]
1457    fn test_drug_discovery_problem() {
1458        let mut molecule = Molecule::new("test_drug".to_string());
1459        molecule.add_atom(Atom::new(0, AtomType::Carbon));
1460        molecule.add_atom(Atom::new(1, AtomType::Nitrogen));
1461
1462        let target_interaction = DrugTargetInteraction {
1463            drug_molecule: molecule,
1464            target_id: "test_target".to_string(),
1465            target_type: TargetType::Enzyme,
1466            binding_affinity: Some(7.0),
1467            selectivity: HashMap::new(),
1468            admet_properties: AdmetProperties::default(),
1469        };
1470
1471        let problem = DrugDiscoveryProblem::new(target_interaction)
1472            .add_objective(DrugOptimizationObjective::MaximizeAffinity)
1473            .add_constraint(MolecularConstraint::MolecularWeightRange {
1474                min: 20.0,
1475                max: 500.0,
1476            });
1477
1478        assert!(problem.validate().is_ok());
1479
1480        let metrics = problem.size_metrics();
1481        assert_eq!(metrics["num_atoms"], 2);
1482    }
1483
1484    #[test]
1485    fn test_ising_conversion() {
1486        let mut molecule = Molecule::new("test".to_string());
1487        molecule.add_atom(Atom::new(0, AtomType::Carbon));
1488        molecule.add_atom(Atom::new(1, AtomType::Nitrogen));
1489
1490        let target_interaction = DrugTargetInteraction {
1491            drug_molecule: molecule,
1492            target_id: "test".to_string(),
1493            target_type: TargetType::Enzyme,
1494            binding_affinity: None,
1495            selectivity: HashMap::new(),
1496            admet_properties: AdmetProperties::default(),
1497        };
1498
1499        let problem = DrugDiscoveryProblem::new(target_interaction);
1500        let (ising, variable_map) = problem
1501            .to_ising_model()
1502            .expect("should convert to Ising model");
1503
1504        assert_eq!(ising.num_qubits, 16); // 2 atoms * 8 bits each
1505        assert!(!variable_map.is_empty());
1506    }
1507}