Source code for sknano.core.math.vectors

# -*- coding: utf-8 -*-
"""
==============================================================================
Class for collection of `Vector`\ s (:mod:`sknano.core.math.vectors`)
==============================================================================

.. currentmodule:: sknano.core.math.vectors

"""
from __future__ import absolute_import, division, print_function
from __future__ import unicode_literals
__docformat__ = 'restructuredtext en'

from operator import attrgetter

import numpy as np

from sknano.core import UserList, TabulateMixin, minmax
from .transforms import transformation_matrix
# from sknano.core.geometric_regions import Cuboid  # , Rectangle

__all__ = ['Vectors']

operand_shape_error_msg = \
    "operands could not be broadcast together with shapes {}, {}"


[docs]class Vectors(UserList, TabulateMixin): """Container class for collection of `Vector` objects. Parameters ---------- vectors : {None, sequence, `Vectors`}, optional if not `None`, then a list of `Vector` instance objects or an existing `Vectors` instance object. """ def __init__(self, vectors=None): super().__init__(initlist=vectors) self.fmtstr = "{vectors!r}" def _tabular_data(self): """Return values for pretty tabulated output.""" begin = len('Vector(') fmt = super()._tabular_data_format_string values = list(zip(['V{}'.format(i+1) for i in range(len(self))], [fmt(vec, begin, end=-1) for vec in self])) return values, @property def __item_class__(self): return Vector def sort(self, key=attrgetter('p0', 'length'), reverse=False): super().sort(key=key, reverse=reverse) def __cast(self, other): return other.data if isinstance(other, UserList) else other def _is_valid_operand(self, other): return isinstance(other, (tuple, list, np.ndarray, self.__class__, self.__item_class__)) def _is_compatible_shape(self, other): other = np.asarray(self.__cast(other)) return np.asarray(self.data).shape[-1] == len(other) if \ other.ndim == 1 else np.asarray(self.data).shape == other.shape def _validate_operand(self, other): valid = self._is_valid_operand(other) compatible = self._is_compatible_shape(other) if not valid: raise TypeError('Invalid operand type {!r}.'.format(type(other))) if not compatible: self_shape = np.asarray(self.data).shape other_shape = np.asarray(self.__cast(other)).shape raise ValueError(operand_shape_error_msg.format(self_shape, other_shape)) def __add__(self, other): if not ((self._is_valid_operand(other) and self._is_compatible_shape(other)) or np.isscalar(other)): return NotImplemented if np.isscalar(other) or isinstance(other, self.__item_class__): data = [vec.__add__(other) for vec in self] else: other = np.asarray(self.__cast(other)) if other.ndim == 1: return self.__add__(self.__item_class__(other)) else: data = [vec.__add__(other_vec) for vec, other_vec in zip(self.data, other)] return self.__class__(data) def __radd__(self, other): if not ((self._is_valid_operand(other) and self._is_compatible_shape(other)) or np.isscalar(other)): return NotImplemented return self.__add__(self.__item_class__(other)) def __iadd__(self, other): if not ((self._is_valid_operand(other) and self._is_compatible_shape(other)) or np.isscalar(other)): return NotImplemented [self.__setitem__(i, vec.__iadd__(other)) for i, vec in enumerate(self)] return self def __sub__(self, other): if not ((self._is_valid_operand(other) and self._is_compatible_shape(other)) or np.isscalar(other)): return NotImplemented if np.isscalar(other) or isinstance(other, self.__item_class__): data = [vec.__sub__(other) for vec in self] else: other = np.asarray(self.__cast(other)) if other.ndim == 1: return self.__sub__(self.__item_class__(other)) else: data = [vec.__sub__(other_vec) for vec, other_vec in zip(self.data, other)] return self.__class__(data) def __rsub__(self, other): if not ((self._is_valid_operand(other) and self._is_compatible_shape(other)) or np.isscalar(other)): return NotImplemented return self.__add__(-self.__item_class__(other)) def __isub__(self, other): if not ((self._is_valid_operand(other) and self._is_compatible_shape(other)) or np.isscalar(other)): return NotImplemented [self.__setitem__(i, vec.__isub__(other)) for i, vec in enumerate(self)] return self def __pow__(self, other): if not np.isscalar(other): return NotImplemented data = [vec.__pow__(other) for vec in self] return self.__class__(data) # def __and__(self, other): # if not self._is_valid_operand(other): # return NotImplemented # return self.___class__([atom for atom in self.__cast(other) # if atom in self]) # __rand__ = __and__ # def __iand__(self, other): # if not self._is_valid_operand(other): # return NotImplemented # self.data -= list(atom for atom in self.__cast(other) # if atom not in self) # return self # def __or__(self, other): # if not self._is_valid_operand(other): # return NotImplemented # # data = self.data + list(atom for atom in other if atom not in self) # data = dedupe((atom for data in (self, self.__cast(other)) # for atom in data), key=attrgetter('id')) # return self.__class__(data) # __ror__ = __or__ # def __ior__(self, other): # if not self._is_valid_operand(other): # return NotImplemented # self.data += \ # list(atom for atom in self.__cast(other) if atom not in self) # return self # def __xor__(self, other): # if not self._is_valid_operand(other): # return NotImplemented # other = self.__cast(other) # return (self - other) | (other - self) # __rxor__ = __xor__ # def __ixor__(self, other): # if not self._is_valid_operand(other): # return NotImplemented # if other is self: # self.clear() # else: # other = self.__cast(other) # for atom in other: # if atom in self: # self.remove(atom) # else: # self.append(atom) # return self def __neg__(self): vecs = self.__class__([-vec for vec in self]) vecs.rezero() return vecs @property def A(self): """Return array of vectors.""" return self.asarray() @property def T(self): """Return transpose of :class:`Vectors` as an \ :class:`~numpy:numpy.ndarray`.""" return self.asarray().T @property def M(self): """Return :class:`Vectors` as a :class:`~numpy:numpy.matrix`.""" return self.asmatrix() def asarray(self): """Return :class:`Vectors` as an :class:`~numpy:numpy.ndarray`.""" return np.asarray(self.tolist()) def asmatrix(self): """Return :class:`Vectors` as a :class:`~numpy:numpy.matrix`.""" return np.asmatrix(self.tolist()) @property def x(self): """Return :math:`x` coordinates of `Vector` objects as array.""" return np.asarray([vec.x for vec in self]) @x.setter def x(self, values): self._validate_operand(values) [setattr(vec, 'x', val) for vec, val in zip(self, values)] @property def y(self): """Return :math:`y` coordinates of `Vector` objects as array.""" return np.asarray([vec.y for vec in self]) @y.setter def y(self, values): self._validate_operand(values) [setattr(vec, 'y', val) for vec, val in zip(self, values)] @property def z(self): """Return :math:`z` coordinates of `Vector` objects as array.""" return np.asarray([vec.z for vec in self]) @z.setter def z(self, values): self._validate_operand(values) [setattr(vec, 'z', val) for vec, val in zip(self, values)] @property def minmax(self): """Minimum/maximum x, y, z components. Returns ------- :class:`~python:tuple` """ return tuple(zip(minmax(self.x), minmax(self.y), minmax(self.z))) def angle(self, other): """Angles between each :class:`Vector` with `other`.""" self._validate_operand(other) if isinstance(other, self.__item_class__): return np.asarray([vec.angle(other) for vec in self]) else: return np.asarray([vec.angle(other_vec) for vec, other_vec in zip(self, self.__cast(other))]) def cross(self, other): """Cross product of :class:`Vector`\ s with `other`.""" self._validate_operand(other) if isinstance(other, self.__item_class__): return self.__class__([vec.cross(other) for vec in self]) else: return self.__class__([vec.cross(other_vec) for vec, other_vec in zip(self, self.__cast(other))]) def dot(self, other, out=None): """Dot product of :class:`Vector`\ s with `other`.""" self._validate_operand(other) if isinstance(other, self.__item_class__): return np.asarray([vec.dot(other) for vec in self]) else: return np.asarray([vec.dot(other_vec) for vec, other_vec in zip(self, self.__cast(other))]) def projection(self, other): """Vector projection of each :class:`Vector` onto `other`.""" self._validate_operand(other) if isinstance(other, self.__item_class__): return self.__class__([vec.projection(other) for vec in self]) else: return self.__class__([vec.projection(other_vec) for vec, other_vec in zip(self, self.__cast(other))]) def rejection(self, v): """Vector rejection onto `other`.""" u = self return u - self.projection(v) def normalize(self): """Normalize each :class:`Vector`.""" [vec.normalize() for vec in self] @property def norms(self): """Return `Vector` :attr:`Vector.norm`\ s as array.""" return np.asarray([v.norm for v in self]) @property def lengths(self): """Alias for :attr:`~Vectors.norms`.""" return self.norms @property def magnitudes(self): """Alias for :attr:`~Vectors.norms`.""" return self.norms def filter(self, condition, invert=False): """Filter `Vectors` by `condition`. Parameters ---------- condition : array_like, bool Boolean index array having same shape as the initial dimensions of the list of `Vectors` being indexed. invert : bool, optional If `True`, the boolean array `condition` is inverted element-wise. Returns ------- filtered_vectors : `Vectors` If `invert` is `False`, return the elements where `condition` is `True`. If `invert` is `True`, return the elements where `~condition` (i.e., numpy.invert(condition)) is `True`. """ if invert: condition = ~condition return self.__class__(vectors=np.asarray(self)[condition].tolist()) def rezero(self, epsilon=1.0e-10): """Set really really small coordinates to zero. Set all coordinates with absolute value less than epsilon to zero. Parameters ---------- epsilon : float smallest allowed absolute value of any :math:`x,y,z` component. """ [vector.rezero(epsilon=epsilon) for vector in self] def rotate(self, angle=None, axis=None, anchor_point=None, rot_point=None, from_vector=None, to_vector=None, degrees=False, transform_matrix=None, fix_anchor_points=False, verbose=False, **kwargs): """Rotate `Vector`\ s coordinates. Parameters ---------- angle : float axis : :class:`~sknano.core.math.Vector`, optional anchor_point : :class:`~sknano.core.math.Point`, optional rot_point : :class:`~sknano.core.math.Point`, optional from_vector, to_vector : :class:`~sknano.core.math.Vector`, optional degrees : bool, optional transform_matrix : :class:`~numpy:numpy.ndarray` """ if 'fix_anchor_point' in kwargs: fix_anchor_points = kwargs['fix_anchor_point'] del kwargs['fix_anchor_point'] if transform_matrix is None: transform_matrix = \ transformation_matrix(angle=angle, axis=axis, anchor_point=anchor_point, rot_point=rot_point, from_vector=from_vector, to_vector=to_vector, degrees=degrees, verbose=verbose, **kwargs) [vector.rotate(fix_anchor_point=fix_anchor_points, transform_matrix=transform_matrix) for vector in self] def scale(self): return NotImplemented def translate(self, t, fix_anchor_points=False, **kwargs): """Translate `Vector`\ s by :class:`Vector` `t`. Parameters ---------- t : :class:`Vector` fix_anchor_points : bool, optional """ [vector.translate(t, fix_anchor_point=fix_anchor_points) for vector in self] def tolist(self): """Return `Vectors` as :class:`~python:list`""" # return np.asarray([vec.tolist() for vec in self]).tolist() return [vec.tolist() for vec in self] def todict(self): """Return :class:`~python:dict` of constructor parameters.""" return dict(vectors=self.tolist())
from .vector import Vector