Skip to content

Commit f0385c3

Browse files
Merge pull request #18 from coleygroup/pharm-prioritization
Conditional evaluation pipeline to support pharmacophore prioritization scoring
2 parents 10602e5 + ffcf9b7 commit f0385c3

4 files changed

Lines changed: 113 additions & 20 deletions

File tree

shepherd_score/conformer_generation.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from pathlib import Path
1414
from tqdm import tqdm
1515
import uuid
16-
from typing import Optional, List
16+
from typing import Optional, List, Union
1717
import contextlib
1818
import multiprocessing
1919

@@ -308,7 +308,7 @@ def optimize_conformer_with_xtb(conformer: Chem.Mol,
308308
solvent: Optional[str] = None,
309309
num_cores: int = 1,
310310
charge: int = 0,
311-
temp_dir: str | Path = TMPDIR):
311+
temp_dir: Union[str, Path] = TMPDIR):
312312
"""
313313
Use external calls to GFN2-XTB (command line) to optimize a conformer geometry.
314314
@@ -395,7 +395,7 @@ def optimize_conformer_with_xtb_from_xyz_block(xyz_block: str,
395395
solvent: Optional[str] = None,
396396
num_cores: int = 1,
397397
charge: int = 0,
398-
temp_dir: str | Path = TMPDIR):
398+
temp_dir: Union[str, Path] = TMPDIR):
399399
"""
400400
Use external calls to GFN2-XTB (command line) to optimize coordinates from an xyz block.
401401
@@ -483,7 +483,7 @@ def charges_from_single_point_conformer_with_xtb(conformer: Chem.Mol,
483483
solvent: Optional[str] = None,
484484
num_cores: int = 1,
485485
charge: int = 0,
486-
temp_dir: str | Path = TMPDIR
486+
temp_dir: Union[str, Path] = TMPDIR
487487
):
488488
"""
489489
Compute atomic partial charges from a single point xTB calculation of a provided conformer.
@@ -558,7 +558,7 @@ def single_point_xtb_from_xyz(xyz_block: str,
558558
solvent: Optional[str] = None,
559559
num_cores: int = 1,
560560
charge: int = 0,
561-
temp_dir: str | Path = TMPDIR):
561+
temp_dir: Union[str, Path] = TMPDIR):
562562
"""
563563
Compute energy and atomic partial charges from a single point xTB calculation.
564564
@@ -648,7 +648,7 @@ def optimize_conformer_ensemble_with_xtb(conformers: List[Chem.Mol],
648648
num_processes: int = 1,
649649
num_workers: int = 1,
650650
charge: int = 0,
651-
temp_dir: str | Path = TMPDIR,
651+
temp_dir: Union[str, Path] = TMPDIR,
652652
verbose: bool = False):
653653
"""
654654
GFN2-XTB geometry optimization for a list of conformers.
@@ -735,7 +735,7 @@ def generate_opt_conformers_xtb(smiles: str,
735735
MMFF_optimize: bool = True,
736736
num_processes: int = 1,
737737
num_workers: int = 1,
738-
temp_dir: str | Path = TMPDIR,
738+
temp_dir: Union[str, Path] = TMPDIR,
739739
verbose: bool = False,
740740
num_confs: int = 1000):
741741
"""

shepherd_score/evaluations/evaluate/_pipeline_eval_single.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ def _create_failed_result(i: int, error_msg: str) -> Dict[str, Any]:
5353
}
5454

5555

56-
def _create_conditional_failed_result(i: int, error_msg: str) -> Dict[str, Any]:
56+
def _create_conditional_failed_result(i: int, error_msg: str, priority_pharm_indices: Optional[list] = None) -> Dict[str, Any]:
5757
"""Create a result dict for failed conditional evaluations with all required fields."""
5858
base_failed_results = _create_failed_result(i, error_msg)
59-
return {
59+
res = {
6060
**base_failed_results,
6161
# Additional Conditional 3D similarity attributes
6262
'sim_surf_target': np.nan,
@@ -71,6 +71,12 @@ def _create_conditional_failed_result(i: int, error_msg: str) -> Dict[str, Any]:
7171
'sim_surf_target_relax_esp_aligned': np.nan,
7272
'sim_pharm_target_relax_esp_aligned': np.nan,
7373
}
74+
if priority_pharm_indices is not None:
75+
res.update({
76+
'sim_pharm_priority_target_relax_optimal': np.nan,
77+
'sim_pharm_nonpriority_target_relax_optimal': np.nan,
78+
})
79+
return res
7480

7581

7682
def _create_consistency_failed_result(i: int, error_msg: str) -> Dict[str, Any]:
@@ -310,7 +316,8 @@ def _eval_conditional_single(i: int,
310316
atoms: np.ndarray,
311317
positions: np.ndarray,
312318
solvent: Optional[str],
313-
num_processes: int) -> Dict[str, Any]:
319+
num_processes: int,
320+
priority_pharm_indices: Optional[list] = None) -> Dict[str, Any]:
314321
"""
315322
Evaluate a single molecule and preserve necessary attributes for the pipeline while avoiding
316323
pickling issues.
@@ -331,8 +338,9 @@ def _eval_conditional_single(i: int,
331338
condition=condition,
332339
num_surf_points=num_surf_points,
333340
pharm_multi_vector=pharm_multi_vector,
341+
priority_pharm_indices=priority_pharm_indices,
334342
num_processes=num_processes,
335-
solvent=solvent
343+
solvent=solvent,
336344
)
337345

338346
res = {
@@ -368,12 +376,17 @@ def _eval_conditional_single(i: int,
368376
'sim_pharm_target_relax_esp_aligned': cond_eval.sim_pharm_target_relax_esp_aligned if cond_eval.sim_pharm_target_relax_esp_aligned is not None else np.nan,
369377
'error': None
370378
}
379+
if priority_pharm_indices is not None:
380+
res.update({
381+
'sim_pharm_priority_target_relax_optimal': cond_eval.sim_pharm_priority_target_relax_optimal if cond_eval.sim_pharm_priority_target_relax_optimal is not None else np.nan,
382+
'sim_pharm_nonpriority_target_relax_optimal': cond_eval.sim_pharm_nonpriority_target_relax_optimal if cond_eval.sim_pharm_nonpriority_target_relax_optimal is not None else np.nan,
383+
})
371384
return res
372385

373386
except Exception as e:
374387
error_msg = f"Conditional evaluation failed for molecule {i}: {str(e)}"
375388
logger.error(f"{error_msg}\n{traceback.format_exc()}")
376-
return _create_conditional_failed_result(i, error_msg)
389+
return _create_conditional_failed_result(i, error_msg, priority_pharm_indices)
377390

378391

379392
def _eval_consistency_single(i: int,

shepherd_score/evaluations/evaluate/evals.py

Lines changed: 64 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -473,20 +473,25 @@ def _align_with_esp(self, mp_ref_and_relaxed: MoleculePair) -> float:
473473

474474
def _align_with_pharm(self, mp_ref_and_relaxed: MoleculePair) -> float:
475475
"""
476-
Align relaxed molecule to reference/target molecule with pharmacophores
476+
Align relaxed molecule to reference/target molecule with pharmacophores.
477+
478+
Stores aligned fit anchors and vectors on ``self._aligned_pharm_ancs``
479+
and ``self._aligned_pharm_vecs`` for downstream subset scoring.
477480
478481
Returns
479482
-------
480483
float : Pharmacophore similarity score of optimally aligned molecule.
481484
"""
482-
aligned_fit_anchors, aligned_vectors = mp_ref_and_relaxed.align_with_pharm(
485+
aligned_fit_anchors, aligned_fit_vectors = mp_ref_and_relaxed.align_with_pharm(
483486
similarity='tanimoto',
484487
extended_points=False,
485488
only_extended=False,
486489
num_repeats=1,
487490
trans_init=False,
488491
use_jax=False
489492
)
493+
self._aligned_pharm_ancs = aligned_fit_anchors
494+
self._aligned_pharm_vecs = aligned_fit_vectors
490495
pharm_similarity = mp_ref_and_relaxed.sim_aligned_pharm
491496
return float(pharm_similarity)
492497

@@ -501,8 +506,10 @@ def __init__(self,
501506
condition: str,
502507
num_surf_points: int = 400,
503508
pharm_multi_vector: Optional[bool] = None,
509+
priority_pharm_indices: Optional[list] = None,
504510
solvent: Optional[str] = None,
505-
num_processes: int = 1):
511+
num_processes: int = 1,
512+
):
506513
"""
507514
Evaluation pipeline for conditionally-generated molecules.
508515
@@ -536,6 +543,15 @@ def __init__(self,
536543
Number of surface points to sample for similarity scoring. Default is 400.
537544
pharm_multi_vector : bool, optional
538545
Use multiple vectors to represent Aro/HBA/HBD or single.
546+
priority_pharm_indices : list of int, optional
547+
Indices (into ``ref_molec`` pharmacophore arrays) of "priority"
548+
pharmacophores. When provided, two additional Tversky
549+
(``'tversky_ref'``) scores are computed after the full-set pharm
550+
alignment: one for the priority subset and one for the non-priority
551+
complement subset, each scored against the full pharmacophore set
552+
of the aligned generated molecule. Requires ``condition`` to be
553+
``'pharm'`` or ``'all'`` and ``pharm_multi_vector`` to be a bool.
554+
Must satisfy ``0 < len(priority_pharm_indices) < N_pharm``.
539555
solvent : str, optional
540556
Solvent type for xTB relaxation.
541557
num_processes : int, optional
@@ -574,6 +590,11 @@ def __init__(self,
574590
self.sim_surf_target_relax_esp_aligned = None
575591
self.sim_pharm_target_relax_esp_aligned = None
576592

593+
self.sim_pharm_priority_target_relax_optimal = None
594+
self.sim_pharm_nonpriority_target_relax_optimal = None
595+
596+
self.priority_pharm_indices = priority_pharm_indices
597+
577598
# Scoring parameters
578599
self.num_surf_points = num_surf_points
579600
self.alpha = ALPHA(self.num_surf_points) # Fitted to probe_radius=1.2
@@ -674,6 +695,39 @@ def __init__(self,
674695
if (self.condition == 'pharm' or self.condition == 'all') and isinstance(pharm_multi_vector, bool):
675696
self.sim_pharm_target_relax_optimal = self._align_with_pharm(mp_ref_and_relaxed=mp_ref_and_relaxed)
676697

698+
# Priority subset Tversky scoring using the alignment from the full-set pharm alignment above
699+
if (self.priority_pharm_indices is not None
700+
and hasattr(self, '_aligned_pharm_ancs')
701+
and self.ref_molec.pharm_ancs is not None):
702+
n_pharm = len(self.ref_molec.pharm_types)
703+
nonpriority_indices = sorted(set(range(n_pharm)) - set(self.priority_pharm_indices))
704+
priority_idx = self.priority_pharm_indices
705+
706+
self.sim_pharm_priority_target_relax_optimal = float(get_overlap_pharm_np(
707+
ptype_1=self.ref_molec.pharm_types[priority_idx],
708+
ptype_2=self.molec_post_opt.pharm_types,
709+
anchors_1=self.ref_molec.pharm_ancs[priority_idx],
710+
anchors_2=self._aligned_pharm_ancs,
711+
vectors_1=self.ref_molec.pharm_vecs[priority_idx],
712+
vectors_2=self._aligned_pharm_vecs,
713+
similarity='tversky_ref',
714+
extended_points=False,
715+
only_extended=False
716+
))
717+
718+
if nonpriority_indices:
719+
self.sim_pharm_nonpriority_target_relax_optimal = float(get_overlap_pharm_np(
720+
ptype_1=self.ref_molec.pharm_types[nonpriority_indices],
721+
ptype_2=self.molec_post_opt.pharm_types,
722+
anchors_1=self.ref_molec.pharm_ancs[nonpriority_indices],
723+
anchors_2=self._aligned_pharm_ancs,
724+
vectors_1=self.ref_molec.pharm_vecs[nonpriority_indices],
725+
vectors_2=self._aligned_pharm_vecs,
726+
similarity='tversky_ref',
727+
extended_points=False,
728+
only_extended=False
729+
))
730+
677731
# Compute ESP-aligned surf and pharmacophore similarity scores
678732
if mp_ref_and_relaxed.transform_esp is not None and self.condition in ('esp', 'all'):
679733
molec_post_opt_esp_aligned = mp_ref_and_relaxed.get_transformed_molecule(mp_ref_and_relaxed.transform_esp)
@@ -733,19 +787,24 @@ def _align_with_esp(self, mp_ref_and_relaxed: MoleculePair) -> float:
733787

734788
def _align_with_pharm(self, mp_ref_and_relaxed: MoleculePair) -> float:
735789
"""
736-
Align relaxed molecule to reference/target molecule with pharmacophores
790+
Align relaxed molecule to reference/target molecule with pharmacophores.
791+
792+
Stores aligned fit anchors and vectors on ``self._aligned_pharm_ancs``
793+
and ``self._aligned_pharm_vecs`` for downstream subset scoring.
737794
738795
Returns
739796
-------
740797
float : Pharmacophore similarity score of optimally aligned molecule.
741798
"""
742-
aligned_fit_anchors, aligned_vectors = mp_ref_and_relaxed.align_with_pharm(
799+
aligned_fit_anchors, aligned_fit_vectors = mp_ref_and_relaxed.align_with_pharm(
743800
similarity='tanimoto',
744801
extended_points=False,
745802
only_extended=False,
746803
num_repeats=1,
747804
trans_init=False,
748805
use_jax=False
749806
)
807+
self._aligned_pharm_ancs = aligned_fit_anchors
808+
self._aligned_pharm_vecs = aligned_fit_vectors
750809
pharm_similarity = mp_ref_and_relaxed.sim_aligned_pharm
751810
return float(pharm_similarity)

shepherd_score/evaluations/evaluate/pipelines.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,6 +387,7 @@ def __init__(self,
387387
num_surf_points: int = 400,
388388
pharm_multi_vector: Optional[bool] = None,
389389
solvent: Optional[str] = None,
390+
priority_pharm_indices: Optional[list] = None,
390391
):
391392
"""
392393
Initialize attributes for conditional evaluation pipeline.
@@ -412,10 +413,21 @@ def __init__(self,
412413
ref_molec should match.
413414
solvent : str, optional
414415
Solvent type for xtb relaxation.
416+
priority_pharm_indices : list of int, optional
417+
Indices (into ``ref_molec`` pharmacophore arrays) of "priority"
418+
pharmacophores. When provided, two additional Tversky
419+
(``'tversky_ref'``) scores are computed after the full-set pharm
420+
alignment: one for the priority subset
421+
(``sims_pharm_priority_target_relax_optimal``) and one for the
422+
non-priority complement subset
423+
(``sims_pharm_nonpriority_target_relax_optimal``). Requires
424+
``condition`` to be ``'pharm'`` or ``'all'`` and
425+
``pharm_multi_vector`` to be a bool.
415426
"""
416427
self.generated_mols = generated_mols
417428
self.num_generated_mols = len(self.generated_mols)
418429
self.solvent = solvent
430+
self.priority_pharm_indices = priority_pharm_indices
419431

420432
self.pharm_multi_vector = pharm_multi_vector
421433
self.condition = condition
@@ -487,6 +499,10 @@ def __init__(self,
487499
self.sims_surf_target_relax_esp_aligned = np.empty(self.num_generated_mols)
488500
self.sims_pharm_target_relax_esp_aligned = np.empty(self.num_generated_mols)
489501

502+
if self.priority_pharm_indices is not None:
503+
self.sims_pharm_priority_target_relax_optimal = np.empty(self.num_generated_mols)
504+
self.sims_pharm_nonpriority_target_relax_optimal = np.empty(self.num_generated_mols)
505+
490506
# 2D similarities
491507
self.graph_similarities = np.empty(self.num_generated_mols)
492508
self.graph_similarities_post_opt = np.empty(self.num_generated_mols)
@@ -532,7 +548,7 @@ def evaluate(self,
532548
if num_workers > 1:
533549
multiprocessing.set_start_method(mp_context, force=True)
534550
with set_thread_limits(num_processes):
535-
inputs = [(i, self.ref_molec, self.condition, self.num_surf_points, self.pharm_multi_vector, atoms, positions, self.solvent, 1)
551+
inputs = [(i, self.ref_molec, self.condition, self.num_surf_points, self.pharm_multi_vector, atoms, positions, self.solvent, 1, self.priority_pharm_indices)
536552
for i, (atoms, positions) in enumerate(self.generated_mols)]
537553
with multiprocessing.Pool(num_workers) as pool:
538554
if verbose:
@@ -574,7 +590,8 @@ def evaluate(self,
574590

575591
res = _eval_conditional_single(
576592
i, self.ref_molec, self.condition, self.num_surf_points,
577-
self.pharm_multi_vector, atoms, positions, self.solvent, num_processes
593+
self.pharm_multi_vector, atoms, positions, self.solvent, num_processes,
594+
self.priority_pharm_indices
578595
)
579596
self._process_single_result(res, i)
580597

@@ -658,6 +675,10 @@ def _process_single_result(self, res: Dict[str, Any], i: int):
658675
self.sims_surf_target_relax_esp_aligned[i] = res['sim_surf_target_relax_esp_aligned']
659676
self.sims_pharm_target_relax_esp_aligned[i] = res['sim_pharm_target_relax_esp_aligned']
660677

678+
if self.priority_pharm_indices is not None:
679+
self.sims_pharm_priority_target_relax_optimal[i] = res['sim_pharm_priority_target_relax_optimal']
680+
self.sims_pharm_nonpriority_target_relax_optimal[i] = res['sim_pharm_nonpriority_target_relax_optimal']
681+
661682

662683
def resampling_surf_scores(self) -> Union[np.ndarray, None]:
663684
"""
@@ -767,7 +788,7 @@ def to_pandas(self) -> Tuple[pd.Series, pd.DataFrame]:
767788
for key, value in self.__dict__.items():
768789
if key in ('smiles', 'smiles_post_opt', 'morgan_fps', 'morgan_fps_post_opt', 'ref_molec'):
769790
continue
770-
elif key in ('ref_surf_resampling_scores', 'ref_surf_esp_resampling_scores'):
791+
elif key in ('ref_surf_resampling_scores', 'ref_surf_esp_resampling_scores', 'priority_pharm_indices'):
771792
global_attrs[key] = value
772793

773794
elif isinstance(value, (list, tuple, np.ndarray)) and not (isinstance(value, np.ndarray) and value.ndim == 0):

0 commit comments

Comments
 (0)