import numbers
import math
from typing import List, Set, Tuple, Union
import numpy as np
import clifford as cf
from . import general_exp
from . import _settings
[docs]class MultiVector(object):
"""An element of the algebra
Parameters
-------------
layout: instance of :class:`clifford.Layout`
The layout of the algebra
value : sequence of length ``layout.gaDims``
The coefficients of the base blades
dtype : numpy.dtype
The datatype to use for the multivector, if no
value was passed.
.. versionadded:: 1.1.0
Notes
------
The following operators are overloaded:
* ``A * B`` : geometric product
* ``A ^ B`` : outer product
* ``A | B`` : inner product
* ``A << B`` : left contraction
* ``~M`` : reversion
* ``M(N)`` : grade or subspace projection
* ``M[N]`` : blade projection
"""
__array_priority__ = 100
def __init__(self, layout, value=None, string=None, *, dtype: np.dtype = np.float64) -> None:
"""Constructor."""
self.layout = layout
if value is None:
if string is None:
self.value = np.zeros((self.layout.gaDims,), dtype=dtype)
else:
self.value = layout.parse_multivector(string).value
else:
self.value = np.array(value)
if self.value.shape != (self.layout.gaDims,):
raise ValueError(
"value must be a sequence of length %s" %
self.layout.gaDims)
def __array__(self) -> 'cf.MVArray':
# we are a scalar, and the only appropriate dtype is an object array
return cf.MVArray([self])
def _checkOther(self, other, coerce=True) -> Tuple['MultiVector', bool]:
"""Ensure that the other argument has the same Layout or coerce value if
necessary/requested.
_checkOther(other, coerce=True) --> newOther, isMultiVector
"""
if isinstance(other, MultiVector):
if other.layout != self.layout:
raise ValueError(
"cannot operate on MultiVectors with different Layouts")
else:
return other, True
elif isinstance(other, numbers.Number):
if coerce:
# numeric scalar
newOther = self._newMV(dtype=np.result_type(other))
newOther[()] = other
return newOther, True
else:
return other, False
else:
return other, False
def _newMV(self, newValue=None, *, dtype: np.dtype = None) -> 'MultiVector':
"""Returns a new MultiVector (or derived class instance).
"""
if newValue is None and dtype is None:
raise TypeError("Must specify either a type or value")
return self.__class__(self.layout, newValue, dtype=dtype)
# numeric special methods
# binary
[docs] def exp(self) -> 'MultiVector':
return general_exp(self)
[docs] def vee(self, other) -> 'MultiVector':
r"""
Vee product :math:`A \vee B`.
This is often defined as:
.. math::
(A \vee B)^* &= A^* \wedge B^* \\
\implies A \vee B &= (A^* \wedge B^*)^{-*}
This is very similar to the :meth:`~MultiVector.meet` function, but
always uses the dual in the full space .
Internally, this is actually implemented using the complement
functions instead, as these work in degenerate metrics like PGA too,
and are equivalent but faster in other metrics.
"""
return self.layout.MultiVector(value=self.layout.vee_func(self.value, other.value))
[docs] def __and__(self, other) -> 'MultiVector':
""" ``self & other``, an alias for :meth:`~MultiVector.vee` """
return self.vee(other)
[docs] def __mul__(self, other) -> 'MultiVector':
""" ``self * other``, the geometric product :math:`MN` """
other, mv = self._checkOther(other, coerce=False)
if mv:
newValue = self.layout.gmt_func(self.value, other.value)
else:
if isinstance(other, np.ndarray):
obj = self.__array__()
return obj*other
newValue = other * self.value
return self._newMV(newValue)
def __rmul__(self, other) -> 'MultiVector':
"""Right-hand geometric product, :math:`NM`"""
other, mv = self._checkOther(other, coerce=False)
if mv:
newValue = self.layout.gmt_func(other.value, self.value)
else:
if isinstance(other, np.ndarray):
obj = self.__array__()
return other*obj
newValue = other*self.value
return self._newMV(newValue)
[docs] def __xor__(self, other) -> 'MultiVector':
r""" ``self ^ other``, the Outer product :math:`M \wedge N` """
other, mv = self._checkOther(other, coerce=False)
if mv:
newValue = self.layout.omt_func(self.value, other.value)
else:
if isinstance(other, np.ndarray):
obj = self.__array__()
return obj^other
newValue = other*self.value
return self._newMV(newValue)
def __rxor__(self, other) -> 'MultiVector':
r"""Right-hand outer product, :math:`N \wedge M` """
other, mv = self._checkOther(other, coerce=False)
if mv:
newValue = self.layout.omt_func(other.value, self.value)
else:
if isinstance(other, np.ndarray):
obj = self.__array__()
return other^obj
newValue = other * self.value
return self._newMV(newValue)
[docs] def __or__(self, other) -> 'MultiVector':
r""" ``self | other``, the inner product :math:`M \cdot N` """
other, mv = self._checkOther(other)
if mv:
newValue = self.layout.imt_func(self.value, other.value)
else:
if isinstance(other, np.ndarray):
obj = self.__array__()
return obj|other
# l * M = M * l = 0 for scalar l
return self._newMV(dtype=np.result_type(self.value.dtype, other))
return self._newMV(newValue)
__ror__ = __or__
[docs] def __add__(self, other) -> 'MultiVector':
""" ``self + other``, addition """
other, mv = self._checkOther(other)
if not mv:
if isinstance(other, np.ndarray):
obj = self.__array__()
return obj + other
newValue = self.value + other.value
return self._newMV(newValue)
__radd__ = __add__
[docs] def __sub__(self, other) -> 'MultiVector':
""" ``self - other``, Subtraction """
other, mv = self._checkOther(other)
if not mv:
if isinstance(other, np.ndarray):
obj = self.__array__()
return obj - other
newValue = self.value - other.value
return self._newMV(newValue)
def __rsub__(self, other) -> 'MultiVector':
"""Right-hand subtraction
N - M
"""
other, mv = self._checkOther(other)
if not mv:
if isinstance(other, np.ndarray):
obj = self.__array__()
return other - obj
newValue = other.value - self.value
return self._newMV(newValue)
[docs] def right_complement(self) -> 'MultiVector':
return self.layout.MultiVector(value=self.layout.right_complement_func(self.value))
[docs] def left_complement(self) -> 'MultiVector':
return self.layout.MultiVector(value=self.layout.left_complement_func(self.value))
def __truediv__(self, other) -> 'MultiVector':
"""Division, :math:`M N^{-1}`"""
other, mv = self._checkOther(other, coerce=False)
if mv:
return self * other.inv()
else:
if isinstance(other, np.ndarray):
obj = self.__array__()
return obj/other
newValue = self.value / other
return self._newMV(newValue)
def __rtruediv__(self, other) -> 'MultiVector':
"""Right-hand division, :math:`N M^{-1}`"""
other, mv = self._checkOther(other)
if isinstance(other, np.ndarray):
obj = self.__array__()
return other / obj
return other * self.inv()
def __pow__(self, other) -> 'MultiVector':
"""Exponentiation of a multivector by an integer, :math:`M^{n}` """
if not isinstance(other, (int, float)):
raise ValueError("exponent must be a Python int or float")
if abs(round(other) - other) > _settings._eps:
raise ValueError("exponent must have no fractional part")
other = int(round(other))
if other == 0:
return self._newMV(dtype=self.value.dtype) + 1
newMV = self._newMV(np.array(self.value)) # copy
for i in range(1, other):
newMV = newMV * self
return newMV
def __rpow__(self, other) -> 'MultiVector':
"""Exponentiation of a real by a multivector, :math:`r^{M}`"""
# Let math.log() check that other is a Python number, not something
# else.
# pow(x, y) == exp(y * log(x))
newMV = general_exp(math.log(other) * self)
return newMV
def __lshift__(self, other) -> 'MultiVector':
"""
The ``<<`` operator is the left contraction
"""
return self.lc(other)
# unary
def __neg__(self) -> 'MultiVector':
"""Negation, :math:`-M`"""
newValue = -self.value
return self._newMV(newValue)
[docs] def as_array(self) -> np.ndarray:
return self.value
def __pos__(self) -> 'MultiVector':
"""Positive (just a copy), :math:`+M` """
newValue = self.value + 0 # copy
return self._newMV(newValue)
[docs] def mag2(self) -> numbers.Number:
"""Magnitude (modulus) squared, :math:`{|M|}^2`
Note in mixed signature spaces this may be negative
"""
mv_val = self.layout.gmt_func(self.layout.adjoint_func(self.value), self.value)
return mv_val[0]
def __abs__(self) -> numbers.Number:
"""Magnitude (modulus), :math::`|M|`
This is ``sqrt(abs(~M*M))``.
The abs inside the sqrt is need for spaces of mixed signature
"""
return np.sqrt(abs(self.mag2()))
[docs] def adjoint(self) -> 'MultiVector':
r"""Adjoint / reversion, :math:`\tilde M`
Aliased as ``~M`` to reflect :math:`\tilde M`, one of several
conflicting notations.
Note that ``~(N * M) == ~M * ~N``.
"""
# The multivector created by reversing all multiplications
return self._newMV(self.layout.adjoint_func(self.value))
__invert__ = adjoint
# builtin
def __int__(self) -> int:
"""Coerce to an integer iff scalar.
"""
return int(self.__float__())
def __float__(self) -> float:
""""Coerce to a float iff scalar.
"""
if self.isScalar():
return float(self[()])
else:
raise ValueError("non-scalar coefficients are non-zero")
# sequence special methods
def __len__(self) -> int:
"""Returns length of value array.
"""
return self.layout.gaDims
[docs] def __getitem__(self, key: Union['MultiVector', tuple, int]) -> numbers.Number:
"""
``value = self[key]``.
If key is a blade tuple (e.g. ``(0, 1)`` or ``(1, 3)``), or a blade,
(e.g. ``e12``), then return the (real) value of that blade's coefficient.
Otherwise, treat key as an index into the list of coefficients.
"""
if isinstance(key, MultiVector):
inds, = np.nonzero(key.value)
if len(inds) > 1:
raise ValueError("Must be a single basis element")
return self.value[inds[0]]
elif isinstance(key, tuple):
sign, idx = self.layout._sign_and_index_from_tuple(key)
return sign*self.value[idx]
return self.value[key]
def __setitem__(self, key: Union[tuple, int], value: numbers.Number) -> None:
"""
Implements ``self[key] = value``.
If key is a blade tuple (e.g. (0, 1) or (1, 3)), then set
the (real) value of that blade's coeficient.
Otherwise treat key as an index into the list of coefficients.
"""
if isinstance(key, tuple):
sign, idx = self.layout._sign_and_index_from_tuple(key)
self.value[idx] = sign*value
else:
self.value[key] = value
# grade projection
[docs] def __call__(self, other, *others) -> 'MultiVector':
r"""Return a new multi-vector projected onto a grade or another MultiVector
``M(g1, ... gn)`` gives :math:`\left<M\right>_{g1} + \cdots + \left<M\right>_{gn}`
``M(N)`` calls :meth:`project` as ``N.project(M)``.
Examples
--------
>>> from clifford.g2 import *
>>> M = 1 + 2*e1 + 3*e12
>>> M(0)
1
>>> M(0, 2)
1 + (3^e12)
"""
if isinstance(other, MultiVector):
return other.project(self)
else:
# we are making a grade projection
grade = other
if len(others) != 0:
return sum([self.__call__(k) for k in (other,)+others])
if grade not in self.layout.gradeList:
raise ValueError("algebra does not have grade %s" % grade)
if not np.issubdtype(type(grade), np.integer):
raise ValueError("grade must be an integer")
mask = np.equal(grade, self.layout.gradeList)
newValue = np.multiply(mask, self.value)
return self._newMV(newValue)
# fundamental special methods
def __str__(self) -> str:
"""Return pretty-printed representation.
"""
s = ''
p = _settings._print_precision
for grade, name, coeff in zip(self.layout.gradeList, self.layout.names, self.value):
# if we have nothing yet, don't use + and - as operators but
# use - as an unary prefix if necessary
if s:
seps = (' + ', ' - ')
else:
seps = ('', '-')
# note: these comparisons need to ensure nan is shown, noting that
# `nan {} x` is always false for all comparisons `{}`.`
if abs(coeff) < _settings._eps:
continue # too small to print
else:
if coeff < 0:
sep = seps[1]
abs_coeff = -round(coeff, p)
else:
sep = seps[0]
abs_coeff = round(coeff, p)
if grade == 0:
# scalar
s = '%s%s%s' % (s, sep, abs_coeff)
else:
# not a scalar
s = '%s%s(%s^%s)' % (s, sep, abs_coeff, name)
if s:
# non-zero
return s
else:
# return scalar 0
return '0'
def __repr__(self) -> str:
"""Return eval-able representation if global _pretty is false.
Otherwise, return str(self).
"""
if _settings._pretty:
return self.__str__()
if self.value.dtype != np.float64:
dtype_str = ", dtype={}".format(self.value.dtype)
else:
dtype_str = None
if hasattr(self.layout, '__name__') and '__module__' in self.layout.__dict__:
fmt = "{l.__module__}.{l.__name__}.MultiVector({v!r}{d})"
else:
fmt = "{l!r}.MultiVector({v!r}{d})"
return fmt.format(l=self.layout, v=list(self.value), d=dtype_str)
def _repr_pretty_(self, p, cycle):
if cycle:
raise RuntimeError("Should not be cyclic")
if _settings._pretty:
p.text(str(self))
return
if hasattr(self.layout, '__name__') and '__module__' in self.layout.__dict__:
prefix = "{l.__module__}.{l.__name__}.MultiVector(".format(l=self.layout)
include_layout = False
else:
include_layout = True
prefix = "MultiVector("
with p.group(len(prefix), prefix, ")"):
if include_layout:
p.pretty(self.layout)
p.text(",")
p.breakable()
p.text(repr(list(self.value)))
if self.value.dtype != np.float64:
p.text(",")
p.breakable()
p.text("dtype={}".format(self.value.dtype))
def __bool__(self) -> bool:
"""Instance is nonzero iff at least one of the coefficients is nonzero.
"""
zeroes = np.absolute(self.value) < _settings._eps
return not zeroes.all()
def __eq__(self, other) -> bool:
other, mv = self._checkOther(other)
if not mv:
return NotImplemented
if (np.absolute(self.value - other.value) < _settings._eps).all():
# equal within epsilon
return True
else:
return False
[docs] def clean(self, eps=None) -> 'MultiVector':
"""Sets coefficients whose absolute value is < eps to exactly 0.
eps defaults to the current value of the global _settings._eps.
"""
if eps is None:
eps = _settings._eps
mask = np.absolute(self.value) > eps
# note element-wise multiplication
self.value = mask * self.value
return self
[docs] def round(self, eps=None) -> 'MultiVector':
"""Rounds all coefficients according to Python's rounding rules.
eps defaults to the current value of the global _settings._eps.
"""
if eps is None:
eps = _settings._eps
self.value = np.around(self.value, eps)
return self
# Geometric Algebraic functions
[docs] def lc(self, other) -> 'MultiVector':
r"""The left-contraction of two multivectors, :math:`M\rfloor N`"""
other, mv = self._checkOther(other, coerce=True)
newValue = self.layout.lcmt_func(self.value, other.value)
return self._newMV(newValue)
@property
def pseudoScalar(self) -> 'MultiVector':
"Returns a MultiVector that is the pseudoscalar of this space."
return self.layout.pseudoScalar
I = pseudoScalar
[docs] def invPS(self) -> 'MultiVector':
"Returns the inverse of the pseudoscalar of the algebra."
ps = self.pseudoScalar
return ps.inv()
[docs] def isScalar(self) -> bool:
"""Returns true iff self is a scalar.
"""
indices = list(range(self.layout.gaDims))
indices.remove(self.layout.gradeList.index(0))
for i in indices:
if abs(self.value[i]) < _settings._eps:
continue
else:
return False
return True
[docs] def isBlade(self) -> bool:
"""Returns true if multivector is a blade.
"""
if len(self.grades()) != 1:
return False
return self.isVersor()
[docs] def isVersor(self) -> bool:
"""Returns true if multivector is a versor.
From Leo Dorsts GA for computer science section 21.5, definition from 7.6.4
"""
Vhat = self.gradeInvol()
Vrev = ~self
Vinv = Vrev/(self*Vrev)[0]
# Test if the versor inverse (~V)/(V * ~V) is truly the inverse of the
# multivector V
if (Vhat*Vinv).grades(eps=0.000001) != {0}:
return False
if not np.sum(np.abs((Vhat*Vinv).value - (Vinv*Vhat).value)) < 0.0001:
return False
# applying a versor (and hence an invertible blade) to a vector should
# not change the grade
if not all(
(Vhat*e*Vrev).grades(eps=0.000001) == {1}
for e in cf.basis_vectors(self.layout).values()
):
return False
return True
[docs] def grades(self, eps=None) -> Set[int]:
"""Return the grades contained in the multivector.
.. versionchanged:: 1.1.0
Now returns a set instead of a list
.. versionchanged:: 1.3.0
Accepts an `eps` argument
"""
if eps is None:
eps = _settings._eps
nonzero = abs(self.value) > eps
return {
grade_i
for grade_i, nonzero_i in zip(self.layout.gradeList, nonzero)
if nonzero_i
}
@property
def blades_list(self) -> List['MultiVector']:
'''
ordered list of blades present in this MV
'''
blades_list = self.layout.blades_list
value = self.value
b = [value[0]] + [value[k]*blades_list[k] for k in range(1, len(self))]
return [k for k in b if k != 0]
[docs] def normal(self) -> 'MultiVector':
r"""Return the (mostly) normalized multivector.
The _mostly_ comes from the fact that some multivectors have a
negative squared-magnitude. So, without introducing formally
imaginary numbers, we can only fix the normalized multivector's
magnitude to +-1.
:math:`\frac{M}{|M|}` up to a sign
"""
return self / abs(self)
[docs] def leftLaInv(self) -> 'MultiVector':
"""Return left-inverse using a computational linear algebra method
proposed by Christian Perwass.
"""
return self._newMV(self.layout.inv_func(self.value))
def _pick_inv(self, fallback):
"""Internal helper to choose an appropriate inverse method.
Parameters
----------
fallback : bool, optional
If `None`, perform no checks on whether normal inv is appropriate.
If `True`, fallback to a linalg approach if necessary.
If `False`, raise an error if normal inv is not appropriate.
"""
Madjoint = ~self
MadjointM = (Madjoint * self)
if fallback is not None and not MadjointM.isScalar():
if fallback:
return self.leftLaInv()
else:
raise ValueError("no inverse exists for this multivector")
MadjointM_scalar = MadjointM[()]
if fallback is not None and not abs(MadjointM_scalar) > _settings._eps:
raise ValueError("no inverse exists for this multivector")
return Madjoint / MadjointM_scalar
[docs] def normalInv(self, check=True) -> 'MultiVector':
r"""The inverse of itself if :math:`M \tilde M = |M|^2`.
.. math::
M^{-1} = \tilde M / (M \tilde M)
Parameters
----------
check : bool
When true, the default, validate that it is appropriate to use this
method of inversion.
"""
return self._pick_inv(fallback=False if check else None)
[docs] def inv(self) -> 'MultiVector':
return self._pick_inv(fallback=True)
leftInv = leftLaInv
rightInv = leftLaInv
[docs] def dual(self, I=None) -> 'MultiVector':
r"""The dual of the multivector against the given subspace I, :math:`\tilde M = MI^{-1}`
I defaults to the pseudoscalar.
"""
if I is None:
return self.layout.MultiVector(value=self.layout.dual_func(self.value))
else:
Iinv = I.inv()
return self * Iinv
[docs] def commutator(self, other) -> 'MultiVector':
r"""The commutator product of two multivectors.
:math:`[M, N] = M \times N = (MN + NM)/2`
"""
return ((self * other) - (other * self)) / 2
x = commutator
[docs] def anticommutator(self, other) -> 'MultiVector':
"""The anti-commutator product of two multivectors, :math:`(MN + NM)/2` """
return ((self * other) + (other * self)) / 2
[docs] def gradeInvol(self) -> 'MultiVector':
r"""The grade involution of the multivector.
.. math::
M^* = \sum_{i=0}^{\text{dims}}
{(-1)^i \left<M\right>_i}
"""
signs = np.power(-1, self.layout.gradeList)
newValue = signs * self.value
return self._newMV(newValue)
@property
def even(self) -> 'MultiVector':
'''
Even part of this multivector
defined as
``M + M.gradInvol()``
'''
return .5*(self + self.gradeInvol())
@property
def odd(self) -> 'MultiVector':
'''
Odd part of this mulitvector
defined as
``M +- M.gradInvol()``
'''
return .5*(self - self.gradeInvol())
[docs] def conjugate(self) -> 'MultiVector':
"""The Clifford conjugate (reversion and grade involution).
:math:`M^*` = ``(~M).gradeInvol()``
"""
return (~self).gradeInvol()
# Subspace operations
[docs] def project(self, other) -> 'MultiVector':
r"""Projects the multivector onto the subspace represented by this blade.
:math:`P_A(M) = (M \rfloor A) A^{-1}`
"""
other, mv = self._checkOther(other, coerce=True)
if not self.isBlade():
raise ValueError("self is not a blade")
return other.lc(self) * self.inv()
[docs] def factorise(self) -> Tuple[List['MultiVector'], numbers.Number]:
"""
Factorises a blade into basis vectors and an overall scale.
Uses Leo Dorsts algorithm from 21.6 of GA for Computer Science
"""
if not self.isBlade():
raise ValueError("self is not a blade")
scale = abs(self)
max_index = np.argmax(np.abs(self.value))
B_max_factors = self.layout._index_as_tuple(max_index)
factors = []
B_c = self/scale
for ind in B_max_factors[1:]:
# get the basis vector
ei = self._newMV(dtype=B_c.value.dtype)
ei[(ind,)] = 1
fi = (ei.lc(B_c)*B_c.normalInv(check=False)).normal()
factors.append(fi)
B_c = B_c * fi.normalInv(check=False)
factors.append(B_c.normal())
factors.reverse()
return factors, scale
[docs] def basis(self) -> List['MultiVector']:
"""Finds a vector basis of this subspace.
"""
if not self.isBlade():
raise ValueError("self is not a blade")
# only one grade, since this is a blade
gr, = self.grades()
selfInv = self.inv()
selfInv.clean()
wholeBasis = [] # vector basis of the whole space
for i in range(self.layout.gaDims):
if self.layout.gradeList[i] == 1:
v = np.zeros((self.layout.gaDims,), dtype=float)
v[i] = 1.
wholeBasis.append(self._newMV(v))
thisBasis = [] # vector basis of this subspace
J, mv = self._checkOther(1.) # outer product of all of the vectors up
# to the point of iteration
for ei in wholeBasis:
Pei = ei.lc(self) * selfInv
J.clean()
J2 = J ^ Pei
if J2 != 0:
J = J2
thisBasis.append(Pei)
if len(thisBasis) == gr: # we have a complete set
break
return thisBasis
[docs] def join(self, other) -> 'MultiVector':
r"""The join of two blades, :math:`J = A \cup B`
Similar to the wedge, :math:`W = A \wedge B`, but without decaying to 0
for blades which share a vector.
"""
other, mv = self._checkOther(other)
grSelf = self.grades()
grOther = other.grades()
if not (len(grSelf) == len(grOther) == 1):
raise ValueError("not blades")
# both blades
grSelf, = grSelf
grOther, = grOther
# try the outer product first
J = self ^ other
if J != 0:
return J.normal()
# try getting the meet via the vee product
M = self & other
if M != 0:
C = M.normal()
J = (self * C.rightInv()) ^ other
return J.normal()
if grSelf >= grOther:
A = self
B = other
else:
A = other
B = self
if (A * B) == (A | B):
# B is a subspace of A or the same if grades are equal
return A.normal()
# ugly, but general way
# watch out for residues
# A is still the larger-dimensional subspace
Bbasis = B.basis()
# add the basis vectors of B one by one to the larger
# subspace except for the ones that make the outer
# product vanish
J = A
for ei in Bbasis:
J.clean()
J2 = J ^ ei
if J2 != 0:
J = J2
# for consistency's sake, we'll normalize the join
J = J.normal()
return J
[docs] def meet(self, other, subspace=None) -> 'MultiVector':
r"""The meet of two blades, :math:`A \cap B`.
Computation is done with respect to a subspace that defaults to
the :meth:`join` if none is given.
Similar to the :meth:`vee`, :math:`V = A \vee B`, but without decaying
to 0 for blades lying in the same subspace.
"""
other, mv = self._checkOther(other)
r = self.grades()
s = other.grades()
if len(r) > 1 or len(s) > 1:
raise ValueError("not blades")
if subspace is None:
subspace = self.join(other)
return (self << subspace.inv()) << other
[docs] def astype(self, *args, **kwargs):
"""
Change the underlying scalar type of this vector.
Can be used to force lower-precision floats or integers
See `np.ndarray.astype` for argument descriptions.
"""
return self._newMV(self.value.astype(*args, **kwargs))