Spaces:
Running on Zero
Running on Zero
Download src/gem/analysis/rdkit_functions.py from hugging-apps/graph-energy-matching: direct link, hf CLI and curl.
- Browser
- Download file 16.4 kB
-
https://huggingface.co/spaces/hugging-apps/graph-energy-matching/resolve/main/src/gem/analysis/rdkit_functions.py
- Command line
-
hf download hf://spaces/hugging-apps/graph-energy-matching/src/gem/analysis/rdkit_functions.py
-
curl -L -o rdkit_functions.py https://huggingface.co/spaces/hugging-apps/graph-energy-matching/resolve/main/src/gem/analysis/rdkit_functions.py
16.4 kB
| import numpy as np | |
| import torch | |
| import re | |
| import warnings | |
| import pandas as pd | |
| from tqdm import tqdm | |
| from rdkit import Chem | |
| allowed_bonds = { | |
| "H": 1, | |
| "C": 4, | |
| "N": 3, | |
| "O": 2, | |
| "F": 1, | |
| "B": 3, | |
| "Al": 3, | |
| "Si": 4, | |
| "P": [3, 5], | |
| "S": 4, | |
| "Cl": 1, | |
| "As": 3, | |
| "Br": 1, | |
| "I": 1, | |
| "Hg": [1, 2], | |
| "Bi": [3, 5], | |
| "Se": [2, 4, 6], | |
| } | |
| bond_dict = [ | |
| None, | |
| Chem.rdchem.BondType.SINGLE, | |
| Chem.rdchem.BondType.DOUBLE, | |
| Chem.rdchem.BondType.TRIPLE, | |
| Chem.rdchem.BondType.AROMATIC, | |
| ] | |
| ATOM_VALENCY = {6: 4, 7: 3, 8: 2, 9: 1, 15: 3, 16: 2, 17: 1, 35: 1, 53: 1} | |
| class BasicMolecularMetrics(object): | |
| def __init__(self, dataset_info, train_smiles=None, args=None): | |
| self.atom_decoder = dataset_info.atom_decoder | |
| self.dataset_info = dataset_info | |
| self.args = args | |
| default_mode = "vfm_relaxed" | |
| validity_mode = default_mode | |
| if args is not None: | |
| try: | |
| validity_mode = str( | |
| getattr(getattr(args, "general", object()), "validity_mode", default_mode) | |
| ).lower() | |
| except AttributeError: | |
| validity_mode = default_mode | |
| if validity_mode not in {"vfm_relaxed", "strict"}: | |
| warnings.warn( | |
| f"Unknown validity_mode='{validity_mode}', falling back to '{default_mode}'.", | |
| stacklevel=2, | |
| ) | |
| validity_mode = default_mode | |
| self.validity_mode = validity_mode | |
| self.validity_labels = { | |
| "vfm_relaxed": "validity_VFM_relaxed", | |
| "strict": "validity_strict", | |
| } | |
| self.primary_validity_label = self.validity_labels[self.validity_mode] | |
| self._secondary_mode = "strict" if self.validity_mode == "vfm_relaxed" else "vfm_relaxed" | |
| self.secondary_validity_label = self.validity_labels[self._secondary_mode] | |
| self.latest_validity_values = {"strict": None, "vfm_relaxed": None} | |
| self.latest_validity_labels = { | |
| "primary": self.primary_validity_label, | |
| "secondary": self.secondary_validity_label, | |
| } | |
| self.dataset_smiles_list = train_smiles | |
| def compute_validity(self, generated): | |
| """generated: list of couples (positions, atom_types)""" | |
| valid = [] | |
| num_components = [] | |
| all_smiles = [] | |
| all_smiles_without_test = [] | |
| for graph in tqdm( | |
| generated, desc="Generated molecules validity check progress" | |
| ): | |
| atom_types, edge_types = graph | |
| mol = build_molecule(atom_types, edge_types, self.dataset_info.atom_decoder) | |
| smiles = mol2smiles(mol) | |
| all_smiles_without_test.append(mol2smilesWithNoSanitize(mol)) | |
| try: | |
| mol_frags = Chem.rdmolops.GetMolFrags( | |
| mol, asMols=True, sanitizeFrags=True | |
| ) | |
| num_components.append(len(mol_frags)) | |
| except Exception: | |
| pass | |
| if smiles is not None: | |
| try: | |
| mol_frags = Chem.rdmolops.GetMolFrags( | |
| mol, asMols=True, sanitizeFrags=True | |
| ) | |
| largest_mol = max( | |
| mol_frags, default=mol, key=lambda m: m.GetNumAtoms() | |
| ) | |
| smiles = mol2smiles(largest_mol) | |
| valid.append(smiles) | |
| all_smiles.append(smiles) | |
| except Chem.rdchem.AtomValenceException: | |
| print("Valence error in GetmolFrags") | |
| all_smiles.append(None) | |
| except Chem.rdchem.KekulizeException: | |
| print("Can't kekulize molecule") | |
| all_smiles.append(None) | |
| else: | |
| all_smiles.append(None) | |
| # Persist SMILES; optionally log depending on config | |
| with open(r"final_smiles_all.txt", "w") as fp: | |
| for smiles in all_smiles_without_test: | |
| fp.write("%s\n" % smiles) | |
| df = pd.DataFrame(all_smiles_without_test, columns=["SMILES"]) | |
| df.to_csv("final_smiles_all.csv", index=False) | |
| if getattr(getattr(self.args, "sample", object()), "log_smiles", False): | |
| print("All smiles saved") | |
| print(all_smiles_without_test) | |
| print("All SMILES saved to CSV") | |
| return valid, len(valid) / len(generated), np.array(num_components), all_smiles | |
| def compute_uniqueness(self, valid): | |
| """valid: list of SMILES strings.""" | |
| return list(set(valid)), len(set(valid)) / len(valid) | |
| def compute_novelty(self, unique): | |
| num_novel = 0 | |
| novel = [] | |
| if self.dataset_smiles_list is None: | |
| print("Dataset smiles is None, novelty computation skipped") | |
| return 1, 1 | |
| for smiles in tqdm(unique, desc="Unique molecules novelty check progress"): | |
| if smiles not in self.dataset_smiles_list: | |
| novel.append(smiles) | |
| num_novel += 1 | |
| return novel, num_novel / len(unique) | |
| def compute_relaxed_validity(self, generated): | |
| valid = [] | |
| for graph in tqdm( | |
| generated, desc="Generated molecules relaxed validity check progress" | |
| ): | |
| atom_types, edge_types = graph | |
| mol = build_molecule_with_partial_charges( | |
| atom_types, edge_types, self.dataset_info.atom_decoder | |
| ) | |
| smiles = mol2smiles(mol) | |
| if smiles is not None: | |
| try: | |
| mol_frags = Chem.rdmolops.GetMolFrags( | |
| mol, asMols=True, sanitizeFrags=True | |
| ) | |
| largest_mol = max( | |
| mol_frags, default=mol, key=lambda m: m.GetNumAtoms() | |
| ) | |
| smiles = mol2smiles(largest_mol) | |
| valid.append(smiles) | |
| except Chem.rdchem.AtomValenceException: | |
| print("Valence error in GetmolFrags") | |
| except Chem.rdchem.KekulizeException: | |
| print("Can't kekulize molecule") | |
| return valid, len(valid) / len(generated) | |
| def evaluate(self, generated, input_properties=None, test=False): | |
| """generated: list of pairs (positions: n x 3, atom_types: n [int]) | |
| the positions and atom types should already be masked.""" | |
| strict_valid, strict_validity, num_components, all_smiles = self.compute_validity( | |
| generated | |
| ) | |
| if test: | |
| # Persist SMILES (optionally log depending on config) | |
| with open(r"final_smiles.txt", "w") as fp: | |
| for smiles in all_smiles: | |
| fp.write("%s\n" % smiles) | |
| df = pd.DataFrame(all_smiles, columns=["SMILES"]) | |
| df.to_csv("final_smiles.csv", index=False) | |
| if getattr(getattr(self.args, "sample", object()), "log_smiles", False): | |
| print("All smiles saved") | |
| print(all_smiles) | |
| print("All SMILES saved to CSV") | |
| nc_mu = num_components.mean() if len(num_components) > 0 else 0 | |
| nc_min = num_components.min() if len(num_components) > 0 else 0 | |
| nc_max = num_components.max() if len(num_components) > 0 else 0 | |
| total_mols = len(generated) | |
| strict_label = self.validity_labels["strict"] | |
| relaxed_label = self.validity_labels["vfm_relaxed"] | |
| print(f"{strict_label} over {total_mols} molecules: {strict_validity * 100 :.2f}%") | |
| print( | |
| f"Number of connected components of {total_mols} molecules: min:{nc_min:.2f} mean:{nc_mu:.2f} max:{nc_max:.2f}" | |
| ) | |
| relaxed_valid, relaxed_validity = self.compute_relaxed_validity(generated) | |
| print(f"{relaxed_label} over {total_mols} molecules: {relaxed_validity * 100 :.2f}%") | |
| self.latest_validity_values["strict"] = strict_validity | |
| self.latest_validity_values["vfm_relaxed"] = relaxed_validity | |
| self.primary_validity_label = self.validity_labels[self.validity_mode] | |
| self._secondary_mode = "strict" if self.validity_mode == "vfm_relaxed" else "vfm_relaxed" | |
| self.secondary_validity_label = self.validity_labels[self._secondary_mode] | |
| self.latest_validity_labels = { | |
| "primary": self.primary_validity_label, | |
| "secondary": self.secondary_validity_label, | |
| } | |
| if self.validity_mode == "vfm_relaxed": | |
| primary_valid, primary_validity = relaxed_valid, relaxed_validity | |
| secondary_validity = strict_validity | |
| else: | |
| primary_valid, primary_validity = strict_valid, strict_validity | |
| secondary_validity = relaxed_validity | |
| self.latest_validity_values["primary"] = primary_validity | |
| self.latest_validity_values["secondary"] = secondary_validity | |
| if input_properties is not None: | |
| raise NotImplementedError( | |
| "Property-conditioned molecular metrics are not included in this release." | |
| ) | |
| cond_mae = cond_val = -1.0 | |
| if len(primary_valid) > 0: | |
| unique, uniqueness = self.compute_uniqueness(primary_valid) | |
| print( | |
| f"Uniqueness over {len(primary_valid)} valid molecules: {uniqueness * 100 :.2f}%" | |
| ) | |
| if self.dataset_smiles_list is not None: | |
| _, novelty = self.compute_novelty(unique) | |
| print( | |
| f"Novelty over {len(unique)} unique valid molecules: {novelty * 100 :.2f}%" | |
| ) | |
| else: | |
| novelty = -1.0 | |
| else: | |
| novelty = -1.0 | |
| uniqueness = 0.0 | |
| unique = [] | |
| return ( | |
| [primary_validity, secondary_validity, uniqueness, novelty], | |
| unique, | |
| dict(nc_min=nc_min, nc_max=nc_max, nc_mu=nc_mu), | |
| all_smiles, | |
| [cond_mae, cond_val], | |
| ) | |
| def mol2smiles(mol): | |
| try: | |
| Chem.SanitizeMol(mol) | |
| except ValueError: | |
| return None | |
| return Chem.MolToSmiles(mol) | |
| def mol2smilesWithNoSanitize(mol): | |
| return Chem.MolToSmiles(mol) | |
| def build_molecule(atom_types, edge_types, atom_decoder, verbose=False): | |
| if verbose: | |
| print("building new molecule") | |
| mol = Chem.RWMol() | |
| for atom in atom_types: | |
| a = Chem.Atom(atom_decoder[atom.item()]) | |
| mol.AddAtom(a) | |
| if verbose: | |
| print("Atom added: ", atom.item(), atom_decoder[atom.item()]) | |
| edge_types = torch.triu(edge_types) | |
| edge_types[edge_types >= 5] = 0 # set edges in virtual state to non-bonded | |
| all_bonds = torch.nonzero(edge_types) | |
| for bond in all_bonds: | |
| if bond[0].item() != bond[1].item(): | |
| mol.AddBond( | |
| bond[0].item(), | |
| bond[1].item(), | |
| bond_dict[edge_types[bond[0], bond[1]].item()], | |
| ) | |
| if verbose: | |
| print( | |
| "bond added:", | |
| bond[0].item(), | |
| bond[1].item(), | |
| edge_types[bond[0], bond[1]].item(), | |
| bond_dict[edge_types[bond[0], bond[1]].item()], | |
| ) | |
| return mol | |
| def build_molecule_with_partial_charges( | |
| atom_types, edge_types, atom_decoder, verbose=False | |
| ): | |
| if verbose: | |
| print("\nbuilding new molecule") | |
| mol = Chem.RWMol() | |
| for atom in atom_types: | |
| a = Chem.Atom(atom_decoder[atom.item()]) | |
| mol.AddAtom(a) | |
| if verbose: | |
| print("Atom added: ", atom.item(), atom_decoder[atom.item()]) | |
| edge_types = torch.triu(edge_types) | |
| edge_types[edge_types >= 5] = 0 # set edges in virtual state to non-bonded | |
| all_bonds = torch.nonzero(edge_types) | |
| for bond in all_bonds: | |
| if bond[0].item() != bond[1].item(): | |
| mol.AddBond( | |
| bond[0].item(), | |
| bond[1].item(), | |
| bond_dict[edge_types[bond[0], bond[1]].item()], | |
| ) | |
| if verbose: | |
| print( | |
| "bond added:", | |
| bond[0].item(), | |
| bond[1].item(), | |
| edge_types[bond[0], bond[1]].item(), | |
| bond_dict[edge_types[bond[0], bond[1]].item()], | |
| ) | |
| # Add positive formal charges for supported over-valent atoms. | |
| flag, atomid_valence = check_valency(mol) | |
| if verbose: | |
| print("flag, valence", flag, atomid_valence) | |
| if flag: | |
| continue | |
| else: | |
| assert len(atomid_valence) == 2 | |
| idx = atomid_valence[0] | |
| v = atomid_valence[1] | |
| an = mol.GetAtomWithIdx(idx).GetAtomicNum() | |
| if verbose: | |
| print("atomic num of atom with a large valence", an) | |
| if an in (7, 8, 16) and (v - ATOM_VALENCY[an]) == 1: | |
| mol.GetAtomWithIdx(idx).SetFormalCharge(1) | |
| return mol | |
| # Functions from GDSS | |
| def check_valency(mol): | |
| try: | |
| Chem.SanitizeMol(mol, sanitizeOps=Chem.SanitizeFlags.SANITIZE_PROPERTIES) | |
| return True, None | |
| except ValueError as e: | |
| e = str(e) | |
| p = e.find("#") | |
| e_sub = e[p:] | |
| atomid_valence = list(map(int, re.findall(r"\d+", e_sub))) | |
| return False, atomid_valence | |
| def check_stability( | |
| atom_types, edge_types, dataset_info, debug=False, atom_decoder=None | |
| ): | |
| if atom_decoder is None: | |
| atom_decoder = dataset_info.atom_decoder | |
| n_bonds = np.zeros(len(atom_types), dtype="int") | |
| for i in range(len(atom_types)): | |
| for j in range(i + 1, len(atom_types)): | |
| n_bonds[i] += abs((edge_types[i, j] + edge_types[j, i]) / 2) | |
| n_bonds[j] += abs((edge_types[i, j] + edge_types[j, i]) / 2) | |
| n_stable_bonds = 0 | |
| for atom_type, atom_n_bond in zip(atom_types, n_bonds): | |
| possible_bonds = allowed_bonds[atom_decoder[atom_type]] | |
| if type(possible_bonds) == int: | |
| is_stable = possible_bonds == atom_n_bond | |
| else: | |
| is_stable = atom_n_bond in possible_bonds | |
| if not is_stable and debug: | |
| print( | |
| "Invalid bonds for molecule %s with %d bonds" | |
| % (atom_decoder[atom_type], atom_n_bond) | |
| ) | |
| n_stable_bonds += int(is_stable) | |
| molecule_stable = n_stable_bonds == len(atom_types) | |
| return molecule_stable, n_stable_bonds, len(atom_types) | |
| def compute_molecular_metrics( | |
| molecule_list, train_smiles, dataset_info, labels, args=None, test=False | |
| ): | |
| """molecule_list: (dict)""" | |
| if not dataset_info.remove_h: | |
| print("Analyzing molecule stability...") | |
| molecule_stable = 0 | |
| nr_stable_bonds = 0 | |
| n_atoms = 0 | |
| n_molecules = len(molecule_list) | |
| for mol in tqdm(molecule_list, desc="Stability computation progress"): | |
| atom_types, edge_types = mol | |
| validity_results = check_stability(atom_types, edge_types, dataset_info) | |
| molecule_stable += int(validity_results[0]) | |
| nr_stable_bonds += int(validity_results[1]) | |
| n_atoms += int(validity_results[2]) | |
| # Validity | |
| fraction_mol_stable = molecule_stable / float(n_molecules) | |
| fraction_atm_stable = nr_stable_bonds / float(n_atoms) | |
| validity_dict = { | |
| "mol_stable": fraction_mol_stable, | |
| "atm_stable": fraction_atm_stable, | |
| } | |
| else: | |
| validity_dict = {"mol_stable": -1, "atm_stable": -1} | |
| metrics = BasicMolecularMetrics(dataset_info, train_smiles, args) | |
| rdkit_metrics = metrics.evaluate(molecule_list, labels, test) | |
| all_smiles = rdkit_metrics[-2] | |
| nc = rdkit_metrics[-3] | |
| primary_label = getattr(metrics, "primary_validity_label", "validity_VFM_relaxed") | |
| secondary_label = getattr(metrics, "secondary_validity_label", "validity_strict") | |
| dic = { | |
| primary_label: rdkit_metrics[0][0], | |
| secondary_label: rdkit_metrics[0][1], | |
| "Uniqueness": rdkit_metrics[0][2], | |
| "Novelty": rdkit_metrics[0][3], | |
| "nc_min": nc["nc_min"], | |
| "nc_max": nc["nc_max"], | |
| "nc_mu": nc["nc_mu"], | |
| "cond_mae": rdkit_metrics[-1][0], | |
| "cond_val": rdkit_metrics[-1][1], | |
| } | |
| return validity_dict, rdkit_metrics, all_smiles, dic | |