"""
Pythonic wrappings for mmsurf surfaces
"""
import enum
import decorator
import numpy
from schrodinger.analysis.visanalysis import volumedata
from schrodinger.analysis.visanalysis import volumedataio
from schrodinger.infra import mm
from schrodinger.infra import mmbitset
from schrodinger.infra import mmsurf
from schrodinger.structutils import analyze
from schrodinger.structutils import color
from schrodinger.utils import fileutils
# This module is first imported while Maestro is loading, so
# schrodinger.get_maestro() will return a _DummyMaestroModule if we run it now.
# Instead, we import Maestro when the first Surface object is instantiated.
maestro = DELAYED_MAESTRO_LOAD = object()
[docs]class Style(enum.IntEnum):
"""
Surface representation styles.
"""
solid = mmsurf.MMSURF_STYLE_SOLID
mesh = mmsurf.MMSURF_STYLE_MESH
dot = mmsurf.MMSURF_STYLE_DOT
[docs]class ColorFrom(enum.IntEnum):
"""
Values for surface color sources.
"""
surface = mmsurf.MMSURF_COLOR_FROM_SURFACE
vertex = mmsurf.MMSURF_COLOR_FROM_VERTEX
nearest_asl_atom = mmsurf.MMSURF_COLOR_FROM_NEAREST_ASL_ATOM
volume = mmsurf.MMSURF_COLOR_FROM_VOLUME
entry = mmsurf.MMSURF_COLOR_FROM_ENTRY
[docs]class StrEnum(str, enum.Enum):
"""
An enum class where all values are strings. All enum objects stringify to
their value.
"""
def __str__(self):
return self.value
[docs]class ColorBy(StrEnum):
"""
Values for surface color schemes.
"""
source_color = mmsurf.MMSURF_COLOR_BY_SOURCE_COLOR
partial_charge = mmsurf.MMSURF_COLOR_BY_PARTIAL_CHARGE
atom_type = mmsurf.MMSURF_COLOR_BY_ATOM_TYPE
atom_typeMM = mmsurf.MMSURF_COLOR_BY_ATOM_TYPE_MM
chain_name = mmsurf.MMSURF_COLOR_BY_CHAIN_NAME
element = mmsurf.MMSURF_COLOR_BY_ELEMENT
mol_number = mmsurf.MMSURF_COLOR_BY_MOL_NUMBER
mol_number_carbon = mmsurf.MMSURF_COLOR_BY_MOL_NUMBER_CARBON
residue_charge = mmsurf.MMSURF_COLOR_BY_RESIDUE_CHARGE
residue_hydrophobicity = mmsurf.MMSURF_COLOR_BY_RESIDUE_HYDROPHOBICITY
residue_position = mmsurf.MMSURF_COLOR_BY_RESIDUE_POSITION
residue_type = mmsurf.MMSURF_COLOR_BY_RESIDUE_TYPE
grid_property = mmsurf.MMSURF_COLOR_BY_GRID_PROPERTY
atom_color = mmsurf.MMSURF_COLOR_BY_ATOM_COLOR
cavity_depth = mmsurf.MMSURF_COLOR_BY_CAVITY_DEPTH
[docs]class MolSurfType(enum.IntEnum):
"""
Types of molecular surfaces.
"""
vdw = mmsurf.MOLSURF_VDW
extended = mmsurf.MOLSURF_EXTENDED
molecular = mmsurf.MOLSURF_MOLECULAR
@decorator.decorator
def _requires_update(func, self, *args, **kwargs):
"""
A decorator for `Surface` methods that update the visual representation of
the surface. This decorator, tells Maestro to update the workspace surface
representation and ensures that we only force one update even if a decorated
method calls another decorated method.
"""
do_update = not self._force_update
self._force_update = True
retval = func(self, *args, **kwargs)
if do_update:
self._updateMaestro()
self._force_update = False
return retval
[docs]class Surface(object):
"""
A Pythonic wrapping for mmsurf surfaces that are not associated with a
project entry. (For surfaces that are associated with a project entry, see
`ProjectSurface` below.) Surface objects can be created from an existing
mmsurf handle via `__init__`, can be read from disk via `read`, or new
surfaces can be created via `newMolecularSurface`.
"""
Style = Style
ColorBy = ColorBy
ColorFrom = ColorFrom
Color = color.Color
SURFACE_TYPE_NAME = {
MolSurfType.vdw: "van der Waals",
MolSurfType.extended: "extended radius",
MolSurfType.molecular: "molecular surface"
}
[docs] def __init__(self, handle, manage=True):
"""
:param handle: An mmsurf handle to an existing surface
:type handle: int
:param manage: If True, the mmsurf handle will be deleted when this
object is garbage collected.
:type manage: bool
"""
self._initializeMmlibs()
self._handle = handle
self._manage = manage
# Required for compatibility with _requires_update, which is required by
# ProjectSurface
self._force_update = True
@staticmethod
def _initializeMmlibs():
"""
Initialize all mmlib libraries used by this class.
"""
mmsurf.mmsurf_initialize(mm.MMERR_DEFAULT_HANDLER)
mmsurf.mmvol_initialize(mm.MMERR_DEFAULT_HANDLER)
mmsurf.mmvisio_initialize(mm.MMERR_DEFAULT_HANDLER)
@staticmethod
def _terminateMmlibs():
"""
Terminate all mmlib libraries used by this class.
"""
mmsurf.mmsurf_terminate()
mmsurf.mmvol_terminate()
mmsurf.mmvisio_terminate()
def __del__(self):
"""
When this object is garbage collected, terminate the mmlib libraries and
delete the mmsurf handle if it's managed by this object.
"""
if self._manage:
self.delete()
self._terminateMmlibs()
[docs] def delete(self):
"""
Immediately delete the mmsurf handle. After this method has been
called, any further attempts to interact with this object will result in
an MmException.
"""
mmsurf.mmsurf_delete(self._handle)
self._handle = -1
[docs] @classmethod
def newMolecularSurface(cls,
struc,
name,
asl=None,
atoms=None,
resolution=0.5,
probe_radius=None,
vdw_scaling=1.0,
mol_surf_type=MolSurfType.molecular):
"""
Create a new molecular surface for the specified surface
:param struc: The structure to create the surface for
:type proj: `schrodinger.structure.Structure`
:param name: The name of the surface.
:type name: str
:param asl: If given, the surface will only be created for atoms in the
structure that match the provided ASL. Note that only one of `asl` and
`atoms` may be given. If neither are given, then the surface will be
created for all atoms in the structure.
:type asl: str or NoneType
:param atoms: An optional list of atom numbers. If given, the surface
will only be created for the specified atoms. Note that only one of
`asl` and `atoms` may be given. If neither are given, then the
surface will be created for all atoms in the structure.
:type atoms: list or NoneType
:param resolution: The resolution of the surface, generally between
0 and 1. Smaller numbers lead to a more highly detailed surface.
:type resolution: float
:param probe_radius: The radius of the rolling sphere used to calculate
the surface. Defaults to 1.4 if `mol_surf_type` is
`MolSurfType.Molecular` or `MolSurfType.Extended`. May not be given
if `mol_surf_type` is `MolSurfType.vdw`.
:type probe_radius: float
:param vdw_scaling: If given, all atomic radii will be scaled by the
provided value before the surface is calculated.
:type vdw_scaling: float
:param mol_surf_type: The type of surface to create.
:type mol_surf_type: `MolSurfType`
:return: The new surface
:rtype: `Surface`
"""
mmsurf.mmsurf_initialize(mm.MMERR_DEFAULT_HANDLER)
handle = cls._createMolecularSurface(struc, name, asl, atoms,
resolution, probe_radius,
vdw_scaling, mol_surf_type)
surf = cls(handle)
mmsurf.mmsurf_terminate()
return surf
@classmethod
def _createMolecularSurface(cls, struc, name, asl, atoms, resolution,
probe_radius, vdw_scaling, mol_surf_type):
"""
Create a new molecular surface for the specified structure. Arguments
are the same as `newMolecularSurface` above.
:return: An mmsurf handle for the new surface
:rtype: int
"""
if probe_radius is None:
if mol_surf_type in (MolSurfType.extended, MolSurfType.molecular):
probe_radius = 1.4
elif mol_surf_type is MolSurfType.vdw:
probe_radius = 0
elif mol_surf_type is MolSurfType.vdw:
raise ValueError("May not give probe_radius for vdw surfaces.")
bs = cls._generateBitset(struc, asl, atoms)
# See mmshare/mmlibs/mmsurf/mmsurf.h for documentation on the additional
# mmsurf_molsurf() arguments
handle = mmsurf.mmsurf_molsurf(struc, bs, resolution, probe_radius,
vdw_scaling, mol_surf_type, 0)
mmsurf.mmsurf_set_name(handle, name)
surf_type = cls.SURFACE_TYPE_NAME[mol_surf_type]
mmsurf.mmsurf_set_surface_type(handle, surf_type)
return handle
@staticmethod
def _generateBitset(struc, asl, atoms):
"""
Generate a bitself that indicates which atoms to create the surface for.
Note that either `asl` or `atoms` (or neither) may be provided, but
not both. If neither are provided, the bitset will cover all atoms in
`structure`.
:param struc: The structure to generate the bitset for
:type struc: `schrodinger.structure.Structure`
:param asl: If not None, the bitset will contain only atoms that match
this ASL.
:type asl: str or NoneType
:param atoms: If not None, a list of atom numbers. The bitset will
contain only the listed atoms.
:type atoms: list or NoneType
:return: The newly generated bitset
:rtype: `mmbitset.Bitset`
"""
if asl is not None and atoms is not None:
raise ValueError("May not specify both asl and atoms.")
bs = mmbitset.Bitset(size=struc.atom_total)
if asl is not None:
atoms = analyze.evaluate_asl(struc, asl)
if atoms is None:
bs.fill()
elif not atoms:
raise ValueError("No atoms specified.")
else:
list(map(bs.set, atoms))
return bs
[docs] @classmethod
def read(cls, filename):
"""
Read surface data from a file.
:param filename: The file to read from.
:type filename: str
:return: The read surface.
:rtype: `Surface`
"""
mmsurf.mmvisio_initialize(mm.MMERR_DEFAULT_HANDLER)
handle = mmsurf.mmvisio_read_surface_from_file(filename)
surf = cls(handle)
mmsurf.mmvisio_terminate()
return surf
[docs] def write(self, filename):
"""
Write this surface to a file. Note that existing files will be
overwritten.
:param filename: The file to write to.
:type filename: str
"""
# We get strange HDF5-DIAG errors if we attempt to overwrite an existing
# file, so make sure we delete any existing file first
fileutils.force_remove(filename)
mmsurf.mmvisio_write_surface_to_file(filename, self._handle)
@property
def name(self):
"""
The surface name.
:type: str
"""
return mmsurf.mmsurf_get_name(self._handle)
@name.setter
def name(self, value):
mmsurf.mmsurf_set_name(self._handle, value)
[docs] def rename(self, value):
"""
Set the surface name. This method is provided for compatibility with
`ProjectSurface`.
"""
# For compatibility with ProjectSurface
self.name = value
@property
def volume_name(self):
"""
The volume name associated with the given surface
:type: str
"""
return mmsurf.mmsurf_get_volume_name(self._handle)
@volume_name.setter
def volume_name(self, value):
mmsurf.mmsurf_set_volume_name(self._handle, value)
@property
def isovalue(self):
"""
The isovalue for the given surface
:type: float
"""
return mmsurf.mmsurf_get_isovalue(self._handle)
@isovalue.setter
def isovalue(self, value):
mmsurf.mmsurf_set_isovalue(self._handle, value)
@property
def surface_type(self):
"""
A textual description of the type of surface.
:type: str
"""
return mmsurf.mmsurf_get_surface_type(self._handle)
@surface_type.setter
def surface_type(self, val):
mmsurf.mmsurf_set_surface_type(self._handle, val)
@property
def visible(self):
"""
Whether the surface is currently visible. This setting will be
remembered, but it will not have any effect until the surface is added
to a project and loaded into Maestro.
:type: bool
"""
vis = mmsurf.mmsurf_get_visibility(self._handle)
return bool(vis)
@visible.setter
@_requires_update
def visible(self, val):
val = int(bool(val))
mmsurf.mmsurf_set_visibility(self._handle, val)
[docs] def show(self):
"""
Sets the surface to be visible.
"""
self.visible = True
[docs] def hide(self):
"""
Hides the surface.
"""
self.visible = False
@property
def front_transparency(self):
"""
The transparency of the front of the surface (relative to the workspace
camera position). Measured on a scale from 0 (fully opaque) to 100
(fully transparent).
:type: int
"""
return mmsurf.mmsurf_get_transparency(self._handle)
@front_transparency.setter
@_requires_update
def front_transparency(self, val):
mmsurf.mmsurf_set_transparency(self._handle, val)
@property
def back_transparency(self):
"""
The transparency of the back of the surface (relative to the workspace
camera position). Measured on a scale from 0 (fully opaque) to 100
(fully transparent).
:type: int
"""
return mmsurf.mmsurf_get_transparency_back(self._handle)
@back_transparency.setter
@_requires_update
def back_transparency(self, val):
mmsurf.mmsurf_set_transparency_back(self._handle, val)
[docs] @_requires_update
def setTransparency(self, val):
"""
Set both the front and the back transparency.
:param val: The value to set the transparency to
:type val: int
"""
self.front_transparency = val
self.back_transparency = val
@property
def style(self):
"""
The visual style of the surface representation (solid, mesh, or dot).
:type: `Style`
"""
style = mmsurf.mmsurf_get_style(self._handle)
return Style(style)
@style.setter
@_requires_update
def style(self, val):
val = int(val)
mmsurf.mmsurf_set_style(self._handle, val)
@property
def darken_colors_by_cavity_depth(self):
"""
Whether the colors on the surface should be darkened based on the cavity
depth.
:type: bool
"""
val = mmsurf.mmsurf_get_darken_colors_by_cavity_depth(self._handle)
return bool(val)
@darken_colors_by_cavity_depth.setter
@_requires_update
def darken_colors_by_cavity_depth(self, val):
val = bool(val)
mmsurf.mmsurf_set_darken_colors_by_cavity_depth(self._handle, val)
@property
def color_source(self):
"""
The source of the surface colors. Note that coloring()/setColoring()
are recommended over directly manipulating `color_source`, as this will
ensure that `color_source` is set correctly.
:type: `ColorFrom`
"""
val = mmsurf.mmsurf_get_color_source(self._handle)
return ColorFrom(val)
@color_source.setter
@_requires_update
def color_source(self, val):
val = int(val)
mmsurf.mmsurf_set_color_source(self._handle, val)
@property
def color_scheme(self):
"""
The color scheme used to determine surface colors. This value may be
ignored unless `color_source` is set to `ColorFrom.NearestAslAtom`.
Note that coloring()/setColoring() are recommended over directly
manipulating `color_scheme`, as this will ensure that `color_source`
is set correctly.
:type: `ColorBy`
"""
val = mmsurf.mmsurf_get_color_scheme(self._handle)
return ColorBy(val)
@color_scheme.setter
@_requires_update
def color_scheme(self, val):
# color by
val = str(val)
mmsurf.mmsurf_set_color_scheme(self._handle, val)
@property
def color(self):
"""
The constant surface color. This value may be ignored unless
`color_source` is set to `ColorFrom.Surface` and `color_scheme` is
set to `ColorBy.SourceColor`. Note that coloring()/setColoring() are
recommended over directly manipulating `color`, as this will ensure
that `color_source` and `color_scheme` are set correctly.
:type: `Color`
"""
val = mmsurf.mmsurf_get_rgb_color(self._handle)
return color.Color(val)
@color.setter
@_requires_update
def color(self, val):
if isinstance(val, color.Color):
mmsurf.mmsurf_set_rgb_color(self._handle, val.rgb)
else:
mmsurf.mmsurf_set_color(self._handle, val)
[docs] @_requires_update
def setColoring(self, coloring):
"""
Set the surface coloring. Must be one of:
- A `ColorBy` value other than `ColorBy.SourceColor` to color based
on the nearest atom
- A `Color` value for constant coloring
- A list or numpy array containing a color for each vertex
"""
if (isinstance(coloring, ColorBy) and
coloring is not ColorBy.source_color):
# Set the color source so that the color scheme is obeyed
self.color_source = ColorFrom.nearest_asl_atom
self.color_scheme = coloring
elif isinstance(coloring, color.Color):
self.color_source = ColorFrom.surface
self.color_scheme = ColorBy.source_color
self.color = coloring
elif isinstance(coloring, (list, numpy.ndarray)):
self.color_source = ColorFrom.vertex
self.color_scheme = ColorBy.source_color
self.vertex_colors = coloring
else:
raise ValueError("Unrecognized coloring.")
[docs] def coloring(self):
"""
Return the current surface coloring. Is only guaranteed to return a
non-None value if the surface coloring was set via `setColoring`. If
the surface coloring cannot be determined, will return None.
:return: The current surface coloring
:rtype: `ColorBy`, `Color`, `numpy.ndarray`, or NoneType
"""
if (self.color_source == ColorFrom.surface and
self.color_scheme == ColorBy.source_color):
return self.color
elif (self.color_source == ColorFrom.nearest_asl_atom and
self.color_scheme != ColorBy.source_color):
return self.color_scheme
elif (self.color_source == ColorFrom.vertex and
self.color_scheme != ColorBy.source_color and
self.has_vertex_colors):
return self.vertex_colors
@property
def surface_area(self):
"""
The reported surface area of the surface
:type: float
"""
return mmsurf.mmsurf_get_surface_area(self._handle)
@property
def vertex_coords(self):
"""
A list of all vertex coordinates
:type: list
"""
return mmsurf.mmsurf_get_all_vertex_coords(self._handle)
@property
def vertex_count(self):
return mmsurf.mmsurf_get_num_vertices(self._handle)
@property
def vertex_normals(self):
"""
The normal for each vertex
:type: numpy.ndarray
"""
return mmsurf.mmsurf_get_all_vertex_normals(self._handle)
@property
def patch_count(self):
"""
The number of surface patches (i.e. triangles connecting three adjacent
vertices).
:type: int
"""
return mmsurf.mmsurf_get_num_patches(self._handle)
@property
def patch_vertices(self):
"""
A `patch_count` x 3 array containing vertex indices for each surface
patch.
:type: numpy.array
"""
return mmsurf.mmsurf_get_all_patches(self._handle)
@property
def nearest_atom_indices(self):
"""
A list of the atom indices closest to each vertex coordinate. Atom
indices are listed in a corresponding order to vertex_coords.
:type: list
"""
return mmsurf.mmsurf_get_nearest_atoms(self._handle)
def _updateMaestro(self):
"""
This method is used in `ProjectSurface` and is present here for
compatibility with the `_requires_update` decorator.
"""
# This method intentionally left blank
[docs] def smoothColors(self, colors, iterations):
"""
Given a list of vertex colors, return a list of smoothed colors. Does
not modify the surface in any way.
:param colors: A list or numpy array of the colors to smooth, where
colors are represented as either RGB or RGBA values. Note that if this
value is a numpy array, the input array will be modified in place.
:type colors: list or numpy.array
:param iterations: The number of smoothing iterations to carry out.
:type iterations: int
:return: The smoothed colors as a numpy array. If `colors` was a numpy
array, the return value will be a reference to the (modified) input
array.
:rtype: numpy.array
"""
color_len = len(colors[0])
if not isinstance(colors, numpy.ndarray):
if color_len == 3:
colors = numpy.array(colors, dtype=numpy.float32)
else:
colors = numpy.array(colors, dtype=numpy.double)
if color_len == 3:
func = mmsurf.mmsurf_smooth_colors3
elif color_len == 4:
func = mmsurf.mmsurf_smooth_colors
else:
err = ("Colors must be defined using three (r, g, b) or four "
"(r, g, b, a) terms.")
raise ValueError(err)
func(self._handle, colors, iterations)
return colors
@property
def vertex_colors(self):
"""
An array of manually specified per-vertex colors.
:type: `numpy.ndarray`
"""
return mmsurf.mmsurf_get_all_vertex_colors(self._handle)
@vertex_colors.setter
@_requires_update
def vertex_colors(self, val):
if not isinstance(val, numpy.ndarray):
val = numpy.array(val, dtype=numpy.float32)
mmsurf.mmsurf_set_all_vertex_colors(self._handle, val)
@property
def has_vertex_colors(self):
"""
Does this surface contain manually specified per-vertex colors?
:type: bool
"""
return mmsurf.mmsurf_has_vertex_colors(self._handle)
[docs] def curvatures(self, curvature_type):
"""
Return curvature values for all vertices.
:param curvature_type: mmsurf.CURVATURE_GAUSS, mmsurf.CURVATURE_MIN, mmsurf.CURVATURE_MAX, mmsurf.CURVATURE_MEAN
:type: curvature_type: mmsurf.CurvatureType enum
:rtype: numpy.array
"""
return mmsurf.mmsurf_get_curvatures(self._handle, curvature_type)
[docs] def copy(self):
"""
Create a copy of this surface. Note that this method will always return
a `Surface` object, even when a `ProjectSurface` object is copied.
:return: The copied surface
:rtype: `Surface`
"""
copy_handle = mmsurf.mmsurf_copy(self._handle)
return Surface(copy_handle)
[docs]def create_isosurface_from_grid_data(dimensions,
resolution,
origin,
isovalue,
nonzero=None,
grid=None,
surface_color=None,
name=None,
comment=None,
surface_type=None):
"""
Create a new isosurface from 3D grid data
To create a surface attached to a project entry, use::
from schrodinger import surface
from schrodinger.project.surface import ProjectSurface
pysurf = surface.create_isosurface_from_grid_data(*args, **kwargs)
projsurf = ProjectSurface.addSurfaceToProject(pysurf,
ptable,
row)
Where ptable is a Project instance and row is the desired ProjectRow
instance
:type dimensions: list
:param dimensions: The number of grid points in the X, Y and Z directions
:type resolution: list
:param resolution: The gridpoint spacing in the X, Y and Z directions
:type origin: list
:param origin: The X, Y, Z coordinates of the grid origin
:type isovalue: float
:param isovalue: The value of the isosurface
:type nonzero: iterable
:param nonzero: Each item of nonzero is an (x, y, z, value) tuple indicating
the value of the grid at the given x, y, z coordinate. All other gridpoints
are set to zero. Either nonzero or grid must be supplied but not both.
:type grid: numpy.array
:param grid: A 3-dimensional numpy.array the same size as dimensions, the
value at each point is the grid at that point. Either nonzero or grid must
be supplied but not both.
:type surface_color: str or `schrodinger.structutils.color.Color`
:param surface_color: The color of the surface. If a string, must be a color
name recognized by the Color class.
:type name: str
:param name: The name of the surface - shows in Maestro's Manage Surfaces
dialog under Surface Name
:type comment: str
:param comment: The comment for the surface - shows in Maestro's Manage
surfaces dialog under Comments
:type surface_type: str
:param surface_type: The type of the surface - shows in Maestro's Manage
Surfaces dialog under Surface Type. Note - this has nothing to do with
the molecular surface type (VDW, EXTENDED, MOLECULAR) property and is a
free text field.
:rtype: `Surface`
:return: The created isosurface
"""
Surface._initializeMmlibs()
if nonzero is None and grid is None:
raise RuntimeError('Either nonzero or grid must be given')
elif nonzero is not None and grid is not None:
raise RuntimeError('Only one of nonzero and grid can be given')
# Set the grid data for the volume the surface will be computed from
voldata = volumedata.VolumeData(N=dimensions,
resolution=resolution,
origin=origin)
if nonzero:
datapoints = voldata.getData()
for xind, yind, zind, value in nonzero:
datapoints[xind][yind][zind] = value
else:
voldata.setData(grid)
# Create infrastructure volume object
with fileutils.tempfilename(suffix='vis') as visname:
volumedataio.SaveVisFile(voldata, visname)
infravis = mmsurf.mmvisio_read_volume_from_file(visname)
# Create infrastructure surface and set properties
infrasurf = mmsurf.mmsurf_new_isosurface(infravis, isovalue, False, 0.0,
0.0, 0.0, 0.0, 0)
if comment:
mmsurf.mmsurf_set_comment(infrasurf, comment)
# Create pythonic surface and set properties
pysurf = Surface(infrasurf)
if name:
pysurf.name = name
if surface_color:
if isinstance(surface_color, str):
surface_color = color.Color(surface_color)
pysurf.setColoring(surface_color)
if surface_type:
pysurf.surface_type = surface_type
Surface._terminateMmlibs()
return pysurf