######################################################################
# BioSimSpace: Making biomolecular simulation a breeze!
#
# Copyright: 2017-2024
#
# Authors: Lester Hedges <lester.hedges@gmail.com>
#
# BioSimSpace is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# BioSimSpace is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with BioSimSpace. If not, see <http://www.gnu.org/licenses/>.
#####################################################################
"""Tools for visualising molecular systems."""
__author__ = "Lester Hedges"
__email__ = "lester.hedges@gmail.com"
__all__ = ["View"]
import glob as _glob
import os as _os
import shutil as _shutil
import tempfile as _tempfile
import warnings as _warnings
from sire.legacy import IO as _SireIO
from sire.legacy import Mol as _SireMol
from sire.legacy import System as _SireSystem
from .. import _is_notebook, _isVerbose
from .. import IO as _IO
from ..Process._process import Process as _Process
from .._SireWrappers import System as _System
[docs]
class View:
"""A class for handling interactive molecular visualisations."""
[docs]
def __init__(self, handle, property_map={}, is_lambda1=False):
"""
Constructor.
Parameters
----------
handle : :class:`Process <BioSimSpace.Process>`, \
:class:`System <BioSimSpace._SireWrappers.System>` \
:class:`System <BioSimSpace._SireWrappers.Molecule>` \
:class:`System <BioSimSpace._SireWrappers.Molecules>` \
str, [str]
A handle to a process, system, molecule, or molecule container,
or the path to molecular input file(s).
property_map : dict
A dictionary that maps system "properties" to their user defined
values. This allows the user to refer to properties with their
own naming scheme, e.g. { "charge" : "my-charge" }
is_lambda1 : bool
Whether to use the lambda = 1 end state when visualising
perturbable molecules. By default, the state at lambda = 0
is used.
"""
# Make sure we're running inside a Jupyter notebook.
if not _is_notebook:
_warnings.warn(
"You can only use BioSimSpace.Notebook.View from within a Jupyter notebook."
)
return None
# Validate the map.
if not isinstance(property_map, dict):
raise TypeError("'property_map' must be of type 'dict'")
self._property_map = property_map
# Check the end state flag.
if not isinstance(is_lambda1, bool):
raise TypeError("'is_lambda1' must be of type 'bool'")
self._is_lamba1 = is_lambda1
# Check the handle.
# Convert tuple to list.
if isinstance(handle, tuple):
handle = list(handle)
# Convert single string to list.
if isinstance(handle, str):
handle = [handle]
# List of strings (file paths).
if isinstance(handle, list) and all(isinstance(x, str) for x in handle):
system = _IO.readMolecules(handle, property_map=property_map)
self._handle = system._getSireObject()
self._is_process = False
# BioSimSpace process.
elif isinstance(handle, _Process):
self._handle = handle
self._is_process = True
# BioSimSpace system.
elif isinstance(handle, _System):
self._handle = handle.copy()._getSireObject()
self._is_process = False
else:
try:
handle = handle.toSystem()
self._handle = handle._getSireObject()
self._is_process = False
except:
raise TypeError(
"The handle must be of type 'BioSimSpace.Process', "
"'BioSimSpace._SireWrappers.System', "
"'BioSimSpace._SireWrappers.Molecule', "
"'BioSimSpace._SireWrappers.Molecules', "
"'str', or a list of 'str' types."
)
# Create a temporary workspace for the view object.
self._tmp_dir = _tempfile.TemporaryDirectory()
self._work_dir = self._tmp_dir.name
# Zero the number of views.
self._num_views = 0
# Reconstruct a system representing the chosen lambda end state.
if not self._is_process:
self._handle = self._reconstruct_system(self._handle, self._is_lamba1)
[docs]
def system(self, gui=True):
"""
View the entire molecular system.
Parameters
----------
gui : bool
Whether to display the gui.
"""
# Make sure we're running inside a Jupyter notebook.
if not _is_notebook:
return None
# Get the latest system from the process.
if self._is_process:
system = self._handle.getSystem()
# No system.
if system is None:
return
else:
system = system._getSireObject()
# Reconstruct the chosen lambda end state if perturbable molecules
# are present.
system = self._reconstruct_system(system, self._is_lamba1)
else:
system = self._handle
# Create and return the view.
return self._create_view(system, gui=gui)
[docs]
def molecules(self, indices=None, gui=True):
"""
View specific molecules.
Parameters
----------
indices : [int], range
A list of molecule indices.
gui : bool
Whether to display the gui.
"""
# Make sure we're running inside a Jupyter notebook.
if not _is_notebook:
return None
# Return a view of the entire system.
if indices is None:
return self.system(gui=gui)
# Convert range or tuple to list.
if isinstance(indices, (range, tuple)):
indices = list(indices)
# Convert single indices to a list.
elif not isinstance(indices, list):
indices = [indices]
# Check that the indices is a list of integers.
if not all(type(x) is int for x in indices):
raise TypeError("'indices' must be a 'list' of type 'int'")
# Get the latest system from the process.
if self._is_process:
system = self._handle.getSystem()._getSireObject()
# No system.
if system is None:
return
# Reconstruct the chosen lambda end state if perturbable molecules
# are present.
system = self._reconstruct_system(system, self._is_lamba1)
else:
system = self._handle
# Extract the molecule numbers.
molnums = system.molNums()
# Create a new system.
s = _SireSystem.System("BioSimSpace_System")
m = _SireMol.MoleculeGroup("all")
# Loop over all of the indices.
for index in indices:
if index < 0:
index += len(molnums)
if index < 0 or index > len(molnums):
raise ValueError("Molecule index is out of range!")
# Add the molecule.
m.add(system[molnums[index]])
# Add all of the molecules to the system.
s.add(m)
# Create and return the view.
return self._create_view(s, gui=gui)
[docs]
def molecule(self, index=0, gui=True):
"""
View a specific molecule.
Parameters
----------
index : int
The molecule index.
gui : bool
Whether to display the gui.
"""
# Make sure we're running inside a Jupyter notebook.
if not _is_notebook:
return None
# Check that the index is an integer.
if not type(index) is int:
raise TypeError("'index' must be of type 'int'")
# Get the latest system from the process.
if self._is_process:
system = self._handle.getSystem()._getSireObject()
# No system.
if system is None:
return
# Reconstruct the chosen lambda end state if perturbable molecules
# are present.
system = self._reconstruct_system(system, self._is_lamba1)
else:
system = self._handle
# Extract the molecule numbers.
molnums = system.molNums()
# Make sure the index is valid.
if index < 0:
index += len(molnums)
if index < 0 or index > len(molnums):
raise ValueError("Molecule index is out of range!")
# Create a new system and add a single molecule.
s = _SireSystem.System("BioSimSpace_System")
m = _SireMol.MoleculeGroup("all")
m.add(system[molnums[index]])
s.add(m)
# Create and return the view.
return self._create_view(s, gui=gui)
[docs]
def reload(self, index=None, gui=True):
"""
Reload a particular view.
Parameters
----------
index : int
The view index.
gui : bool
Whether to display the gui.
"""
# Make sure we're running inside a Jupyter notebook.
if not _is_notebook:
return None
# Return if there are no views.
if self._num_views == 0:
return
# Default to the most recent view.
if index is None:
index = self._num_views - 1
# Check that the index is an integer.
elif not type(index) is int:
raise TypeError("'index' must be of type 'int'")
# Make sure the view index is valid.
if index < 0 or index >= self._num_views:
raise ValueError(
"View index (%d) is out of range: [0-%d]" % (index, self._num_views - 1)
)
# Create and return the view.
return self._create_view(view=index, gui=gui)
[docs]
def nViews(self):
"""
Return the number of views.
Return
------
num_views : int
The number of views.
"""
return self._num_views
[docs]
def savePDB(self, file, index=None):
"""
Save a specific view as a PDB file.
Parameters
----------
file : str
The name of the file to write to.
index : int
The view index.
"""
# Make sure we're running inside a Jupyter notebook.
if not _is_notebook:
return None
# Default to the most recent view.
if index is None:
index = self._num_views - 1
# Check that the index is an integer.
elif not type(index) is int:
raise TypeError("'index' must be of type 'int'")
# Make sure the view index is valid.
if index < 0 or index >= self._num_views:
raise ValueError(
"View index (%d) is out of range: [0-%d]" % (index, self._num_views - 1)
)
# Copy the file to the chosen location.
_shutil.copyfile("%s/view_%04d.pdb" % (self._work_dir, index), file)
[docs]
def reset(self):
"""Reset the object, clearing all view files."""
# Glob all of the view PDB structure files.
files = _glob.glob("%s/*.pdb" % self._work_dir)
# Remove all of the files.
for file in files:
_os.remove(file)
# Reset the number of views.
self._num_views = 0
def _create_view(self, system=None, view=None, gui=True):
"""
Helper function to create the NGLview object.
Parameters
----------
system : Sire.System.System
A Sire molecular system.
view : int
The index of an existing view.
gui : bool
Whether to display the gui.
"""
if system is None and view is None:
raise ValueError("Both 'system' and 'view' cannot be 'None'.")
elif system is not None and view is not None:
raise ValueError("One of 'system' or 'view' must be 'None'.")
# Make sure gui flag is valid.
if gui not in [True, False]:
gui = True
# Default to the most recent view.
if view is None:
index = self._num_views
else:
index = view
# Create the file name.
filename = "%s/view_%04d.pdb" % (self._work_dir, index)
# Increment the number of views.
if view is None:
self._num_views += 1
# Create a PDB object and write to file.
if system is not None:
try:
pdb = _SireIO.PDB2(system, self._property_map)
pdb.writeToFile(filename)
except Exception as e:
msg = "Failed to write system to 'PDB' format."
if _isVerbose():
print(msg)
raise IOError(e) from None
else:
raise IOError(msg) from None
# Import NGLView when it is used for the first time.
import nglview as _nglview
# Create the NGLview object.
view = _nglview.show_file(filename)
# Return the view and display it.
return view.display(gui=gui)
def _reconstruct_system(self, system, is_lambda1=False):
"""
Helper function to reconstruct a lambda end state for a perturbable
system.
Parameters
----------
system : Sire.System.System
A Sire molecular system.
is_lambda1 : bool
Whether to use the lambda = 1 end state for reconstructing
perturbable molecules. By default, the state at lambda = 0
"""
# Convert to a BioSimSpace system.
system = _System(system)
# Get a list of perturbable molecules.
pert_mols = system.getPerturbableMolecules()
# If there are no perturbable molecules, simply return the original system.
if len(pert_mols) == 0:
return system._sire_object
# Convert all perturbable molecules to the chosen end state, updating
# them in the system.
else:
for mol in pert_mols:
system.updateMolecules(mol._toRegularMolecule(is_lambda1=is_lambda1))
return system._sire_object