1#![allow(dead_code)]
8
9use scirs2_core::ndarray::Array2;
10use std::collections::{HashMap, HashSet};
11
12pub struct MolecularDesignOptimizer {
14 target_properties: TargetProperties,
16 fragment_library: FragmentLibrary,
18 scoring_function: ScoringFunction,
20 constraints: DesignConstraints,
22 strategy: OptimizationStrategy,
24}
25
26#[derive(Debug, Clone)]
27pub struct TargetProperties {
28 pub molecular_weight: Option<(f64, f64)>, pub logp: Option<(f64, f64)>,
32 pub logs: Option<(f64, f64)>,
34 pub hbd: Option<(usize, usize)>,
36 pub hba: Option<(usize, usize)>,
38 pub rotatable_bonds: Option<(usize, usize)>,
40 pub tpsa: Option<(f64, f64)>,
42 pub custom_descriptors: HashMap<String, (f64, f64)>,
44}
45
46#[derive(Debug, Clone)]
47pub struct FragmentLibrary {
48 pub fragments: Vec<MolecularFragment>,
50 pub connection_rules: ConnectionRules,
52 pub fragment_scores: HashMap<usize, f64>,
54 pub privileged_scaffolds: Vec<usize>,
56}
57
58#[derive(Debug, Clone)]
59pub struct MolecularFragment {
60 pub id: usize,
62 pub smiles: String,
64 pub attachment_points: Vec<AttachmentPoint>,
66 pub properties: FragmentProperties,
68 pub pharmacophores: Vec<PharmacophoreFeature>,
70}
71
72#[derive(Debug, Clone)]
73pub struct AttachmentPoint {
74 pub atom_idx: usize,
76 pub bond_types: Vec<BondType>,
78 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 pub mw_contribution: f64,
101 pub logp_contribution: f64,
103 pub hbd_count: usize,
105 pub hba_count: usize,
107 pub rotatable_count: usize,
109 pub tpsa_contribution: f64,
111}
112
113#[derive(Debug, Clone)]
114pub struct PharmacophoreFeature {
115 pub feature_type: PharmacophoreType,
117 pub position: Vec3D,
119 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 pub compatible_pairs: HashMap<(usize, usize), f64>,
138 pub forbidden_connections: HashSet<(usize, usize)>,
140 pub reaction_templates: Vec<ReactionTemplate>,
142}
143
144#[derive(Debug, Clone)]
145pub struct ReactionTemplate {
146 pub name: String,
148 pub reactants: Vec<FunctionalGroup>,
150 pub product_pattern: String,
152 pub feasibility: f64,
154}
155
156#[derive(Debug, Clone)]
157pub struct FunctionalGroup {
158 pub smarts: String,
160 pub count: usize,
162}
163
164#[derive(Debug, Clone)]
165pub enum ScoringFunction {
166 Additive { weights: HashMap<String, f64> },
168 MLBased { model_path: String },
170 DockingBased { receptor: ProteinStructure },
172 MultiObjective { objectives: Vec<ObjectiveFunction> },
174 PharmacophoreMatching { reference: PharmacophoreModel },
176}
177
178#[derive(Debug, Clone)]
179pub struct ProteinStructure {
180 pub pdb_id: String,
182 pub active_site: Vec<usize>,
184 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 pub features: Vec<PharmacophoreFeature>,
199 pub distance_constraints: Vec<DistanceConstraint>,
201 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 BindingAffinity { weight: f64 },
226 SyntheticAccessibility { weight: f64 },
228 ADMET {
230 property: ADMETProperty,
231 weight: f64,
232 },
233 Novelty {
235 reference_set: Vec<String>,
236 weight: f64,
237 },
238 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 pub max_mw: f64,
258 pub lipinski: bool,
260 pub veber: bool,
262 pub pains_filter: bool,
264 pub max_sa_score: f64,
266 pub min_qed: f64,
268 pub smarts_filters: Vec<String>,
270}
271
272#[derive(Debug, Clone)]
273pub enum OptimizationStrategy {
274 FragmentGrowing { core: MolecularFragment },
276 FragmentLinking { fragments: Vec<MolecularFragment> },
278 FragmentHopping { scaffold: MolecularFragment },
280 DeNovo,
282 LeadOptimization { lead: String },
284}
285
286impl MolecularDesignOptimizer {
287 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 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 pub fn with_scoring(mut self, scoring: ScoringFunction) -> Self {
315 self.scoring_function = scoring;
316 self
317 }
318
319 pub fn with_constraints(mut self, constraints: DesignConstraints) -> Self {
321 self.constraints = constraints;
322 self
323 }
324
325 pub fn with_strategy(mut self, strategy: OptimizationStrategy) -> Self {
327 self.strategy = strategy;
328 self
329 }
330
331 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 fn build_fragment_growing_qubo(
344 &self,
345 core: &MolecularFragment,
346 ) -> Result<(Array2<f64>, HashMap<String, usize>), String> {
347 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 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 self.add_fragment_scores(&mut qubo, &var_map, core)?;
365
366 self.add_property_constraints(&mut qubo, &var_map, core)?;
368
369 self.add_connection_compatibility(&mut qubo, &var_map, core)?;
371
372 self.add_uniqueness_constraints(&mut qubo, &var_map, positions, fragments)?;
374
375 Ok((qubo, var_map))
376 }
377
378 fn build_de_novo_qubo(&self) -> Result<(Array2<f64>, HashMap<String, usize>), String> {
380 let max_positions = 10; let fragments = self.fragment_library.fragments.len();
383
384 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 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 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 self.add_de_novo_objective(&mut qubo, &var_map, max_positions)?;
414
415 self.add_connectivity_constraints(&mut qubo, &var_map, max_positions)?;
417
418 self.add_global_property_constraints(&mut qubo, &var_map, max_positions)?;
420
421 Ok((qubo, var_map))
422 }
423
424 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 let score = self
439 .fragment_library
440 .fragment_scores
441 .get(&f)
442 .unwrap_or(&0.0);
443
444 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 fn compute_attachment_compatibility(
460 &self,
461 attachment: &AttachmentPoint,
462 fragment: &MolecularFragment,
463 ) -> f64 {
464 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 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 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 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 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 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 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 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 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 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 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 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 for p in 0..positions {
634 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 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 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 if self.fragment_library.privileged_scaffolds.contains(&f) {
672 qubo[[var_idx, var_idx]] -= 2.0;
673 }
674 }
675 }
676 }
677
678 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 qubo[[conn_idx, conn_idx]] += 0.1;
685 }
686 }
687 }
688
689 Ok(())
690 }
691
692 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 for i in 0..max_positions {
703 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 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 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 self.add_connection_compatibility_de_novo(qubo, var_map, max_positions)?;
731
732 Ok(())
733 }
734
735 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 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 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 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 let penalty = 10.0;
787
788 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 if mw > max_mw {
800 qubo[[var_idx, var_idx]] += penalty * 10.0;
801 }
802
803 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 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 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 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, })
874 }
875
876 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 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 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 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 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 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 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 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
1023pub struct LeadOptimizer {
1025 lead_compound: String,
1027 objectives: Vec<OptimizationObjective>,
1029 modifications: AllowedModifications,
1031 admet_predictor: ADMETPredictor,
1033}
1034
1035#[derive(Debug, Clone)]
1036pub enum OptimizationObjective {
1037 Potency { target_ic50: f64 },
1039 Selectivity { off_targets: Vec<String> },
1041 ADMET { properties: Vec<ADMETProperty> },
1043 ReduceMW { target_mw: f64 },
1045 Solubility { target_logs: f64 },
1047}
1048
1049#[derive(Debug, Clone)]
1050pub struct AllowedModifications {
1051 pub bioisosteres: HashMap<String, Vec<String>>,
1053 pub r_groups: Vec<RGroupModification>,
1055 pub scaffold_hopping: bool,
1057 pub max_similarity: f64,
1059}
1060
1061#[derive(Debug, Clone)]
1062pub struct RGroupModification {
1063 pub position: String,
1065 pub substituents: Vec<String>,
1067 pub preferred_properties: HashMap<String, f64>,
1069}
1070
1071#[derive(Debug, Clone)]
1072pub struct ADMETPredictor {
1073 pub models: HashMap<ADMETProperty, PredictionModel>,
1075 pub experimental_data: HashMap<String, ADMETProfile>,
1077}
1078
1079#[derive(Debug, Clone)]
1080pub struct PredictionModel {
1081 pub model_type: ModelType,
1083 pub parameters: Vec<f64>,
1085 pub accuracy: f64,
1087}
1088
1089#[derive(Debug, Clone)]
1090pub enum ModelType {
1091 RandomForest,
1093 NeuralNetwork,
1095 SVM,
1097 PhysicsBased,
1099}
1100
1101#[derive(Debug, Clone)]
1102pub struct ADMETProfile {
1103 pub absorption: AbsorptionProfile,
1105 pub distribution: DistributionProfile,
1107 pub metabolism: MetabolismProfile,
1109 pub excretion: ExcretionProfile,
1111 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, 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
1155pub struct VirtualScreeningEngine {
1157 library: CompoundLibrary,
1159 protocol: ScreeningProtocol,
1161 hit_criteria: HitSelectionCriteria,
1163}
1164
1165#[derive(Debug, Clone)]
1166pub struct CompoundLibrary {
1167 pub source: LibrarySource,
1169 pub size: usize,
1171 pub diversity: DiversityMetrics,
1173 pub filters: Vec<LibraryFilter>,
1175}
1176
1177#[derive(Debug, Clone)]
1178pub enum LibrarySource {
1179 Commercial { vendor: String },
1181 FDAApproved,
1183 NaturalProducts,
1185 Fragments,
1187 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 MolecularWeight { min: f64, max: f64 },
1202 Lipinski,
1204 PAINS,
1206 SMARTS { pattern: String, exclude: bool },
1208}
1209
1210#[derive(Debug, Clone)]
1211pub struct EnumerationRule {
1212 pub scaffold: String,
1214 pub variation_points: Vec<VariationPoint>,
1216 pub strategy: EnumerationStrategy,
1218}
1219
1220#[derive(Debug, Clone)]
1221pub struct VariationPoint {
1222 pub position: String,
1224 pub building_blocks: Vec<String>,
1226}
1227
1228#[derive(Debug, Clone)]
1229pub enum EnumerationStrategy {
1230 Exhaustive,
1232 RandomSampling { size: usize },
1234 Focused { criteria: Vec<String> },
1236}
1237
1238#[derive(Debug, Clone)]
1239pub enum ScreeningProtocol {
1240 StructureBased {
1242 receptor: ProteinStructure,
1243 docking_program: DockingProgram,
1244 scoring_function: String,
1245 },
1246 LigandBased {
1248 reference_ligands: Vec<String>,
1249 similarity_metric: SimilarityMetric,
1250 threshold: f64,
1251 },
1252 PharmacaophoreBased {
1254 pharmacophore: PharmacophoreModel,
1255 tolerance: f64,
1256 },
1257 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 pub score_threshold: f64,
1285 pub top_n: Option<usize>,
1287 pub diversity_selection: bool,
1289 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}