def conformers( mol: Chem.rdchem.Mol, conf_id: int = -1, n_confs: Union[int, List[int]] = None, align_conf: bool = True, n_cols: int = 3, sync_views: bool = True, remove_hs: bool = True, width: str = "auto", ): """Visualize the conformer(s) of a molecule. Args: mol: a molecule. conf_id: The ID of the conformer to show. -1 shows the first conformer. Only works if `n_confs` is None. n_confs: Can be a number of conformers to shows or a list of conformer indices. When None, only the first conformer is displayed. When -1, show all conformers. align_conf: Whether to align conformers together. n_cols: Number of columns. Defaults to 3. sync_views: Wether to sync the multiple views. remove_hs: Wether to remove the hydrogens of the conformers. width: The width of the returned view. Defaults to "auto". """ widgets = _get_ipywidgets() nv = _get_nglview() if mol.GetNumConformers() == 0: raise ValueError( "The molecule has 0 conformers. You can generate conformers with `dm.conformers.generate(mol)`." ) # Clone the molecule mol = copy.deepcopy(mol) if remove_hs: mol = Chem.RemoveHs(mol) # type: ignore else: mol = Chem.AddHs(mol) # type: ignore if n_confs is None: return nv.show_rdkit(mol, conf_id=conf_id) # If n_confs is int, convert to list of conformer IDs if n_confs == -1: n_confs = [conf.GetId() for conf in mol.GetConformers()] elif isinstance(n_confs, int): if n_confs > mol.GetNumConformers(): n_confs = mol.GetNumConformers() n_confs = list(range(n_confs)) # type: ignore if align_conf: rdMolAlign.AlignMolConformers(mol, confIds=n_confs) # Get number of rows n_rows = len(n_confs) // n_cols n_rows += 1 if (len(n_confs) % n_cols) > 0 else 0 # Create a grid grid = widgets.GridspecLayout(n_rows, n_cols) # type: ignore # Create and add views to the grid. widget_coords = itertools.product(range(n_rows), range(n_cols)) views = [] for i, (conf_id, (x, y)) in enumerate(zip(n_confs, widget_coords)): view = nv.show_rdkit(mol, conf_id=conf_id) view.layout.width = width view.layout.align_self = "stretch" grid[x, y] = view views.append(view) # Sync views if sync_views: for view in views: view._set_sync_camera(views) return grid
def generate( mol: Chem.rdchem.Mol, n_confs: int = None, rms_cutoff: Optional[float] = None, clear_existing: bool = True, align_conformers: bool = True, minimize_energy: bool = False, method: str = None, energy_iterations: int = 500, warning_not_converged: int = 10, random_seed: int = 19, add_hs: bool = True, verbose: bool = False, ) -> Chem.rdchem.Mol: """Compute conformers of a molecule. Example: ```python import datamol as dm smiles = "O=C(C)Oc1ccccc1C(=O)O" mol = dm.to_mol(smiles) mol = dm.conformers.generate(mol) # Get all conformers as a list conformers = mol.GetConformers() # Get the 3D atom positions of the first conformer positions = mol.GetConformer(0).GetPositions() # If minimization has been enabled (default to True) # you can access the computed energy. conf = mol.GetConformer(0) props = conf.GetPropsAsDict() print(props) # {'rdkit_uff_energy': 1.7649408317784008} ``` Args: mol: a molecule n_confs: Number of conformers to generate. Depends on the number of rotatable bonds by default. rms_cutoff: The minimum RMS value in Angstrom at which two conformers are considered redundant and one is deleted. If None, all conformers are kept. This step is done after an eventual minimization step. clear_existing: Whether to overwrite existing conformers for the molecule. align_conformers: Wehther to align conformer. minimize_energy: Wether to minimize conformer's energies using UFF. Disable to generate conformers much faster. method: RDKit method to use for embedding. Choose among ["ETDG", "ETKDG", "ETKDGv2", "ETKDGv3"]. If None, "ETKDGv3" is used. energy_iterations: Maximum number of iterations during the energy minimization procedure. It corresponds to the `maxIters` argument in RDKit. warning_not_converged: Wether to log a warning when the number of not converged conformers during the minimization is higher than `warning_not_converged`. Only works when `verbose` is set to True. Disable with 0. Defaults to 10. random_seed: Set to None or -1 to disable. add_hs: Whether to add hydrogens to the mol before embedding. If set to True, the hydrogens are removed in the returned molecule. Warning: explicit hydrogens won't be conserved. It is strongly recommended to let the default value to True. The RDKit documentation says: "To get good 3D conformations, it’s almost always a good idea to add hydrogens to the molecule first." verbose: Wether to enable logs during the process. Returns: mol: the molecule with the conformers. """ AVAILABLE_METHODS = ["ETDG", "ETKDG", "ETKDGv2", "ETKDGv3"] if method is None: method = "ETKDGv3" if method not in AVAILABLE_METHODS: raise ValueError( f"The method {method} is not supported. Use from {AVAILABLE_METHODS}" ) # Random seed if random_seed is None: random_seed = -1 # Clone molecule mol = copy.deepcopy(mol) # Remove existing conformers if clear_existing: mol.RemoveAllConformers() # Add hydrogens if add_hs: mol = Chem.AddHs(mol) if not n_confs: # Set the number of conformers depends on # the number of rotatable bonds. rotatable_bonds = Descriptors.NumRotatableBonds(mol) if rotatable_bonds < 8: n_confs = 50 elif rotatable_bonds < 12: n_confs = 200 else: n_confs = 300 # Embed conformers params = getattr(AllChem, method)() params.randomSeed = random_seed params.enforceChirality = True confs = AllChem.EmbedMultipleConfs(mol, numConfs=n_confs, params=params) # Sometime embedding fails. Here we try again by disabling `enforceChirality`. if len(confs) == 0: if verbose: logger.warning( f"Conformers embedding failed for {dm.to_smiles(mol)}. Trying without enforcing chirality." ) params = getattr(AllChem, method)() params.randomSeed = random_seed params.enforceChirality = False confs = AllChem.EmbedMultipleConfs(mol, numConfs=n_confs, params=params) if len(confs) == 0: raise ValueError( f"Conformers embedding failed for {dm.to_smiles(mol)}") # Minimize energy if minimize_energy: # Minimize conformer's energy using UFF results = AllChem.UFFOptimizeMoleculeConfs(mol, maxIters=energy_iterations) energies = [energy for _, energy in results] # Some conformers might not have converged during minimization. not_converged = sum( [not_converged for not_converged, _ in results if not_converged]) if warning_not_converged != 0 and not_converged > warning_not_converged and verbose: logger.warning( f"{not_converged}/{len(results)} conformers have not converged for {dm.to_smiles(mol)}" ) # Add the energy as a property to each conformers [ conf.SetDoubleProp("rdkit_uff_energy", energy) for energy, conf in zip(energies, mol.GetConformers()) ] # Now we reorder conformers according to their energies, # so the lowest energies conformers are first. mol_clone = copy.deepcopy(mol) ordered_conformers = [ conf for _, conf in sorted(zip(energies, mol_clone.GetConformers())) ] mol.RemoveAllConformers() [mol.AddConformer(conf, assignId=True) for conf in ordered_conformers] # Align conformers to each others if align_conformers: rdMolAlign.AlignMolConformers(mol) if rms_cutoff is not None: mol = cluster( mol, rms_cutoff=rms_cutoff, already_aligned=align_conformers, centroids=True, ) # type: ignore if add_hs: mol = Chem.RemoveHs(mol) return mol