From 4471cb91de37edc1679da4152ad51d22b8975c49 Mon Sep 17 00:00:00 2001 From: Josh Mitchell Date: Thu, 19 Oct 2023 15:35:04 +1100 Subject: [PATCH 01/11] Fix Molecule.visualize() overrides --- openff/toolkit/topology/molecule.py | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/openff/toolkit/topology/molecule.py b/openff/toolkit/topology/molecule.py index 30ed926a7..b9719a011 100644 --- a/openff/toolkit/topology/molecule.py +++ b/openff/toolkit/topology/molecule.py @@ -5381,7 +5381,10 @@ def add_conformer(self, coordinates): @overload def visualize( self, - backend: Literal["rdkit"], + backend: Literal["rdkit"] = ..., + width: int = ..., + height: int = ..., + show_all_hydrogens: bool = ..., ) -> "IPython.display.SVG": ... @@ -5389,6 +5392,9 @@ def visualize( def visualize( self, backend: Literal["openeye"], + width: int = ..., + height: int = ..., + show_all_hydrogens: bool = ..., ) -> "IPython.display.Image": ... @@ -5401,30 +5407,30 @@ def visualize( def visualize( self, - backend: str = "rdkit", - width: int = 500, - height: int = 300, - show_all_hydrogens: bool = True, + backend="rdkit", + width=500, + height=300, + show_all_hydrogens=True, ) -> Union["IPython.display.SVG", "IPython.display.Image", "nglview.NGLWidget"]: """ Render a visualization of the molecule in Jupyter Parameters ---------- - backend : str, optional, default='rdkit' + backend The visualization engine to use. Choose from: - - ``"rdkit"`` + - ``"rdkit"`` (default) - ``"openeye"`` - ``"nglview"`` (requires conformers) - width : int, default=500 + width Width of the generated representation (only applicable to ``backend="openeye"`` or ``backend="rdkit"``) - height : int, default=300 + height Width of the generated representation (only applicable to ``backend="openeye"`` or ``backend="rdkit"``) - show_all_hydrogens : bool, default=True + show_all_hydrogens Whether to explicitly depict all hydrogen atoms. (only applicable to ``backend="openeye"`` or ``backend="rdkit"``) From d934f2f82dde93b2487283e1fb0055dd52b44014 Mon Sep 17 00:00:00 2001 From: Josh Mitchell Date: Thu, 19 Oct 2023 15:47:49 +1100 Subject: [PATCH 02/11] Fix path to Ipython display module --- openff/toolkit/topology/molecule.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/openff/toolkit/topology/molecule.py b/openff/toolkit/topology/molecule.py index b9719a011..16107a46b 100644 --- a/openff/toolkit/topology/molecule.py +++ b/openff/toolkit/topology/molecule.py @@ -90,7 +90,7 @@ from openff.toolkit.utils.utils import get_data_file_path, requires_package if TYPE_CHECKING: - import IPython.display + import IPython.core.display import nglview from openff.toolkit.topology._mm_molecule import _SimpleAtom, _SimpleMolecule @@ -5385,7 +5385,7 @@ def visualize( width: int = ..., height: int = ..., show_all_hydrogens: bool = ..., - ) -> "IPython.display.SVG": + ) -> "IPython.core.display.SVG": ... @overload @@ -5395,7 +5395,7 @@ def visualize( width: int = ..., height: int = ..., show_all_hydrogens: bool = ..., - ) -> "IPython.display.Image": + ) -> "IPython.core.display.Image": ... @overload @@ -5411,7 +5411,7 @@ def visualize( width=500, height=300, show_all_hydrogens=True, - ) -> Union["IPython.display.SVG", "IPython.display.Image", "nglview.NGLWidget"]: + ): """ Render a visualization of the molecule in Jupyter @@ -5439,8 +5439,8 @@ def visualize( object Depending on the backend chosen: - - rdkit → IPython.display.SVG - - openeye → IPython.display.Image + - rdkit → IPython.core.display.SVG + - openeye → IPython.core.display.Image - nglview → nglview.NGLWidget """ @@ -5502,7 +5502,7 @@ def visualize( if backend == "rdkit": if RDKIT_AVAILABLE: - from IPython.display import SVG + from IPython.core.display import SVG from rdkit.Chem.Draw import ( # type: ignore[import] rdDepictor, rdMolDraw2D, @@ -5535,7 +5535,7 @@ def visualize( backend = "openeye" if backend == "openeye": if OPENEYE_AVAILABLE: - from IPython.display import Image + from IPython.core.display import Image from openeye import oedepict oemol = self.to_openeye() From 81e7170392f760e493ecaaed9160aeef62674497 Mon Sep 17 00:00:00 2001 From: Josh Mitchell Date: Thu, 19 Oct 2023 16:14:58 +1100 Subject: [PATCH 03/11] Correct docs and types re: show_all_hydrogens w/ backend='nglview' --- openff/toolkit/topology/molecule.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/openff/toolkit/topology/molecule.py b/openff/toolkit/topology/molecule.py index 16107a46b..033384f19 100644 --- a/openff/toolkit/topology/molecule.py +++ b/openff/toolkit/topology/molecule.py @@ -5402,6 +5402,8 @@ def visualize( def visualize( self, backend: Literal["nglview"], + *, + show_all_hydrogens: bool = ..., ) -> "nglview.NGLWidget": ... @@ -5431,8 +5433,7 @@ def visualize( Width of the generated representation (only applicable to ``backend="openeye"`` or ``backend="rdkit"``) show_all_hydrogens - Whether to explicitly depict all hydrogen atoms. (only applicable to - ``backend="openeye"`` or ``backend="rdkit"``) + Whether to explicitly depict all hydrogen atoms. Returns ------- From 840dfd3db9081cb7b1783aac5ef65f41bf181a1e Mon Sep 17 00:00:00 2001 From: Josh Mitchell Date: Thu, 19 Oct 2023 16:15:39 +1100 Subject: [PATCH 04/11] Clean up Molecule.visualize docstring and code --- openff/toolkit/topology/molecule.py | 61 +++++++++++++---------------- 1 file changed, 27 insertions(+), 34 deletions(-) diff --git a/openff/toolkit/topology/molecule.py b/openff/toolkit/topology/molecule.py index 033384f19..adbf35b9e 100644 --- a/openff/toolkit/topology/molecule.py +++ b/openff/toolkit/topology/molecule.py @@ -5427,22 +5427,16 @@ def visualize( - ``"nglview"`` (requires conformers) width - Width of the generated representation (only applicable to + Width of the generated representation in pixels (only applicable to ``backend="openeye"`` or ``backend="rdkit"``) height - Width of the generated representation (only applicable to + Width of the generated representation in pixels (only applicable to ``backend="openeye"`` or ``backend="rdkit"``) show_all_hydrogens Whether to explicitly depict all hydrogen atoms. Returns ------- - object - Depending on the backend chosen: - - - rdkit → IPython.core.display.SVG - - openeye → IPython.core.display.Image - - nglview → nglview.NGLWidget """ import inspect @@ -5463,43 +5457,42 @@ def visualize( ): warnings.warn( f"Arguments `width` and `height` are ignored with {backend=}." - f"Found non-default values {width=} and {height=}", + + f"Found non-default values {width=} and {height=}", stacklevel=2, ) if self.conformers is None: raise MissingConformersError( "Visualizing with NGLview requires that the molecule has " - f"conformers, found {self.conformers=}" + + f"conformers, found {self.conformers=}" ) - else: - from openff.toolkit.utils._viz import MoleculeNGLViewTrajectory + from openff.toolkit.utils._viz import MoleculeNGLViewTrajectory - try: - widget = nv.NGLWidget( - MoleculeNGLViewTrajectory( - molecule=self, - ext="MOL2", - ) + try: + widget = nv.NGLWidget( + MoleculeNGLViewTrajectory( + molecule=self, + ext="MOL2", ) - except ValueError: - widget = nv.NGLWidget( - MoleculeNGLViewTrajectory( - molecule=self, - ext="PDB", - ) + ) + except ValueError: + widget = nv.NGLWidget( + MoleculeNGLViewTrajectory( + molecule=self, + ext="PDB", ) - - widget.clear_representations() - widget.add_representation( - "licorice", - sele="*" if show_all_hydrogens else "NOT hydrogen", - radius=0.25, - multipleBond=True, ) - return widget + widget.clear_representations() + widget.add_representation( + "licorice", + sele="*" if show_all_hydrogens else "NOT hydrogen", + radius=0.25, + multipleBond=True, + ) + + return widget if backend == "rdkit": if RDKIT_AVAILABLE: @@ -5529,8 +5522,8 @@ def visualize( else: warnings.warn( "RDKit was requested as a visualization backend but " - "it was not found to be installed. Falling back to " - "trying to use OpenEye for visualization.", + + "it was not found to be installed. Falling back to " + + "trying to use OpenEye for visualization.", stacklevel=2, ) backend = "openeye" From 4a3197e5fb01f980a851dd97a6273bdf1100edeb Mon Sep 17 00:00:00 2001 From: Josh Mitchell Date: Thu, 19 Oct 2023 17:12:26 +1100 Subject: [PATCH 05/11] remove ensure_correct_connectivity and update docstring --- openff/toolkit/topology/topology.py | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/openff/toolkit/topology/topology.py b/openff/toolkit/topology/topology.py index 19d243bb7..88b7e5abb 100644 --- a/openff/toolkit/topology/topology.py +++ b/openff/toolkit/topology/topology.py @@ -2404,24 +2404,16 @@ def is_constrained(self, iatom, jatom): return False @requires_package("nglview") - def visualize(self, ensure_correct_connectivity: bool = False) -> "NGLWidget": + def visualize(self) -> "NGLWidget": """ Visualize with NGLView. - Requires all molecules in this topology have positions. + Requires that all molecules in this topology have positions. NGLView is a 3D molecular visualization library for use in Jupyter - notebooks. Note that for performance reasons, by default the - visualized connectivity is inferred from positions and may not reflect - the connectivity in the ``Topology``. - - Parameters - ========== - - ensure_correct_connectivity: bool, default=False - If ``True``, the visualization will be guaranteed to reflect the - connectivity in the ``Topology``. Note that this will severely - degrade performance, especially for topologies with many atoms. + notebooks. Note that the visualized connectivity does not include bond + order information and may, in strained conformations, include bonds not + present in the topology. Examples ======== @@ -2439,15 +2431,10 @@ def visualize(self, ensure_correct_connectivity: bool = False) -> "NGLWidget": from openff.toolkit.utils._viz import TopologyNGLViewStructure - if ensure_correct_connectivity: - raise ValueError( - "`ensure_correct_connectivity` not (yet) implemented " - "(requires passing multi-molecule SDF files to NGLview)" - ) - if self.get_positions() is None: raise MissingConformersError( - "All molecules in this topology must have positions for it to be visualized in a widget." + "All molecules in this topology must have positions for it to " + + "be visualized in a widget." ) widget = nglview.NGLWidget( @@ -2467,7 +2454,6 @@ def visualize(self, ensure_correct_connectivity: bool = False) -> "NGLWidget": "licorice", sele="not water and not ion and not protein", radius=0.25, - multipleBond=bool(ensure_correct_connectivity), ) return widget From 6f05c45e70188974198ee64b143b706de70d7eb0 Mon Sep 17 00:00:00 2001 From: Josh Mitchell Date: Thu, 19 Oct 2023 18:00:07 +1100 Subject: [PATCH 06/11] Display multiple bonds when available --- openff/toolkit/topology/topology.py | 1 + 1 file changed, 1 insertion(+) diff --git a/openff/toolkit/topology/topology.py b/openff/toolkit/topology/topology.py index 88b7e5abb..49e4e6bdc 100644 --- a/openff/toolkit/topology/topology.py +++ b/openff/toolkit/topology/topology.py @@ -2454,6 +2454,7 @@ def visualize(self) -> "NGLWidget": "licorice", sele="not water and not ion and not protein", radius=0.25, + multipleBond=True, ) return widget From c003634d0697ccf68339fd31706232564e0d45b7 Mon Sep 17 00:00:00 2001 From: Josh Mitchell Date: Thu, 19 Oct 2023 18:01:53 +1100 Subject: [PATCH 07/11] Document that bonds may be deduced from positions in mol.visualize() --- openff/toolkit/topology/molecule.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/openff/toolkit/topology/molecule.py b/openff/toolkit/topology/molecule.py index adbf35b9e..d7bf54d58 100644 --- a/openff/toolkit/topology/molecule.py +++ b/openff/toolkit/topology/molecule.py @@ -5415,7 +5415,10 @@ def visualize( show_all_hydrogens=True, ): """ - Render a visualization of the molecule in Jupyter + Render a visualization of the molecule in Jupyter. + + Note that the ``"nglview"`` backend may, in strained conformations, + include bonds not present in the topology. Parameters ---------- From e80b3478845a8fd06ed07f163ffaecace3b4bd30 Mon Sep 17 00:00:00 2001 From: Josh Mitchell Date: Thu, 19 Oct 2023 18:35:03 +1100 Subject: [PATCH 08/11] Write PDB with RDKit when visualizing topologies --- openff/toolkit/topology/topology.py | 5 +--- openff/toolkit/utils/_viz.py | 45 +++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 9 deletions(-) diff --git a/openff/toolkit/topology/topology.py b/openff/toolkit/topology/topology.py index 49e4e6bdc..9250a20c7 100644 --- a/openff/toolkit/topology/topology.py +++ b/openff/toolkit/topology/topology.py @@ -2438,10 +2438,7 @@ def visualize(self) -> "NGLWidget": ) widget = nglview.NGLWidget( - TopologyNGLViewStructure( - topology=self, - ext="pdb", - ), + TopologyNGLViewStructure(topology=self), representations=[ dict(type="unitcell", params=dict()), ], diff --git a/openff/toolkit/utils/_viz.py b/openff/toolkit/utils/_viz.py index 6c4732be3..70a9757b1 100644 --- a/openff/toolkit/utils/_viz.py +++ b/openff/toolkit/utils/_viz.py @@ -1,5 +1,7 @@ +import math import uuid from io import StringIO +from typing import TextIO from nglview.base_adaptor import Structure, Trajectory from openff.units import unit @@ -93,15 +95,48 @@ class TopologyNGLViewStructure(Structure): def __init__( self, topology: Topology, - ext: str = "PDB", ): self.topology = topology - self.ext = ext.lower() + self.ext = "pdb" self.params: dict = dict() self.id = str(uuid.uuid4()) def get_structure_string(self): + from rdkit.Chem.rdmolfiles import PDBWriter + with StringIO() as f: - self.topology.to_file(f, file_format=self.ext) - structure_string = f.getvalue() - return structure_string + write_box_vectors(f, self.topology) + + writer = PDBWriter(f) + for mol in self.topology.molecules: + writer.write(mol.to_rdkit()) + + writer.close() + + return f.getvalue() + + +def write_box_vectors(file_obj: TextIO, topology: Topology): + if topology.box_vectors is not None: + a, b, c = topology.box_vectors.m_as(unit.nanometer) + a_length = a.norm() + b_length = b.norm() + c_length = c.norm() + + alpha = math.acos(b.dot(c) / (b_length * c_length)) + beta = math.acos(c.dot(a) / (c_length * a_length)) + gamma = math.acos(a.dot(b) / (a_length * b_length)) + + RAD_TO_DEG = 180 / math.pi + print( + "CRYST1%9.3f%9.3f%9.3f%7.2f%7.2f%7.2f P 1 1 " + % ( + a_length * 10, + b_length * 10, + c_length * 10, + alpha * RAD_TO_DEG, + beta * RAD_TO_DEG, + gamma * RAD_TO_DEG, + ), + file=file_obj, + ) From 0dc93d02acd6723de93701c008e9a5e14fa8070d Mon Sep 17 00:00:00 2001 From: Josh Mitchell Date: Thu, 19 Oct 2023 18:49:20 +1100 Subject: [PATCH 09/11] Fall back to OpenMM if RDKit unavailable and document --- openff/toolkit/topology/topology.py | 6 ++--- openff/toolkit/utils/_viz.py | 37 ++++++++++++++++------------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/openff/toolkit/topology/topology.py b/openff/toolkit/topology/topology.py index 9250a20c7..494b86b50 100644 --- a/openff/toolkit/topology/topology.py +++ b/openff/toolkit/topology/topology.py @@ -2411,9 +2411,9 @@ def visualize(self) -> "NGLWidget": Requires that all molecules in this topology have positions. NGLView is a 3D molecular visualization library for use in Jupyter - notebooks. Note that the visualized connectivity does not include bond - order information and may, in strained conformations, include bonds not - present in the topology. + notebooks. Note that the visualized molecule may, in strained + conformations, include bonds not present in the topology. Rendering + multiple bonds additionally requires RDKit. Examples ======== diff --git a/openff/toolkit/utils/_viz.py b/openff/toolkit/utils/_viz.py index 70a9757b1..83a23b577 100644 --- a/openff/toolkit/utils/_viz.py +++ b/openff/toolkit/utils/_viz.py @@ -7,6 +7,7 @@ from openff.units import unit from openff.toolkit import Molecule, Topology +from openff.toolkit.utils.toolkits import RDKIT_AVAILABLE MOLECULE_DEFAULT_REPS = [ dict(type="licorice", params=dict(radius=0.25, multipleBond=True)) @@ -25,7 +26,7 @@ class MoleculeNGLViewTrajectory(Structure, Trajectory): Parameters ---------- molecule - The `Molecule` object to display. + The ``Molecule`` object to display. ext The file extension to use to communicate with NGLView. The format must be supported for export by the Toolkit via the `Molecule.to_file() @@ -73,16 +74,14 @@ class TopologyNGLViewStructure(Structure): """ OpenFF Topology adaptor. + Communicates with NGLView via PDB, using RDKit to write redundant CONECT + records indicating multiple bonds. If RDKit is unavailable, falls back + to ``Topology.to_file``. + Parameters ---------- topology - The `Topology` object to display. - ext - The file extension to use to communicate with NGLView. The format must - be supported for export by the Toolkit via the `Topology.to_file() - ` method, and import by - NGLView. File formats supported by NGLView can be found at - https://nglviewer.org/ngl/api/manual/file-formats.html + The ``Topology`` object to display. Example ------- @@ -102,18 +101,24 @@ def __init__( self.id = str(uuid.uuid4()) def get_structure_string(self): - from rdkit.Chem.rdmolfiles import PDBWriter - with StringIO() as f: - write_box_vectors(f, self.topology) + if RDKIT_AVAILABLE: + from rdkit.Chem.rdmolfiles import PDBWriter + + write_box_vectors(f, self.topology) - writer = PDBWriter(f) - for mol in self.topology.molecules: - writer.write(mol.to_rdkit()) + writer = PDBWriter(f) + for mol in self.topology.molecules: + writer.write(mol.to_rdkit()) - writer.close() + writer.close() - return f.getvalue() + structure_string = f.getvalue() + else: + self.topology.to_file(f, file_format="pdb") + structure_string = f.getvalue() + + return structure_string def write_box_vectors(file_obj: TextIO, topology: Topology): From 32e87003ab99b323dd8b8af421dd067f4c356d25 Mon Sep 17 00:00:00 2001 From: Josh Mitchell Date: Thu, 19 Oct 2023 18:53:48 +1100 Subject: [PATCH 10/11] Update changelog --- docs/releasehistory.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/releasehistory.md b/docs/releasehistory.md index 3400b4ca1..0ae149ac9 100644 --- a/docs/releasehistory.md +++ b/docs/releasehistory.md @@ -20,8 +20,9 @@ Releases follow the `major.minor.micro` scheme recommended by [PEP440](https://w ## Current development -- [PR #1747](https://github.com/openforcefield/openff-toolkit/pull/1747): Warns if a SMILES with full atom mappings is passed to `Moleucle.from_smiles`, which does not use the atom map for atom ordering (`Molecule.from_mapped_smiles` does). -- [PR #1731](https://github.com/openforcefield/openff-toolkit/pull/1731): Suppot SMIRNOFF vdW version 0.5. +- [PR #1747](https://github.com/openforcefield/openff-toolkit/pull/1747): Warns if a SMILES with full atom mappings is passed to `Molecule.from_smiles`, which does not use the atom map for atom ordering (`Molecule.from_mapped_smiles` does). +- [PR #1731](https://github.com/openforcefield/openff-toolkit/pull/1731): Support SMIRNOFF vdW version 0.5. +- [PR #1751](https://github.com/openforcefield/openff-toolkit/pull/1751): Improve visualization API docs and support multiple bonds in `Topology.visualize()` ## 0.14.4 From 4ff6bab7a66e390b23e2f3fac73511045ea469f9 Mon Sep 17 00:00:00 2001 From: Josh Mitchell Date: Fri, 10 Nov 2023 14:44:09 +1100 Subject: [PATCH 11/11] Ignore missing type annotations in rdkit --- openff/toolkit/utils/_viz.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openff/toolkit/utils/_viz.py b/openff/toolkit/utils/_viz.py index 83a23b577..1c05c2ba0 100644 --- a/openff/toolkit/utils/_viz.py +++ b/openff/toolkit/utils/_viz.py @@ -103,11 +103,11 @@ def __init__( def get_structure_string(self): with StringIO() as f: if RDKIT_AVAILABLE: - from rdkit.Chem.rdmolfiles import PDBWriter + from rdkit.Chem.rdmolfiles import PDBWriter # type: ignore write_box_vectors(f, self.topology) - writer = PDBWriter(f) + writer: PDBWriter = PDBWriter(f) for mol in self.topology.molecules: writer.write(mol.to_rdkit())