"""
Maestro 3D toruses.
The torus module allows creation and drawing of toruses. Clients draw using
Group instances not through Torus instances.
Control over the center, radius, color, resolution and opacity of a torus are
provided. See the Torus class for more info.
To draw any number of toruses, create the Torus instance and add it to a Group
instance. Then invoke the Group's draw() method.
Copyright Schrodinger, LLC. All rights reserved.
"""
# Contributors: Herc Silverstein, Scott MacHoffie, Matvey Adzhigirey, Lixing Fang
import copy
import math
import numpy
import schrodinger
from . import common
from .common import OPACITY_MAX
from .common import OPACITY_MIN
from .common import TRANSPARENCY_MAX
from .common import TRANSPARENCY_MIN
from .common import Group as TorusGroup # noqa: F401
RESOLUTION_MIN = 2
RESOLUTION_MAX = 30
RESOLUTION_DEFAULT_U = 5
RESOLUTION_DEFAULT_V = 3
TRANSPARENCY_DEFAULT = 0.0 # for backwards-compatibility
OPACITY_DEFAULT = 1.0
# Constants used to calculate bounding box:
BOUNDING_BOX_INIT_VALUE = 100000000.0
EPSILON = 0.0001
maestro = schrodinger.get_maestro()
[docs]def vector_cross(X, Y):
"""
Returns the cross product of the len 3 vectors X and Y
"""
r = numpy.array([0.0, 0.0, 0.0])
r[0] = X[1] * Y[2] - X[2] * Y[1]
r[1] = X[2] * Y[0] - X[0] * Y[2]
r[2] = X[0] * Y[1] - X[1] * Y[0]
return r
#############################################################################
# CLASSES #
#############################################################################
[docs]class TorusCore(common.Primitive):
"""
Base class for MaestroTorus.
See doc string for __init__ for details on instantiation.
Toruses should be added to a TorusGroup and drawing done via
TorusGroup. See TorusGroup documentation.
"""
_blend = None
_smooth_point = None
_cull = None
_lighting = None
_lighting_model = None
_shading_model = None
[docs] def __init__(
self,
x0=None,
y0=None,
z0=None,
x1=None,
y1=None,
z1=None,
color=None,
r=None,
g=None,
b=None, # for backwards-compatability
radius=None,
tube_radius=None,
transparency=None, # for backwards-compatability
opacity=OPACITY_DEFAULT,
resolution_u=RESOLUTION_DEFAULT_U,
resolution_v=RESOLUTION_DEFAULT_V):
"""
Constructor requires:
x0, y0, z0: coordinate specifying center of torus in Angstroms.
x1, y1, z1: coordinate specifying projected center of torus
in Angstroms.
color: One of:
Color object
Color name (string)
Tuble of (R, G, B) (each 0.0-1.0)
radius: radius of the torus in Angstroms
tube_radius: radius of the tube of the torus in Angstroms
Optional arguments:
opacity: 0.0 (transparent) through 1.0 (opaque)
Defaults to 1.0
resolution_u: 2 to 30, resolution in the U direction of the surface
Defaults to 5
resolution_v: 2 to 30, resolution in the V direction of the surface
Defaults to 3
"""
if x0 is None or y0 is None or z0 is None:
raise ValueError(
"Must specify x0, y0 and z0 values to define the torus center")
elif x1 is None or y1 is None or z1 is None:
raise ValueError(
"Must specify x1, y1 and z1 values to define the torus projected center"
)
elif radius is None:
raise ValueError("Must specify a value for radius")
elif tube_radius is None:
raise ValueError("Must specify a value for tube radius")
else:
self.x0 = x0
self.y0 = y0
self.z0 = z0
self.x1 = x1
self.y1 = y1
self.z1 = z1
self.radius = radius
self.tube_radius = tube_radius
if color is not None:
self.r, self.g, self.b = common.color_arg_to_rgb(color)
elif r is not None and g is not None and b is not None:
# for backwards-compatability
self.r = float(r)
self.g = float(g)
self.b = float(b)
else:
raise ValueError("Must specify a color")
# Clamp to range of 0.0 and 1.0, inclusive
if transparency is not None: # for backwards-compatability
if transparency < TRANSPARENCY_MIN:
self.opacity = OPACITY_MAX
elif transparency > TRANSPARENCY_MAX:
self.opacity = OPACITY_MIN
else:
self.opacity = 1.0 - transparency
else: # Use opacity
if opacity < OPACITY_MIN:
self.opacity = OPACITY_MIN
elif opacity > OPACITY_MAX:
self.opacity = OPACITY_MAX
else:
self.opacity = opacity
# Clamp the resolution in U direction
if resolution_u < RESOLUTION_MIN:
self.resolution_u = RESOLUTION_MIN
elif resolution_u > RESOLUTION_MAX:
self.resolution_u = RESOLUTION_MAX
else:
self.resolution_u = resolution_u
# Clamp the resolution in U direction
if resolution_v < RESOLUTION_MIN:
self.resolution_v = RESOLUTION_MIN
elif resolution_v > RESOLUTION_MAX:
self.resolution_v = RESOLUTION_MAX
else:
self.resolution_v = resolution_v
return
def _calculateBoundingBox(self, mat):
xyzmin = []
xyzmax = []
for k in range(6):
xyzmin.append(BOUNDING_BOX_INIT_VALUE)
xyzmax.append(-BOUNDING_BOX_INIT_VALUE)
# Two points for defining the torus axis
C = numpy.array([self.x0, self.y0, self.z0])
D = numpy.array([self.x1, self.y1, self.z1])
# CD is the vector between points C and D
CD = D - C
# We need to find bottom point A and top point B of the torus
A = C - self.tube_radius * CD
B = C + self.tube_radius * CD
# AB is the vector between points A and B
AB = B - A
# Perform the vector operation
# We have coordinates for two positions (A, B) which
# define the membrane planes. We need to figure out a vector
# perpendiclar to the vector AB and a series of points
# in the planes in order to draw a polygon
# Assign the size of the planes
l = self.radius + self.tube_radius # noqa: E741
# C is the midpoint between the two points A and B
# N is an arbitrary point we will project onto C to get
# a perpendicular vector
N = copy.copy(C)
if math.fabs(AB[0]) < EPSILON and math.fabs(AB[1]) < EPSILON:
N[0] += 10.0
elif math.fabs(AB[1]) < EPSILON and math.fabs(AB[2]) < EPSILON:
N[1] += 10.0
elif math.fabs(AB[2]) < EPSILON and math.fabs(AB[0]) < EPSILON:
N[2] += 10.0
else:
N[0] += 10.0
# CN is a vector crosses AB at C
CN = N - C
# O is the projection of N onto AB. The vector O-N perpendicular
# to A-B
dot1 = numpy.dot(AB, AB)
dot2 = numpy.dot(AB, CN)
K = dot2 / dot1
O = C + numpy.dot(K, AB) # noqa: E741
ON = common.get_normalized(N - O)
# P is perpendicular to both AB and ON
P = common.get_normalized(vector_cross(AB, ON))
# Now we have everything we need to generate the planes
plane1 = []
plane2 = []
# Op is used to generate a vertex at each corner of what will
# be drawn as a square plane. Basically it's atom1+S, atom1+U,
# atom1-S, atom1-U
op = [[l, 0.0], [0.0, l], [-l, 0.0], [0.0, -l]]
for i in range(0, 4):
plane1.append(A + op[i][0] * ON + op[i][1] * P)
plane2.append(B + op[i][0] * ON + op[i][1] * P)
tmp = [0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
for i in range(4):
for k in range(3):
tmp[k + 3] = plane1[i][k]
tmp[0] = mat[0][0] * tmp[3] + mat[0][1] * tmp[4] + mat[0][2] * tmp[
5] + mat[0][3]
tmp[1] = mat[1][0] * tmp[3] + mat[1][1] * tmp[4] + mat[1][2] * tmp[
5] + mat[1][3]
tmp[2] = mat[2][0] * tmp[3] + mat[2][1] * tmp[4] + mat[2][2] * tmp[
5] + mat[2][3]
for k in range(6):
if xyzmin[k] > tmp[k]:
xyzmin[k] = tmp[k]
if xyzmax[k] < tmp[k]:
xyzmax[k] = tmp[k]
for i in range(4):
for k in range(3):
tmp[k + 3] = plane2[i][k]
tmp[0] = mat[0][0] * tmp[3] + mat[0][1] * tmp[4] + mat[0][2] * tmp[
5] + mat[0][3]
tmp[1] = mat[1][0] * tmp[3] + mat[1][1] * tmp[4] + mat[1][2] * tmp[
5] + mat[1][3]
tmp[2] = mat[2][0] * tmp[3] + mat[2][1] * tmp[4] + mat[2][2] * tmp[
5] + mat[2][3]
for k in range(6):
if xyzmin[k] > tmp[k]:
xyzmin[k] = tmp[k]
if xyzmax[k] < tmp[k]:
xyzmax[k] = tmp[k]
return (xyzmin, xyzmax)
[docs]class MaestroTorus(common._MaestroPrimitiveMixin, TorusCore):
"""
Class to create a 3D torus in Maestro. This torus will be drawn at the
appropriate time in Maestro to interact properly with transparent objects.
Toruses should be added to a Group and drawing done via the Group. See
Group documentation.
API Example::
import schrodinger.maestro.maestro as maestro
import schrodinger.graphics3d.common as common
import schrodinger.graphics3d.torus as torus
torus_grp = common.Group()
st = maestro.workspace_get()
for bond in st.bond:
trs = torus.MaestroTorus(
x0=bond.atom1.x,
y0=bond.atom1.y,
z0=bond.atom1.z,
x1=bond.atom2.x,
y1=bond.atom2.y,
z1=bond.atom2.z,
radius=0.1,
tube_radius=0.5,
resolution_u=7,
resolution_v=5,
opacity=0.8,
color='red'
)
# Add the primative to the container.
torus_grp.add(trs)
# MaestroTorus needs to be drawn.
# No special callback like there is for Toruses is needed.
torus_grp.show()
# Hide the markers.
torus_grp.hide()
# Remove the markers and the callback.
torus_grp.clear()
maestro.workspace_draw_function_remove(torus_grp.draw)
"""
[docs] def __init__(
self,
x0=None,
y0=None,
z0=None,
x1=None,
y1=None,
z1=None,
color=None,
r=None,
g=None,
b=None, # for backwards-compatability
radius=None,
tube_radius=None,
transparency=None, # for backwards-compatability
opacity=OPACITY_DEFAULT,
resolution_u=RESOLUTION_DEFAULT_U,
resolution_v=RESOLUTION_DEFAULT_V):
"""
Constructor requires:
x0, y0, z0: coordinate specifying center of torus in Angstroms.
x1, y1, z1: coordinate specifying projected center of torus
in Angstroms.
color: One of:
Color object
Color name (string)
Tuble of (R, G, B) (each 0.0-1.0)
radius: radius of the torus in Angstroms
tube_radius: radius of the tube of the torus in Angstroms
Optional arguments:
opacity: 0.0 (transparent) through 1.0 (opaque)
Defaults to 1.0
resolution_u: 2 to 30, resolution in the U direction of the surface
Defaults to 5
resolution_v: 2 to 30, resolution in the V direction of the surface
Defaults to 3
"""
self.torus = None
self._x0 = 0
self._y0 = 0
self._z0 = 0
self._x1 = 0
self._y1 = 0
self._z1 = 0
self._radius = 0
self._tube_radius = 0
TorusCore.__init__(self, x0, y0, z0, x1, y1, z1, color, r, g, b, radius,
tube_radius, transparency, opacity, resolution_u,
resolution_v)
self.torus = maestro.create_torus(self.x0, self.y0, self.z0, self.x1,
self.y1, self.z1, self.r, self.g,
self.b, self.radius, self.tube_radius,
self.opacity, self.resolution_u,
self.resolution_v)
maestro_objects = [self.torus]
common.Primitive.__init__(self, maestro_objects)
# Helper functions
[docs] def setCoords(self):
if self.torus:
maestro.set_coords(self.torus, self._x0, self._y0, self._z0)
# Accessors
def _getX0(self):
return self._x0
def _setX0(self, value):
self._x0 = value
self.setCoords()
def _getY0(self):
return self._y0
def _setY0(self, value):
self._y0 = value
self.setCoords()
def _getZ0(self):
return self._z0
def _setZ0(self, value):
self._z0 = value
self.setCoords()
def _getX1(self):
return self._x1
def _setX1(self, value):
self._x1 = value
def _getY1(self):
return self._y1
def _setY1(self, value):
self._y1 = value
def _getZ1(self):
return self._z1
def _setZ1(self, value):
self._z1 = value
def _getRadius(self):
return self._radius
def _setRadius(self, value):
self._radius = value
if self.torus:
maestro.set_radius(self.torus, self._radius)
def _getTubeRadius(self):
return self._tube_radius
def _setTubeRadius(self, value):
self._tube_radius = value
x0 = property(_getX0, _setX0, doc="X0 coordinate")
y0 = property(_getY0, _setY0, doc="Y0 coordinate")
z0 = property(_getZ0, _setZ0, doc="Z0 coordinate")
x1 = property(_getX1, _setX1, doc="X1 coordinate")
y1 = property(_getY1, _setY1, doc="Y1 coordinate")
z1 = property(_getZ1, _setZ1, doc="Z1 coordinate")
radius = property(_getRadius, _setRadius, doc="Torus's radius")
tube_radius = property(_getTubeRadius,
_setTubeRadius,
doc="Torus's tube_radius")