carrotcake3 commited on
Commit
29d44c1
·
verified ·
1 Parent(s): ff2c86c

Upload property_predictor.py

Browse files
core/predictors/pure_component/property_predictor.py CHANGED
@@ -0,0 +1,150 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ from .generic import GenericPredictor
3
+ from core.shared_features import featurize_df
4
+ from core.config import EvolutionConfig
5
+ from typing import List, Dict, Optional, Tuple, Callable
6
+ from .hf_models import load_models
7
+ from rdkit import Chem
8
+ from rdkit.Chem import AllChem, DataStructs
9
+
10
+ class PropertyPredictor:
11
+ """Handles batch prediction for all molecular properties."""
12
+
13
+ _cached_paths = None
14
+ _shared: "PropertyPredictor | None" = None
15
+
16
+ @classmethod
17
+ def _get_paths(cls):
18
+ if cls._cached_paths is None:
19
+ cls._cached_paths = load_models()
20
+ return cls._cached_paths
21
+
22
+ @classmethod
23
+ def get_shared(cls) -> "PropertyPredictor":
24
+ """Return a shared config-less instance — reused across all callers."""
25
+ if cls._shared is None:
26
+ cls._shared = cls(config=None)
27
+ return cls._shared
28
+
29
+ def __init__(self, config: EvolutionConfig | None = None):
30
+ self.config = config
31
+
32
+ paths = self._get_paths()
33
+
34
+ self.predictors = {}
35
+ self.predictors['cn'] = GenericPredictor(paths['cn'], 'Cetane Number')
36
+ self.predictors['ysi'] = GenericPredictor(paths['ysi'], 'YSI')
37
+ self.predictors['bp'] = GenericPredictor(paths['bp'], 'Boiling Point')
38
+ self.predictors['density'] = GenericPredictor(paths['density'], 'Density')
39
+ self.predictors['lhv'] = GenericPredictor(paths['lhv'], 'LHV')
40
+ self.predictors['dynamic_viscosity'] = GenericPredictor(paths['dynamic_viscosity'], 'Dynamic Viscosity')
41
+ self._train_fps = None # lazy-loaded on first compute_tanimoto call
42
+
43
+
44
+ if self.config is None:
45
+ self.validators = {}
46
+ else:
47
+ # Define validation rules
48
+ self.validators = {
49
+ 'bp': lambda v: self.config.min_bp <= v <= self.config.max_bp,
50
+ 'density': lambda v: v > self.config.min_density,
51
+ 'lhv': lambda v: v > self.config.min_lhv,
52
+ 'dynamic_viscosity': lambda v: self.config.min_dynamic_viscosity < v <= self.config.max_dynamic_viscosity
53
+ }
54
+ def _init_tanimoto_reference(self):
55
+ """
56
+ Load training-set fingerprints for Tanimoto similarity.
57
+ Used as a proxy for prediction confidence.
58
+ """
59
+ # You MUST replace this with your actual training dataset source
60
+ from core.data_prep import df # or wherever your training df lives
61
+
62
+ train_smiles = df["SMILES"].tolist()
63
+
64
+ self._train_fps = []
65
+ for s in train_smiles:
66
+ mol = Chem.MolFromSmiles(s)
67
+ if mol is not None:
68
+ fp = AllChem.GetMorganFingerprintAsBitVect(
69
+ mol, radius=2, nBits=2048
70
+ )
71
+ self._train_fps.append(fp)
72
+
73
+ def compute_tanimoto(self, smiles_list: List[str]) -> List[Optional[float]]:
74
+ """
75
+ Compute max Tanimoto similarity to training set
76
+ for each SMILES.
77
+ """
78
+ if self._train_fps is None:
79
+ self._init_tanimoto_reference()
80
+ results = []
81
+
82
+ for s in smiles_list:
83
+ mol = Chem.MolFromSmiles(s)
84
+ if mol is None:
85
+ results.append(None)
86
+ continue
87
+
88
+ fp = AllChem.GetMorganFingerprintAsBitVect(
89
+ mol, radius=2, nBits=2048
90
+ )
91
+
92
+ sims = DataStructs.BulkTanimotoSimilarity(fp, self._train_fps)
93
+ results.append(max(sims))
94
+
95
+ return results
96
+
97
+
98
+ def _safe_predict(self, predictions: List) -> List[Optional[float]]:
99
+ """Safely convert predictions, handling None/NaN/inf values."""
100
+ return [
101
+ float(pred) if pred is not None and np.isfinite(pred) else None
102
+ for pred in predictions
103
+ ]
104
+
105
+ def predict_all_properties(self, smiles_list: List[str]) -> Dict[str, List[Optional[float]]]:
106
+ """
107
+ Predict all properties for a batch of SMILES.
108
+ Featurizes ONCE and reuses features for all predictors.
109
+ Returns lists aligned with smiles_list; invalid SMILES yield None for all properties.
110
+ """
111
+ if not smiles_list:
112
+ return {prop: [] for prop in self.predictors.keys()}
113
+
114
+ featurize_result = featurize_df(smiles_list, return_df=True)
115
+
116
+ if featurize_result is None or featurize_result[0] is None:
117
+ return {prop: [None] * len(smiles_list) for prop in self.predictors.keys()}
118
+
119
+ X_full, valid_df = featurize_result
120
+ # valid_df["SMILES"] holds the successfully featurized SMILES in order
121
+ valid_smiles = valid_df["SMILES"].tolist()
122
+ # Map each valid SMILES to its position in the compact predictions array
123
+ smiles_to_pred_idx = {smi: i for i, smi in enumerate(valid_smiles)}
124
+
125
+ # Predict on compact (valid-only) feature matrix
126
+ compact = {}
127
+ for prop_name, predictor in self.predictors.items():
128
+ compact[prop_name] = self._safe_predict(predictor.predict_from_features(X_full))
129
+
130
+ # Expand back to full length, inserting None for molecules that failed featurization
131
+ results = {}
132
+ for prop_name, preds in compact.items():
133
+ full = [None] * len(smiles_list)
134
+ for orig_i, smi in enumerate(smiles_list):
135
+ pred_i = smiles_to_pred_idx.get(smi)
136
+ if pred_i is not None:
137
+ full[orig_i] = preds[pred_i]
138
+ results[prop_name] = full
139
+
140
+ return results
141
+
142
+ def is_valid(self, name, value):
143
+ if value is None or name not in self.config.filters:
144
+ return True
145
+ lo, hi = self.config.filters[name]
146
+ if lo is not None and value < lo:
147
+ return False
148
+ if hi is not None and value > hi:
149
+ return False
150
+ return True