Source code for schrodinger.application.watermap.shapes

"""
Module containing all table functionality for the WaterMap
results GUI.

"""

#- Imports -------------------------------------------------------------------

import numpy as np

from schrodinger.graphics3d import polyhedron
from schrodinger.graphics3d import sphere
from schrodinger.structutils import color

#- Globals -------------------------------------------------------------------

WATERMAP_MATERIAL = "watermap"

ENTRY_SCHEME = color.get_color_scheme("entry")
ENTRY_SCHEME_COLORS = [
    color.Color(rule.getColorName()) for rule in ENTRY_SCHEME
]

# These thresholds are used for absolute coloring (Ev:93561):
ENTROPY_MIN_THRESHOLD = 0.0
ENTROPY_MAX_THRESHOLD = 5.0
ENTHALPY_MIN_THRESHOLD = -2.5
ENTHALPY_MAX_THRESHOLD = +2.5
FREES_MIN_THRESHOLD = ENTROPY_MIN_THRESHOLD + ENTHALPY_MIN_THRESHOLD
FREES_MAX_THRESHOLD = ENTROPY_MAX_THRESHOLD + ENTHALPY_MAX_THRESHOLD

#- Classes -------------------------------------------------------------------


[docs]class ShapeFactory(object): """ Class to create a 3D object from one of the available 3D objects in `schrodinger.graphics3d`. This class unifies the needed arguments regardless of object type. For example a `schrodinger.graphics3d.box.Box` uses the keyword `extents` but this class will convert `radius` to `extents`. """
[docs] def __init__(self, entry_id, shape_index, center, radius=0.2, color="red", resolution=20, opacity=0.8, style=1, by_entry=False): """ :param center: The x, y, z coordinates of the HS the shape will be added to. :type center: list of 3 floats :param radius: The radius of the hydration site. Default: 0.2 ("off") :type radius: float :param by_entry: Whether the shapes will be singular or different based on entry id. :type by_entry: bool """ self.entry_id = int(entry_id) self.shape_index = int(shape_index) self.center = center self.radius = float(radius) self.color = color self.resolution = resolution self.opacity = opacity self.style = style shape_map = [ ('sphere', self.sphere), ('tetrahedron', self.tetrahedron), ('box', self.box), ('octahedron', self.octahedron), ('dodecahedron', self.dodecahedron), ('icosahedron', self.icosahedron), ] # Use spheres if the "Shape by Entry" is not toggled if not by_entry: shape_idx = 0 else: shape_idx = self.shape_index % len(shape_map) self.shape_type, self.func = shape_map[shape_idx]
@property def obj(self): """ Property that returns the shape object. This needs to be set as a property and not in the init or there will be references left to the object and their `clear` method will not clear them from the workspace. """ shape_obj = self.func() shape_obj.setEntryID(str(self.entry_id)) return shape_obj @property def _volume(self): """ Private `_volume` property getter. Using the property decorator because `_volume` is never passed into this class, only `radius`. This makes it much easier to have the correct value for volume. Note the volume is always based on a sphereical hydration site. """ return (4.0 / 3.0) * np.pi * (self.radius**3) @property def _box_length(self): """ Private method to get the edge length of a box based on radius. The hydration sites in WaterMap all represent spherical information so the edge of the box should not go beyond the spherical sapce. radius of circumscribed sphere = (sqrt(3)/2) * edge length Using the property decorator because `_box_length` is never passed into this class, only `radius`. This makes it much easier to have the correct value for volume. """ return self._volume**(1.0 / 3.0) # Removed due to issues with shapes, see Ev:107523 """ def changeSize(self, radius): Change the size of the shape. :param radius: The new radius :type radius: float self.radius = radius if self.shape_type == 'sphere': self.obj.radius = radius elif self.shape_type == 'box': self.obj = self.box() # Must be a polyhedron else: self.obj.updateVertices(self.center, volume=self._volume) """
[docs] def sphere(self): """ Return a `sphere.MaestroSphere` object """ x, y, z = self.center[0], self.center[1], self.center[2] obj = sphere.MaestroSphere(x=x, y=y, z=z, radius=self.radius, resolution=self.resolution, opacity=self.opacity, color=self.color) obj.material = WATERMAP_MATERIAL return obj
[docs] def box(self): """ Return a `polyhedron.Cube` object """ obj = polyhedron.MaestroCube(self.center, polyhedron.MODE_MAESTRO, length=self._box_length, color=self.color, opacity=self.opacity, style=self.style) return obj
[docs] def tetrahedron(self): """ Return a `polyhedron.Tetrahedron` object """ # Decrease the size of the shape since it is least spherical, # making the edges quite long Ev:112063 volume = 0.65 * self._volume obj = polyhedron.MaestroTetrahedron(self.center, polyhedron.MODE_MAESTRO, volume=volume, color=self.color, opacity=self.opacity, style=self.style) return obj
[docs] def octahedron(self): """ Return a `polyhedron.Octahedron` object """ obj = polyhedron.MaestroOctahedron(self.center, polyhedron.MODE_MAESTRO, volume=self._volume, color=self.color, opacity=self.opacity, style=self.style) return obj
[docs] def dodecahedron(self): """ Return a `polyhedron.Dodecahedron` object """ obj = polyhedron.MaestroDodecahedron(self.center, polyhedron.MODE_MAESTRO, volume=self._volume, color=self.color, opacity=self.opacity, style=self.style) return obj
[docs] def icosahedron(self): """Return a `polyhedron.Icosahedron` object""" obj = polyhedron.MaestroIcosahedron(self.center, polyhedron.MODE_MAESTRO, volume=self._volume, color=self.color, opacity=self.opacity, style=self.style) return obj
[docs]def get_absolute_free_ratio(delta_dG): """ Normalize the difference in free energy to 0-1 (absolute) :return: Normalized delta_dG in range [0.0-1.0] :rtype: float """ delta_dg = np.clip(delta_dG, FREES_MIN_THRESHOLD, FREES_MAX_THRESHOLD) return ((delta_dG - FREES_MIN_THRESHOLD) / (FREES_MAX_THRESHOLD - FREES_MIN_THRESHOLD))
[docs]def get_RGB_free(free_ratio, red_blue=False): """ Get the RGB corresponding to the normalized free energy difference :param free_ratio: free energy [0.0, 1.0] :type free_ratio: float :param red_blue: Whether to use blue for more negative free ratio (instead of the default green) :type red_blue: bool """ # More positive delta G should be red: r = free_ratio # By default, more negative delta G should be green g = 1.0 - free_ratio b = 0.0 if red_blue: g, b = b, g return r, g, b
[docs]def get_absolute_enthalpy_entropy_ratio(delta_enthalpy, delta_entropy): """ Normalize the delta enthalpy and delta entropy values to 0-1 (absolute) :return: Normalized delta enthalpy and delta entropy in range [0.0-1.0] :rtype: tuple(float, float) """ delta_enthalpy = np.clip(delta_enthalpy, ENTHALPY_MIN_THRESHOLD, ENTHALPY_MAX_THRESHOLD) delta_entropy = np.clip(delta_entropy, ENTROPY_MIN_THRESHOLD, ENTROPY_MAX_THRESHOLD) enthalpyr = ((delta_enthalpy - ENTHALPY_MIN_THRESHOLD) / (ENTHALPY_MAX_THRESHOLD - ENTHALPY_MIN_THRESHOLD)) entropyr = ((delta_entropy - ENTROPY_MIN_THRESHOLD) / (ENTROPY_MAX_THRESHOLD - ENTROPY_MIN_THRESHOLD)) return enthalpyr, entropyr
[docs]def get_RGB_enthalpy_entropy(enthalpyr, entropyr): """ Returns r, g, b based on enthalpyr and entropyr. :param enthalpyr: relative enthalpy within range of (0, 1.0) :type enthalpyr: float (>= 0.0) :param entropyr: relative entropy within range of (0, 1.0) :type entropyr: float (>=0.0) """ # high enthalpyr and entropyr should be faint # low enthalpyr and entropyr should be bright # enthalpyr & entropyr range from 0.0 to 1.0 # Ev:69831 R G B # energy: cyan 0.0 1.0 1.0 # entropy: yellow 1.0 1.0 0.0 # if energy is less than entropy: # - color is between cyan and green # - R=0 # - G=higher values (darker) for higher energy (unhappy) # - B=ratio of 1-energy to sum of 1-energy and 1-entropy scaled by 1-energy # if energy is greater than entropy # - color is be between yellow and green # - R=ratio of 1-entropy to sum of 1-energy and 1-entropy scaled by 1-entropy # - G=higher values (darker) for higher entropy (unhappy) # - B=0 # lower enthalpy=brighter cyan, low entropy=brighter yellow if enthalpyr <= entropyr: r = 0.0 # lower free energy=brighter g = 1.0 - 0.3 * (enthalpyr + entropyr) entropyr = max(1.0e-100, entropyr) # prevent entropyr = 0.0 # the more enthalpy contributes to free energy, the more cyan b = 1.0 - (enthalpyr / entropyr) else: enthalpyr = max(1.0e-100, enthalpyr) # prevent enthalpyr = 0.0 # the more entropy contributes to free energy, the more yellow r = 1.0 - (entropyr / enthalpyr) # lower free energy=brighther g = 1.0 - 0.3 * (enthalpyr + entropyr) b = 0.0 return (r, g, b)
[docs]def get_color_by_entry(entry_id, all_entry_ids, *, colors=ENTRY_SCHEME_COLORS): """ Get a color from the entry color scheme relative to `entry_id`'s sorted position in `all_entry_ids` :param entry_id: The entry ID to get the color for :type entry_id: str or int :param all_entry_ids: Iterable of all entry IDs :type all_entry_ids: typing.Iterable[str | int] :param colors: Colors to use (defaults to ENTRY_SCHEME_COLORS) :type colors: typing.Sequence :return: Color item from `colors` :rtype: schrodinger.structutils.color.Color """ # Sort ids based on int value wm_ids = sorted(int(i) for i in all_entry_ids) index = wm_ids.index(int(entry_id)) % len(colors) return colors[index]