import functools
import itertools
import numpy as np
from .. import math
from .internal import ShapeDecorator, ShapeAttribute
from .Lines import Lines
[docs]@ShapeDecorator
class Box(Lines):
"""A triclinic box frame.
This primitive draws a triclinic box centered at the origin. It is
specified in terms of three lattice vector lengths `Lx`, `Ly`,
`Lz` and tilt factors, defined using the `hoomd-blue schema
<https://hoomd-blue.readthedocs.io/en/stable/box.html>`_.
Rather than directly initializing via attributes, `Box` objects
can also be automatically created from box-type objects using the
:py:func:`from_box` method.
Examples::
Lx = Ly = Lz = 10
xy = xz = yz = 0
box_primitive = draw.Box(Lx=Lx, Ly=Ly, Lz=Lz, width=width, color=color)
box_tuple = (Lx, Ly, Lz, xy, xz, yz)
box_primitive = draw.Box.from_box(box_tuple)
"""
_ATTRIBUTES = Lines._ATTRIBUTES + list(
itertools.starmap(ShapeAttribute, [
('Lx', np.float32, 1, 0, False,
'Length of first box vector'),
('Ly', np.float32, 1, 0, False,
'Length of second box vector'),
('Lz', np.float32, 1, 0, False,
'Length of third box vector'),
('xy', np.float32, 0, 0, False,
'Tilt factor between the first and second box vectors'),
('xz', np.float32, 0, 0, False,
'Tilt factor between the first and third box vectors'),
('yz', np.float32, 0, 0, False,
'Tilt factor between the second and third box vectors'),
('width', np.float32, 0.01, 0, False,
'Width of box line segments'),
('color', np.float32, (0, 0, 0, 1), 1, False,
'Color, RGBA, [0, 1] for the box line segments'),
]))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# specially set these attributes since the array-broadcasting
# behavior depends on calling the property setter
if 'width' not in kwargs:
self.width = 0.01
if 'color' not in kwargs:
self.color = (0, 0, 0, 1)
# ...and set this one because it's non-default
if 'cap_mode' not in kwargs:
self.cap_mode = 1
[docs] @classmethod
def from_box(cls, box, width=0.01, color=(0, 0, 0, 1)):
"""Duck type the box from a valid input.
Boxes can be a list, dictionary, or object with attributes.
"""
try:
# Handles freud.box.Box and namedtuple
Lx = box.Lx
Ly = box.Ly
Lz = getattr(box, 'Lz', 0)
xy = getattr(box, 'xy', 0)
xz = getattr(box, 'xz', 0)
yz = getattr(box, 'yz', 0)
except AttributeError:
try:
# Handle dictionary-like
Lx = box['Lx']
Ly = box['Ly']
Lz = box.get('Lz', 0)
xy = box.get('xy', 0)
xz = box.get('xz', 0)
yz = box.get('yz', 0)
except (IndexError, KeyError, TypeError):
if not len(box) in [2, 3, 6]:
raise ValueError(
"List-like objects must have length 2, 3, or 6 to be "
"converted to a box.")
# Handle list-like
Lx = box[0]
Ly = box[1]
Lz = box[2] if len(box) > 2 else 0
xy, xz, yz = box[3:6] if len(box) == 6 else (0, 0, 0)
return cls(Lx=Lx, Ly=Ly, Lz=Lz, xy=xy, xz=xz, yz=yz, width=width, color=color)
def _update_coordinates(self, *args, **kwargs):
box_tuple = (self.Lx, self.Ly, self.Lz, self.xy, self.xz, self.yz)
fractions = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0], [0, 0, 1],
[1, 1, 0], [1, 0, 1], [0, 1, 1], [1, 1, 1]])
coordinates = math.fractions_to_coordinates(box_tuple, fractions)
start_points = coordinates[[0, 0, 0, 1, 1, 2, 2, 3, 3, 4, 5, 6]]
end_points = coordinates[[1, 2, 3, 4, 5, 4, 6, 5, 6, 7, 7, 7]]
self.start_points = start_points
self.end_points = end_points
Lx = Ly = Lz = xy = xz = yz = _update_coordinates
@property
def width(self):
return self.widths[0]
@width.setter
def width(self, value):
self.widths = np.repeat(value, 12)
@property
def color(self):
return self.colors[0]
@color.setter
def color(self, value):
self.colors = np.tile(np.atleast_2d(value), (12, 1))