"""
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]