Source code for schrodinger.graphics3d.polyhedron

"""
The polyhedron module allows creation and drawing of polyhedra.The body of the
polyhedron is composed of faces that are composed of vertices.

Control over the vertices, faces, color, and opacity of a box are provided.
However, please note that the current implementation does not ensure the
passed in vertices and faces will enclose to form a volume. This must be
determined in the subclass.

To draw any number of polyhedra, create the Polyhedron instances, add them to
a Group instance then invoke the Group's draw() method.

Copyright Schrodinger, LLC. All rights reserved.

"""

from math import sqrt

import schrodinger

from . import common
from .common import FILL
from .common import LINE
from .common import OPACITY_MAX
from .common import OPACITY_MIN
from .common import Group  # noqa: F401

maestro = schrodinger.get_maestro()

# Each polyhedron is either going to be in Maestro mode or standalone mode
MODE_MAESTRO, MODE_STANDALONE = list(range(2))

OPACITY_DEFAULT = 1.0
COLOR_DEFAULT = 'red'

PHI = (1.0 + sqrt(5.0)) / 2.0


[docs]def origin_to_point(vertices, center): """ Takes a set of vertices that have been created around the origin and translates them to the be centered around the x, y, z coordinates supplied in `center`. :param vertices: The list of vertices around the origin :type vertices: list of lists (e.g. [[x1,y1,z1],[x2,y2,z2],...] :param center: The x, y, and z coordinates to center the `vertices` on :type center: list of floats """ for i in range(len(vertices)): vertices[i] = [(v + c) for v, c in zip(vertices[i], center)] return vertices
[docs]def scale_vertices(vertices, scale): """ Scale a set of vertices. :param vertices: The list of vertices around the origin :type vertices: list of lists (e.g. [[x1,y1,z1],[x2,y2,z2],...] :param scale: The scale to apply to the vertices :type scale: float """ for i in range(len(vertices)): vertices[i] = [(v * scale) for v in vertices[i]] return vertices
[docs]class MaestroPolyhedronCore(common._MaestroPrimitiveMixin, common.Primitive):
[docs] def __init__(self, center, mode, length=None, radius=None, volume=None, color=COLOR_DEFAULT, opacity=OPACITY_DEFAULT, style=FILL): """ Create a polyhedron centered at `center`. Note that one of length, radius, or volume is needed to create the shape. :param center: List of 3 Angstrom values indicating the center coordinate of the tetrahedron. :type center: list(float) :param length: Length in Angstroms of each of the edges of the tetrahedron. :type length: float :param radius: Circumsphere radius in Angstroms from center of tetrahedron. :type radius: float :param volume: Volume in cubed Angstroms of the object. :type volume: float :param color: One of - Color object, Color name (string), or Tuple of (R, G, B) (each 0.0-1.0) :param opacity: 0.0 (invisible) through 1.0 (opaque). Defaults to 1.0 :type opacity: float :param style: LINE or FILL. Default is FILL. :type style: int :raise RuntimeError: If a single option from `length`, `radius`, and `volume` does not have a value. :raise ValueError: If the size parameter (`length`, `radius`, or `volume`) is not a float. :raise NotImplementedError: If the initiating subclass does not have a `getVertices` or `getFaces` method. """ self._checkSizeArgs(length, radius, volume) self.length = length self.radius = radius self.volume = volume self.center = center self.vertices = self.getVertices(center, length=length, radius=radius, volume=volume) self.faces = self.getFaces(self.vertices) self._setNormals() self.r, self.g, self.b = common.color_arg_to_rgb(color) # Clamp to range of 0.0 and 1.0, inclusive opacity = float(opacity) if opacity < OPACITY_MIN: self.opacity = OPACITY_MIN elif opacity > OPACITY_MAX: self.opacity = OPACITY_MAX else: self.opacity = opacity if (style != FILL and style != LINE): raise ValueError("Style must be FILL or LINE") else: self.style = style if mode != MODE_MAESTRO and mode != MODE_STANDALONE: raise ValueError("mode must be MODE_MAESTRO or MODE_STANDALONE") else: self.mode = mode if mode == MODE_MAESTRO and maestro: self.polyhedron = maestro.create_polyhedron(self.vertices, self.getIndices(), self.normals, self.r, self.g, self.b, self.opacity, self.style) maestro_objects = [self.polyhedron] common.Primitive.__init__(self, maestro_objects) else: common.Primitive.__init__(self) return
def _setNormals(self): """ Private method to set the normals based on the `self.faces` value. A `normal is created for each face in `self.faces`. This method will clear all normals and re-calculate them. """ self.normals = [] for face in self.faces: self.normals.append(common.create_normal(face)) def _calculateBoundingBox(self, mat): """ Calculate a bounding box for the polyhedron. This is used when resizing the molecule in the workspace. Returns two lists of coordinates, each of 6 elements. The first three elements of the first list are the minimum values of the rotated object, the second three elements are the minimum values of the unrotated object. The second list has the corresponding maximum values. """ i = 0 tmp = [0.0, 0.0, 0.0] + self.vertices[0] 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] xyzmin = tmp[:] xyzmax = tmp[:] for i in range(1, len(self.vertices)): tmp[3:6] = self.vertices[i][0:3] 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] def update(self, vertices, faces): """ Update the polyhedron's shape. :param vertices: List of vertices. Each member of list should be a list of 3 coords, [x,y,z] :type vertices: list of lists :param faces: List of faces comprising the polyhedron. Each face should be a list of at least 3 vertices. :type faces: list of lists :see: `Tetrahedron.updateVertices` for an example of usage """ self.vertices = vertices self.faces = faces self._setNormals()
[docs] def setStyle(self, style): """ Sets the polyhedron's drawing style. :param style: Whether to fill the polyhedron in or to leave it as lines connecting vertices. :type style: Choice, FILL or LINE """ if self.mode == MODE_MAESTRO and maestro: maestro.set_polyhedron_style(self.polyhedron, style)
def _checkSizeArgs(self, length, radius, volume): """ A private method to make sure the size options are correctly set. """ size_args = 0 for arg in (length, radius, volume): if arg: try: arg = float(arg) except ValueError: raise ValueError('The "length", "radius" and "volume" ' 'keywords must be a float.') size_args += 1 if size_args != 1: raise RuntimeError('A single size paramter must be supplied from ' '"length", "radius" or "volume".')
[docs] def getVertices(self): """Abstract method, defined by convention only""" raise NotImplementedError("%s must implement an upload method." % self.__class__.__name__)
[docs] def getIndices(self): """Abstract method, defined by convention only""" raise NotImplementedError("%s must implement a getIndices() method." % self.__class__.__name__)
[docs] def getFaces(self, vertices): """ :param vertices: List of vertices. Each member of list should be a list of 3 coords, [x,y,z] :type vertices: list of lists """ faces = [] indices = self.getIndices() for index in indices: face = [] for i in index: face.append(vertices[i]) faces.append(face) return faces
[docs] def updateVertices(self, center, length=None, radius=None, volume=None): """ Update the vertices given a new `center` and size parameter. The changes will be seen the next time the object is drawn. :param center: List of 3 Angstrom values indicating the center coordinate of the tetrahedron. :type center: list(float, float, float) :param length: Length in Angstroms of each of the edges of the tetrahedron. :type length: float :param radius: Circumsphere radius in Angstroms from center of tetrahedron. :type radius: float :param volume: Volume in cubed Angstroms of the object. :type volume: float :type radius: float :raise RuntimeError: If a single option from `length`, `radius`, and `volume` does not have a value. :raise ValueError: If the size parameter (`length`, `radius`, or `volume`) is not a float. See `Polyhedron.update` """ self._checkSizeArgs(length, radius, volume) self.vertices = self.getVertices(center, length=length, radius=radius, volume=volume) self.faces = self.getFaces(self.vertices) self.update(self.vertices, self.faces)
[docs]class MaestroCube(MaestroPolyhedronCore): """ Class to draw a 3D cube in Maestro's Workspace. Cubes should be added to a graphics3d.common.Group, or CubeGroup, and drawing done via the Group. See the `graphics3d.common.Group` documentation. API Example:: import schrodinger.maestro.maestro as maestro import schrodinger.graphics3d.polyhedron as polyhedron cube_group = polyhedron.Group() st = maestro.workspace_get() # Here, st is methane. for atom in st.atom: if atom.element == 'C': center = atom.xyz cube = polyhedron.Cube( center = center, mode = polyhedron.MODE_MAESTRO, length = 1.828, # length between Hs color = 'goldenrod', opacity = 1.0, style = polyhedron.LINE ) # Add the primative to the container. cube_group.add(cube) # Show the markers cube.show() cube_group.show() # Hide the markers. cube_group.hide() # Remove the markers and the callback. cube_group.clear() """
[docs] def __init__(self, center, mode, length=None, radius=None, volume=None, color=COLOR_DEFAULT, opacity=OPACITY_DEFAULT, style=FILL): """ :see: `MaestroPolyhedronCore.__init__` """ MaestroPolyhedronCore.__init__(self, center, mode, length=length, radius=radius, volume=volume, color=color, opacity=opacity, style=style)
[docs] def getVertices(self, center, length=None, radius=None, volume=None): """ Get a list of vertices. If the center coordinates are considered the origin the vertices will have a base on the y-plane, a vertex on the x-axis and a vertex directly in the +z-axis from the `center`. :param center: List of 3 Angstrom values indicating the center coordinate of the tetrahedron. :type center: list(float, float, float) :param length: Length in Angstroms of each of the sides of the tetrahedron. Note: `length` or `radius` must be specified to create tetrahedron. :type length: float :param radius: Circumsphere radius in Angstroms from center of tetrahedron. Note: `length` or `radius` must be specified to create tetrahedron. :type radius: float :param volume: Volume in cubed Angstroms of the object. :type volume: float """ if length is not None: edge_length = float(length) elif radius is not None: edge_length = 2.0 * sqrt(float(radius)) / 3.0 elif volume is not None: edge_length = float(volume)**(1.0 / 3.0) # We need to scale the edge length. The base_vertices below produce # a cube with an edge length of 2 edge_length /= 2.0 # Define a cube about the origin base_vertices = [[1.0, 1.0, 1.0], [-1.0, 1.0, 1.0], [-1.0, -1.0, 1.0], [1.0, -1.0, 1.0], [1.0, 1.0, -1.0], [-1.0, 1.0, -1.0], [-1.0, -1.0, -1.0], [1.0, -1.0, -1.0]] vertices = scale_vertices(base_vertices, edge_length) return origin_to_point(vertices, center)
[docs] def getIndices(self): """ :return The indices of the faces """ indices = [[0, 1, 2, 3], [4, 7, 6, 5], [0, 4, 5, 1], [1, 5, 6, 2], [2, 6, 7, 3], [3, 7, 4, 0]] return indices
# **************************************************************************
[docs]class MaestroTetrahedron(MaestroPolyhedronCore): """ Class to draw a 3D tetrahedron in Maestro's Workspace. Tetrahedrons should be added to a graphics3d.common.Group, or TetrahedronGroup, and drawing done via the Group. See the `graphics3d.common.Group` documentation. API Example:: import schrodinger.maestro.maestro as maestro import schrodinger.graphics3d.polyhedron as polyhedron tetrahedron_grp = polyhedron.Group() st = maestro.workspace_get() # Here, st is methane. for atom in st.atom: if atom.element == 'C': center = atoms.xyz tetra = polyhedron.Tetrahedron( center = center, length = 1.828, # length between Hs color = 'goldenrod', opacity = 1.0, style = tetrahedron.LINE ) # Add the primative to the container. tetrahedron_grp.add(tetra) # Hide the markers. tetrahedron_grp.hide() # Remove the markers tetrahedron_grp.clear() """
[docs] def __init__(self, center, mode, length=None, radius=None, volume=None, color=COLOR_DEFAULT, opacity=OPACITY_DEFAULT, style=FILL): """ :see: `MaestroPolyhedronCore.__init__` """ MaestroPolyhedronCore.__init__(self, center, mode, length=length, radius=radius, volume=volume, color=color, opacity=opacity, style=style)
[docs] def getVertices(self, center, length=None, radius=None, volume=None): """ Get a list of vertices. If the center coordinates are considered the origin the vertices will have a base on the y-plane, a vertex on the x-axis and a vertex directly in the +z-axis from the `center`. :param center: List of 3 Angstrom values indicating the center coordinate of the tetrahedron. :type center: list(float, float, float) :param length: Length in Angstroms of each of the sides of the tetrahedron. Note: `length` or `radius` must be specified to create tetrahedron. :type length: float :param radius: Circumsphere radius in Angstroms from center of tetrahedron. Note: `length` or `radius` must be specified to create tetrahedron. :type radius: float :param volume: Volume in cubed Angstroms of the object. :type volume: float """ if length is not None: edge_length = float(length) elif radius is not None: edge_length = radius / sqrt(3.0 / 8.0) elif volume is not None: edge_length = (12.0 / sqrt(2.0) * float(volume))**(1.0 / 3.0) # We need to scale the edge length. The base_vertices below produce # a tetrahedron with an edge length of 2*sqrt(2) edge_length /= 2.0 * sqrt(2.0) # Define a tetrahedron about the origin base_vertices = [[1.0, 1.0, 1.0], [-1.0, -1.0, 1.0], [-1.0, 1.0, -1.0], [1.0, -1.0, -1.0]] vertices = scale_vertices(base_vertices, edge_length) return origin_to_point(vertices, center)
[docs] def getIndices(self): """ :return The indices of the faces """ indices = [[2, 1, 0], [1, 3, 0], [3, 2, 0], [2, 3, 1]] return indices
[docs]class MaestroOctahedron(MaestroPolyhedronCore): """ Class to draw a 3D octahedron in Maestro's Workspace. See `Tetrahedron` doc string for more details. """
[docs] def __init__(self, center, mode, length=None, radius=None, volume=None, color=COLOR_DEFAULT, opacity=OPACITY_DEFAULT, style=FILL): """ :see: `MaestroPolyhedronCore.__init__` """ MaestroPolyhedronCore.__init__(self, center, mode, length=length, radius=radius, volume=volume, color=color, opacity=opacity, style=style)
[docs] def getVertices(self, center, length=None, radius=None, volume=None): """ Get a list of vertices. If the center coordinates are considered the origin the vertices will have a base on the y-plane, a vertex on the x-axis and a vertex directly in the +z-axis from the `center`. :param center: List of 3 Angstrom values indicating the center coordinate of the tetrahedron. :type center: list(float, float, float) :param length: Length in Angstroms of each of the sides of the tetrahedron. Note: `length` or `radius` must be specified to create tetrahedron. :type length: float :param radius: Circumsphere radius in Angstroms from center of tetrahedron. Note: `length` or `radius` must be specified to create tetrahedron. :type radius: float :param volume: Volume in cubed Angstroms of the object. :type volume: float """ if length is not None: edge_length = float(length) elif radius is not None: edge_length = float(radius) * 2.0 / sqrt(2.0) elif volume is not None: edge_length = (3.0 * float(volume) / sqrt(2.0))**(1.0 / 3.0) # We need to scale the edge length. The base_vertices below produce # an octahedron with an edge length of sqrt(2) edge_length /= sqrt(2.0) # Define the octahedron about the origin base_vertices = [[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0], [-1.0, 0.0, 0.0], [0.0, -1.0, 0.0], [0.0, 0.0, -1.0]] vertices = scale_vertices(base_vertices, edge_length) return origin_to_point(vertices, center)
[docs] def getIndices(self): """ :return The indices of the faces """ indices = [[0, 1, 2], [0, 2, 4], [0, 4, 5], [0, 5, 1], [3, 1, 5], [3, 5, 4], [3, 4, 2], [3, 2, 1]] return indices
[docs]class MaestroDodecahedron(MaestroPolyhedronCore): """ Class to draw a 3D dodecahedron in Maestro's Workspace. See `Tetrahedron` doc string for more details. """
[docs] def __init__(self, center, mode, length=None, radius=None, volume=None, color=COLOR_DEFAULT, opacity=OPACITY_DEFAULT, style=FILL): """ :see: `MaestroPolyhedronCore.__init__` """ MaestroPolyhedronCore.__init__(self, center, mode, length=length, radius=radius, volume=volume, color=color, opacity=opacity, style=style)
[docs] def getVertices(self, center, length=None, radius=None, volume=None): """ Get a list of vertices. If the center coordinates are considered the origin the vertices will have a base on the y-plane, a vertex on the x-axis and a vertex directly in the +z-axis from the `center`. :param center: List of 3 Angstrom values indicating the center coordinate of the dodecahedron. :type center: list(float, float, float) :param length: Length in Angstroms of each of the sides of the dodecahedron. Note: `length` or `radius` must be specified to create dodecahedron. :type length: float :param radius: Circumsphere radius in Angstroms from center of dodecahedron. Note: `length` or `radius` must be specified to create dodecahedron. :type radius: float :param volume: Volume in cubed Angstroms of the object. :type volume: float """ if length is not None: edge_length = float(length) elif radius is not None: edge_length = radius / sqrt(3.0 / 8.0) elif volume is not None: edge_length = (4.0 * float(volume) / (15 + (7.0 * sqrt(15.0))))**(1.0 / 3.0) # We need to scale the edge length. The base_vertices below produce # a dodecahedron with an edge length of 2/phi edge_length /= (2.0 / PHI) # Initial dodecahedron placed at origin iPHI = 1.0 / PHI base_vertices = [ [1, 1, 1], [1, 1, -1], [1, -1, 1], [1, -1, -1], [-1, 1, 1], [-1, 1, -1], [-1, -1, 1], [-1, -1, -1], [0, iPHI, PHI], [ 0, iPHI, -PHI ], [0, -iPHI, PHI], [0, -iPHI, -PHI], [ iPHI, PHI, 0 ], [iPHI, -PHI, 0], [-iPHI, PHI, 0], [-iPHI, -PHI, 0], [PHI, 0, iPHI], [PHI, 0, -iPHI], [-PHI, 0, iPHI], [-PHI, 0, -iPHI] ] # yapf:disable # Scale the vertices by length vertices = scale_vertices(base_vertices, edge_length) return origin_to_point(vertices, center)
[docs] def getIndices(self): """ :return The indices of the faces """ indices = [ [0, 16, 17, 1, 12], [16, 2, 13, 3, 17], [3, 13, 15, 7, 11], [15, 6, 18, 19, 7], [9, 5, 14, 12, 1], [1, 17, 3, 11, 9], [16, 0, 8, 10, 2], [12, 14, 4, 8, 0], [5, 19, 18, 4, 14], [7, 19, 5, 9, 11], [15, 13, 2, 10, 6], [4, 18, 6, 10, 8], ] return indices
# **************************************************************************
[docs]class MaestroIcosahedron(MaestroPolyhedronCore): """ Class to draw a 3D icosahedron in Maestro's Workspace. See `Tetrahedron` doc string for more details. """
[docs] def __init__(self, center, mode, length=None, radius=None, volume=None, color=COLOR_DEFAULT, opacity=OPACITY_DEFAULT, style=FILL): """ :see: `MaestroPolyhedronCore.__init__` """ MaestroPolyhedronCore.__init__(self, center, mode, length=length, radius=radius, volume=volume, color=color, opacity=opacity, style=style)
[docs] def getVertices(self, center, length=None, radius=None, volume=None): """ Get a list of vertices. If the center coordinates are considered the origin the vertices will have a base on the y-plane, a vertex on the x-axis and a vertex directly in the +z-axis from the `center`. :param center: List of 3 Angstrom values indicating the center coordinate of the icosahedron. :type center: list(float, float, float) :param length: Length in Angstroms of each of the sides of the icosahedron. Note: `length` or `radius` must be specified to create icosahedron. :type length: float :param radius: Circumsphere radius in Angstroms from center of icosahedron. Note: `length` or `radius` must be specified to create icosahedron. :type radius: float :param volume: Volume in cubed Angstroms of the object. :type volume: float """ if length is not None: edge_length = float(length) elif radius is not None: edge_length = 2.0 * float(radius) / (sqrt(1 + PHI * PHI)) elif volume is not None: edge_length = (6.0 * float(volume) / (5.0 * PHI * PHI))**(1.0 / 3.0) PHI2 = PHI * PHI PHI3 = PHI2 * PHI # We need to scale the edge length. The base_vertices below produce # a dodecahedron with an edge length of 2*phi^2 edge_length /= (2.0 * PHI2) # Initial dodecahedron placed at origin base_vertices = [[PHI2, 0, PHI3], [-PHI2, 0, PHI3], [0, PHI3, PHI2], [0, -PHI3, PHI2], [PHI3, PHI2, 0], [-PHI3, PHI2, 0], [-PHI3, -PHI2, 0], [PHI3, -PHI2, 0], [0, PHI3, -PHI2], [0, -PHI3, -PHI2], [PHI2, 0, -PHI3], [-PHI2, 0, -PHI3]] # Scale the vertices by length vertices = scale_vertices(base_vertices, edge_length) return origin_to_point(vertices, center)
[docs] def getIndices(self): """ :return The indices of the faces """ indices = [[0, 1, 3], [0, 2, 1], [0, 3, 7], [0, 7, 4], [0, 4, 2], [7, 10, 4], [4, 10, 8], [4, 8, 2], [2, 8, 5], [2, 5, 1], [1, 5, 6], [1, 6, 3], [3, 6, 9], [3, 9, 7], [7, 9, 10], [11, 10, 9], [11, 8, 10], [11, 5, 8], [11, 6, 5], [11, 9, 6]] return indices