Source code for garnett.shapes

# Copyright (c) 2020 The Regents of the University of Michigan
# All rights reserved.
# This software is licensed under the BSD 3-Clause License.

"""Abstract shape definitions used to read/write particle shapes."""

import json
import logging
import numpy as np

__all__ = [
    'FallbackShape',
    'Shape',
    'SphereShape',
    'ArrowShape',
    'SphereUnionShape',
    'PolygonShape',
    'SpheropolygonShape',
    'ConvexPolyhedronShape',
    'ConvexPolyhedronUnionShape',
    'ConvexSpheropolyhedronShape',
    'GeneralPolyhedronShape',
    'EllipsoidShape',
]

logger = logging.getLogger(__name__)

SHAPE_DEFAULT_COLOR = '005984FF'


class _NumpyEncoder(json.JSONEncoder):
    """JSONEncoder class converting NumPy arrays to lists."""
    def default(self, obj):
        if isinstance(obj, np.number):
            return obj.item()
        elif isinstance(obj, np.ndarray):
            return obj.tolist()
        return json.JSONEncoder.default(self, obj)


def _json_sanitize(func):
    """Decorator ensuring that returned data is JSON-encodable."""
    def wrapper(*args, **kwargs):
        data = func(*args, **kwargs)
        return json.loads(json.dumps(data, cls=_NumpyEncoder))
    # Ensure that the decorated function inherits the intended docstring.
    wrapper.__doc__ = func.__doc__
    return wrapper


[docs]class FallbackShape(str): """This shape definition class is used when no specialized Shape class can be applied. The fallback shape definition is a string containing the definition.""" pass
[docs]class Shape(object): """Parent class of all shape objects. :param shape_class: Shape class directive, used for POS format (default: :code:`None`). :type shape_class: str :param color: Hexadecimal color string in format :code:`RRGGBBAA` (default: :code:`None`). :type color: str """ def __init__(self, shape_class=None, color=None): self.shape_class = shape_class self.color = color if color else SHAPE_DEFAULT_COLOR def __getitem__(self, key): try: return getattr(self, key) except AttributeError as e: raise KeyError(*e.args) @property def pos_string(self): return "{} {}".format(self.shape_class, self.color) @property @_json_sanitize def type_shape(self): return {"type": self.shape_class} def __str__(self): return json.dumps(self.type_shape) def __repr__(self): return str(self) def __eq__(self, other): return self.type_shape == other.type_shape
[docs]class SphereShape(Shape): """Shape class for spheres of a specified diameter. :param diameter: Diameter of the sphere. :type diameter: float :param orientable: Set to True for spheres with orientation (default: :code:`False`). :type orientable: bool :param color: Hexadecimal color string in format :code:`RRGGBBAA` (default: :code:`None`). :type color: str """ def __init__(self, diameter, orientable=False, color=None): super(SphereShape, self).__init__( shape_class='sphere', color=color) self.diameter = diameter self.orientable = orientable @property def pos_string(self): return "{} {} {}".format(self.shape_class, self.diameter, self.color) @property @_json_sanitize def type_shape(self): """Shape as dictionary. Example: >>> SphereShape(2.0).type_shape {'type': 'Sphere', 'diameter': 2.0} """ return {'type': 'Sphere', 'diameter': self.diameter}
[docs]class ArrowShape(Shape): """Shape class for arrows of a specified thickness. :param thickness: Thickness of the arrow. :type thickness: float :param color: Hexadecimal color string in format :code:`RRGGBBAA` (default: :code:`None`). :type color: str """ def __init__(self, thickness=0.1, color=None): super(ArrowShape, self).__init__( shape_class='arrow', color=color) self.thickness = thickness @property def pos_string(self): return "{} {} {}".format(self.shape_class, self.thickness, self.color)
[docs]class SphereUnionShape(Shape): """Shape class for sphere unions, such as rigid bodies of many spheres. :param diameters: List of sphere diameters. :type diameters: list :param centers: List of 3D center vectors. :type centers: list :param colors: List of hexadecimal color strings in format :code:`RRGGBBAA` (default: :code:`None`). :type colors: list """ def __init__(self, diameters, centers, colors=None): super(SphereUnionShape, self).__init__( shape_class='sphere_union', color='') self.diameters = diameters self.centers = centers self.colors = colors @property def pos_string(self): shape_def = '{} {} '.format(self.shape_class, len(self.centers)) for d, p, c in zip(self.diameters, self.centers, self.colors): shape_def += '{0} '.format(d) shape_def += '{0} {1} {2} '.format(*p) shape_def += '{0} '.format(c) return shape_def @property @_json_sanitize def type_shape(self): """Shape as dictionary. Example: >>> SphereUnionShape([0.5, 0.5, 0.5], [[0, 0, 1.0], [0, 1.0, 0], [1.0, 0, 0]]).type_shape {'type': 'SphereUnion', 'diameters': [0.5, 0.5, 0.5], 'centers': [[0, 0, 1.0], [0, 1.0, 0], [1.0, 0, 0]]} """ return {'type': 'SphereUnion', 'diameters': self.diameters, 'centers': self.centers}
[docs]class PolygonShape(Shape): """Shape class for polygons in a 2D plane. :param vertices: List of 2D vertex vectors. :type vertices: list :param color: Hexadecimal color string in format :code:`RRGGBBAA` (default: :code:`None`). :type color: str """ def __init__(self, vertices, color=None): super(PolygonShape, self).__init__( shape_class='poly3d', color=color) self.vertices = vertices @property def pos_string(self): return "{} {} {} {}".format( self.shape_class, len(self.vertices), ' '.join('{} {} 0'.format(v[0], v[1]) for v in self.vertices), self.color) @property @_json_sanitize def type_shape(self): """Shape as dictionary. Example: >>> PolygonShape([[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5]]).type_shape {'type': 'Polygon', 'rounding_radius': 0, 'vertices': [[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5]]} """ return {'type': 'Polygon', 'rounding_radius': 0, 'vertices': self.vertices}
[docs]class SpheropolygonShape(Shape): """Shape class for rounded polygons in a 2D plane. :param vertices: List of 2D vertex vectors. :type vertices: list :param rounding_radius: Rounding radius applied to the spheropolygon (default: 0). :type rounding_radius: float :param color: Hexadecimal color string in format :code:`RRGGBBAA` (default: :code:`None`). :type color: str """ def __init__(self, vertices, rounding_radius=0, color=None): super(SpheropolygonShape, self).__init__( shape_class='spoly3d', color=color) self.vertices = vertices self.rounding_radius = rounding_radius @property def pos_string(self): return "{} {} {} {} {}".format( self.shape_class, self.rounding_radius, len(self.vertices), ' '.join('{} {} 0'.format(v[0], v[1]) for v in self.vertices), self.color) @property @_json_sanitize def type_shape(self): """Shape as dictionary. Example: >>> SpheropolygonShape([[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5]], 0.1).type_shape {'type': 'Polygon', 'rounding_radius': 0.1, 'vertices': [[-0.5, -0.5], [0.5, -0.5], [0.5, 0.5]]} """ return {'type': 'Polygon', 'rounding_radius': self.rounding_radius, 'vertices': self.vertices}
[docs]class ConvexPolyhedronShape(Shape): """Shape class for convex polyhedra. :param vertices: List of 3D vertex vectors. :type vertices: list :param color: Hexadecimal color string in format :code:`RRGGBBAA` (default: :code:`None`). :type color: str """ def __init__(self, vertices, color=None): super(ConvexPolyhedronShape, self).__init__( shape_class='poly3d', color=color) self.vertices = vertices @property def pos_string(self): return "{} {} {} {}".format( self.shape_class, len(self.vertices), ' '.join((str(v) for xyz in self.vertices for v in xyz)), self.color) @property @_json_sanitize def type_shape(self): """Shape as dictionary. Example: >>> ConvexPolyhedronShape([[0.5, 0.5, 0.5], [0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [-0.5, -0.5, 0.5]]).type_shape {'type': 'ConvexPolyhedron', 'rounding_radius': 0, 'vertices': [[0.5, 0.5, 0.5], [0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [-0.5, -0.5, 0.5]]} """ return {'type': 'ConvexPolyhedron', 'rounding_radius': 0, 'vertices': self.vertices}
[docs]class ConvexPolyhedronUnionShape(Shape): """Shape class for unions of convex polyhedra. :param vertices: List of lists of 3D vertex vectors in particle coordinates (each polyhedron, each vertex). :type vertices: list :param centers: List of 3D polyhedra center vectors. :type centers: list :param orientations: Orientations of the polyhedra, as a list of quaternions. :type orientations: list :param colors: List of hexadecimal color strings in format :code:`RRGGBBAA` (default: :code:`None`). :type colors: list """ def __init__(self, vertices, centers, orientations, colors=None): super(ConvexPolyhedronUnionShape, self).__init__( shape_class='poly3d_union', color='') self.vertices = vertices self.centers = centers self.orientations = orientations self.colors = colors @property def pos_string(self): shape_def = '{} {} '.format(self.shape_class, len(self.centers)) for verts, p, q, c in zip(self.vertices, self.centers, self.orientations, self.colors): shape_def += '{0} '.format(len(verts)) for v in verts: shape_def += '{0} {1} {2} '.format(*v) shape_def += '{0} {1} {2} '.format(*p) shape_def += '{0} {1} {2} {3} '.format(*q) shape_def += '{0} '.format(c) return shape_def
[docs]class ConvexSpheropolyhedronShape(Shape): """Shape class for a convex polyhedron extended by a rounding radius. :param vertices: List of 3D vertex vectors. :type vertices: list :param rounding_radius: Rounding radius applied to the spheropolyhedron (default: 0). :type rounding_radius: float :param color: Hexadecimal color string in format :code:`RRGGBBAA` (default: :code:`None`). :type color: str """ def __init__(self, vertices, rounding_radius=0, color=None): super(ConvexSpheropolyhedronShape, self).__init__( shape_class='spoly3d', color=color) self.vertices = vertices self.rounding_radius = rounding_radius @property def pos_string(self): return "{} {} {} {} {}".format( self.shape_class, self.rounding_radius, len(self.vertices), ' '.join((str(v) for xyz in self.vertices for v in xyz)), self.color) @property @_json_sanitize def type_shape(self): """Shape as dictionary. Example: >>> ConvexSpheropolyhedronShape([[0.5, 0.5, 0.5], [0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [-0.5, -0.5, 0.5]], 0.1).type_shape {'type': 'ConvexPolyhedron', 'rounding_radius': 0.1, 'vertices': [[0.5, 0.5, 0.5], [0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [-0.5, -0.5, 0.5]]} """ return {'type': 'ConvexPolyhedron', 'rounding_radius': self.rounding_radius, 'vertices': self.vertices}
[docs]class GeneralPolyhedronShape(Shape): """Shape class for general polyhedra, such as arbitrary meshes. :param vertices: List of 3D vertex vectors. :type vertices: list :param faces: List of lists of integers representing vertex indices for each face. :type faces: list :param color: Hexadecimal color string in format :code:`RRGGBBAA` (default: :code:`None`). :type color: str :param facet_colors: List of hexadecimal color strings in format :code:`RRGGBBAA` for each facet (default: :code:`None`). :type facet_colors: list """ def __init__(self, vertices, faces, color=None, facet_colors=None): super(GeneralPolyhedronShape, self).__init__( shape_class='polyV', color=color) self.vertices = vertices self.faces = faces self.facet_colors = facet_colors @property def pos_string(self): return "{} {} {} {} {} {}".format( self.shape_class, len(self.vertices), ' '.join((str(v) for xyz in self.vertices for v in xyz)), len(self.faces), ' '.join((str(fv) for f in self.faces for fv in [len(f)]+f)), self.color) @property @_json_sanitize def type_shape(self): """Shape as dictionary. Example: >>> GeneralPolyhedronShape([[0.5, 0.5, 0.5], [0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [-0.5, -0.5, 0.5]]).type_shape {'type': 'Mesh', 'vertices': [[0.5, 0.5, 0.5], [0.5, -0.5, -0.5], [-0.5, 0.5, -0.5], [-0.5, -0.5, 0.5]], 'indices': [[0, 1, 2], [0, 1, 3], [0, 2, 3], [1, 2, 3]]} """ return {'type': 'Mesh', 'vertices': self.vertices, 'indices': self.faces}
[docs]class EllipsoidShape(Shape): """Shape class for ellipsoids of with principal axes a, b, and c. :param a: Principal axis a of the ellipsoid (radius in the x direction). :type a: float :param b: Principal axis b of the ellipsoid (radius in the y direction). :type b: float :param c: Principal axis c of the ellipsoid (radius in the z direction). :type c: float :param color: Hexadecimal color string in format :code:`RRGGBBAA` (default: :code:`None`). :type color: str """ def __init__(self, a, b, c, color=None): super(EllipsoidShape, self).__init__( shape_class='ellipsoid', color=color) self.a = a self.b = b self.c = c @property def pos_string(self): return "{} {} {} {} {}".format( self.shape_class, self.a, self.b, self.c, self.color ) @property @_json_sanitize def type_shape(self): """Shape as dictionary. Example: >>> EllipsoidShape(7.0, 5.0, 3.0).type_shape {'type': 'Ellipsoid', 'a': 7.0, 'b': 5.0, 'c': 3.0} """ return {'type': 'Ellipsoid', 'a': self.a, 'b': self.b, 'c': self.c}
def _parse_type_shape(shape): """Parses a shape object from a dictionary. This method parses the `GSD Shape Visualization Specification <https://gsd.readthedocs.io/en/stable/shapes.html>`_, while including backwards compatibility with shape definitions that do not adhere to that specification but were previously supported by HOOMD's :code:`get_type_shapes()` methods. """ if not shape: return FallbackShape('') type_name = shape['type'].lower() type_shape = None if type_name in ('sphere', 'disk'): # disk support is for backwards compatibility with get_type_shapes() # from HOOMD-blue < 2.7 diameter = shape.get('diameter', 2*shape.get('rounding_radius', 0.5)) orientable = shape.get('orientable', False) type_shape = SphereShape(diameter=diameter, orientable=orientable, color=None) elif type_name == 'ellipsoid': type_shape = EllipsoidShape(a=shape['a'], b=shape['b'], c=shape['c'], color=None) elif type_name == 'polygon': rounding_radius = shape.get('rounding_radius', 0) if rounding_radius == 0: type_shape = PolygonShape(vertices=shape['vertices'], color=None) else: type_shape = SpheropolygonShape(vertices=shape['vertices'], rounding_radius=rounding_radius, color=None) elif type_name == 'convexpolyhedron': rounding_radius = shape.get('rounding_radius', 0) if rounding_radius == 0: type_shape = ConvexPolyhedronShape(vertices=shape['vertices'], color=None) else: type_shape = ConvexSpheropolyhedronShape(vertices=shape['vertices'], rounding_radius=rounding_radius, color=None) elif type_name == 'mesh': type_shape = GeneralPolyhedronShape(vertices=shape['vertices'], faces=shape['indices'], facet_colors=shape['colors'], color=None) elif type_name == 'polyhedron': # polyhedron support is for backwards compatibility with # get_type_shapes() from HOOMD-blue < 2.7 type_shape = GeneralPolyhedronShape(vertices=shape['vertices'], faces=shape['faces'], facet_colors=shape['colors'], color=None) elif type_name == 'sphereunion': type_shape = SphereUnionShape(diameters=shape['diameters'], centers=shape['centers'], color=None) if type_shape is None: logger.warning("Failed to parse shape definition: shape {} not supported. " "Using fallback mode.".format(type_name)) type_shape = FallbackShape(json.dumps(shape)) return type_shape