#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # Copyright 2012 California Institute of Technology. ALL RIGHTS RESERVED. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # # United States Government Sponsorship acknowledged. This software is subject to # U.S. export control laws and regulations and has been classified as 'EAR99 NLR' # (No [Export] License Required except when exporting to an embargoed country, # end user, or in support of a prohibited end use). By downloading this software, # the user agrees to comply with all applicable U.S. export laws and regulations. # The user has the responsibility to obtain export licenses, or other export # authority as may be required before exporting this software to any 'EAR99' # embargoed foreign country or citizen of those countries. # # Author: Eric Belz #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ """euclid is for geometric objects in E3. The main objects are: ------------------------------------------------------------------ Scalar rank-0 tensors Vector rank-1 tensors Tensor (Matrix) rank-2 tensors use their docstrings. The main module constants are: ------------------------------------------------------------------- AXES = 'xyz' -- this names the attributes for non-trivial ranks. BASIS -- is a named tuple of standard basis vectors IDEM -- is the Idem-Tensor (aka identity matrix) The main module functions are: really for internal use only, but they are not protected with a leading "_". Other: ------ There is limited support for vectors in polar coordinates. There is a: Polar named tuple, polar2vector conviniece constructor, and Vecotr.polar() method You can build Tensor (Matrix) objects from 3 Vectors using: ziprows, zipcols Final Note on Classes: all tensor objects have special methods: ---------------------- slots These are the attributes (components) iter() v.iter() Iterates over the components tolist() ans puts them in a list __getitem__ v[start:stop:step] will pass __getitem__ down to the attributes __iter__ iter(v) will take array_like vectors an return singleton-like vectors as an iterator next next(v) see __iter__() mean(axis=None) \ sum(axis=None) > apply numpy methods to components, return tensor object. cumsum(axis=None) / append: v.append(u) --> for array like v.x, ..., v.z; append u.x, ..., u.z onto the end. broadcast(func,*args, **kwargs) apply func(componenet, *args, **kwargs) for each componenet. __contains__ u in v test if singlton-like u is in array_like v. __cmp__ u == v etc., when it make sense __nonzero__ bool(v) check for NULL values. These work on Scalar, Vector, Tensor, ECEF, LLH, SCH, LTP objects. See charts.__doc__ for a dicussion of transformation definition """ ## \namespace geo::euclid Geometric Animals living in ## \f$R^3\f$ __date__ = "10/30/2012" __version__ = "1.21" import operator import itertools from functools import partial, reduce import collections import numpy as np ## Names of the coordinate axes AXES = 'xyz' ## Number of Spatial Dimensions DIMENSION = len(AXES) ## This function gets components into a list components = operator.methodcaller("tolist") ## This function makes a generator that generates tensor components component_generator = operator.methodcaller("iter") ## compose is a 2 arg functions that invokes the left args compose method with ## the right arg as an argument (see chain() ). def compose(left, right): """compose(left, right)-->left.compose(right)""" return left.compose(right) ## A named tuple for polar coordinates in terms of radius, polar angle, and ## azimuth angle It has not been raised to the level of a class, yet. Polar = collections.namedtuple("Polar", "radius theta phi") ## This is the angle portion of a Polar LookAngles = collections.namedtuple("LookAngle", "elevation azimuth") ## get the rank from the class of the argument, or None. def rank(tensor): """get rank attribute or None""" try: result = tensor.__class__.rank except AttributeError: result = None pass return result ## \f$ s = v_iv'_i \f$ \n Two Vector()'s --> Scalar . def inner_product(u, v): """s = v_i v_i""" return Scalar( u.x*v.x + u.y*v.y + u.z*v.z ) ## dot product assignemnt dot = inner_product ## \f$ v_i = \epsilon_{ijk} v'_j v''_k \f$ \n Two Vector()'s --> Vector . def cross_product(u, v): """v"_i = e_ijk v'_j v_k""" return u.__class__( u.y*v.z - u.z*v.y, u.z*v.x - u.x*v.z, u.x*v.y - u.y*v.x ) ## cross product assignment cross = cross_product ## \f$ m_{ij} v_iv'_j \f$ \n Two Vector()'s --> Matrix . def outer_product(u, v): """m_ij = u_i v_j""" return Matrix( u.x*v.x, u.x*v.y, u.x*v.z, u.y*v.x, u.y*v.y, u.y*v.z, u.z*v.x, u.z*v.y, u.z*v.z ) ## dyad is the outer product dyadic = outer_product ##\f${\bf[\vec{u},\vec{v},\vec{w}]}\equiv{\bf\vec{u}\cdot(\vec{v}\times\vec{w})}\f$ ## \n Three Vector()'s --> Scalar . def scalar_triple_product(u, v, w): """s = v1_i e_ijk v2_j v3_k""" return inner_product(u, cross_product(v, w)) ## \f${\bf \vec{u} \times (\vec{v} \times \vec{w})} \f$ \n ## Three Vector()'s --> Scalar . def vector_triple_product(u, v, w): """v3_i = e_ijk v1_j e_klm v1_l v2_m""" return reduce(operator.xor, reversed((u, v, w))) ## \f$ v'_i = m_{ij}v_j \f$ \n Matrix() times a Vector() --> Vector . def posterior_product(m, u): """v'_i = m_ij v_j""" return u.__class__(m.xx*u.x + m.xy*u.y + m.xz*u.z, m.yx*u.x + m.yy*u.y + m.yz*u.z, m.zx*u.x + m.zy*u.y + m.zz*u.z) ## \f$ v'_i = m_{ji}v_j \f$ \n Vector() times a Matrix() --> Vector . def anterior_product(v, m): """v'_j = v_i m_ij""" return v.__class__(m.xx*v.x + m.yx*v.y + m.zx*v.z, m.xy*v.x + m.yy*v.y + m.zy*v.z, m.xz*v.x + m.yz*v.y + m.zz*v.z) ## \f$ m_{ij} = m'_{ik}m''_{kj} \f$ \n Matrix() input and output. def matrix_product(m, t): """m_ik = m'_ij m''_jk""" return Matrix( m.xx*t.xx + m.xy*t.yx + m.xz*t.zx, m.xx*t.xy + m.xy*t.yy + m.xz*t.zy, m.xx*t.xz + m.xy*t.yz + m.xz*t.zz, m.yx*t.xx + m.yy*t.yx + m.yz*t.zx, m.yx*t.xy + m.yy*t.yy + m.yz*t.zy, m.yx*t.xz + m.yy*t.yz + m.yz*t.zz, m.zx*t.xx + m.zy*t.yx + m.zz*t.zx, m.zx*t.xy + m.zy*t.yy + m.zz*t.zy, m.zx*t.xz + m.zy*t.yz + m.zz*t.zz, ) ## Scalar times number--> Scalar . def scalar_dilation(s, a): """s' = scalar_dilation(s, a) s, s' is a Scalar instance a is a number. """ return Scalar(s.w*a) ## Vector times number --> Vector . def vector_dilation(v, a): """v' = vector_dilation(v, a) v, v' is a vector instance a is a number. """ return v.__class__(a*v.x, a*v.y, a*v.z) ## Matrix times a number --> Matrix . def matrix_dilation(m, a): """m' = matrix_dilation(m, a) m, m' is a Matrix instance a is a number. """ return Matrix(a*m.xx, a*m.xy, a*m.xz, a*m.yx, a*m.yy, a*m.yz, a*m.zx, a*m.zy, a*m.zz) ## Multiply 2 Scalar inputs and get a Scalar . def scalar_times_scalar(s, t): """s' = scalar_dilation(s, a) s, s' is a Scalar instance a is a number. """ return scalar_dilation(s, t.w) ## Multiply a Scalar and a Vector to get a Vector . def scalar_times_vector(s, v): """v' = scalar_times_vector(s, v) s is a Scalar v, v' is a Vector """ return vector_dilation(v, s.w) ## Multiply a Vector and a Scalar to get a Vector . def vector_times_scalar(v, s): """v' = vector_times_scalar(v, s) s is a Scalar v, v' is a Vector """ return vector_dilation(v, s.w) ## Multiply a Scalar and a Matrix to get a Matrix . def scalar_times_matrix(s, m): """m' = scalar_times_matrix(s, m) s is a Scalar m, m' is a Matrix """ return matrix_dilation(m, s.w) ## Multiply a Matrix and a Scalar to get a Matrix . def matrix_times_scalar(m, s): """m' = matrix_times_scalar(m, s) s is a Scalar m, m' is a Matrix """ return matrix_dilation(m, s.w) ## \f$ T_{ij}' = T_{kl}M_{ik}M_{jl} \f$ def rotate_tensor(t, r): """t' = rotate_tensor(t, r): t', t rank-2 Tensor objects r a rotation object. """ m = r.Matrix() return m.T*t*m ## \f$ P \rightarrow r\sin{\theta}\cos{\phi}{\bf \hat{x}} + r\sin{\theta}\sin{\phi}{\bf \hat{y}} + r\cos{\theta}{\bf \hat{z}} \f$ ## \n Convinience constructor to convert from a Polar tuple to a Vector def polar2vector(polar): """vector = polar2vector """ x = (polar.radius)*np.sin(polar.theta)*np.cos(polar.phi) y = (polar.radius)*np.sin(polar.theta)*np.sin(polar.phi) z = (polar.radius)*np.cos(polar.theta) # Note: if you have numpy.arrays r, theta, phi then: BE CAREFUL with: # >>> r*Vector( sin(theta), ..., cos(theta) ) # which gets mapped to r.__mul__ and not Vector.__rmul___ # return Vector(x, y, z) ## Stack in left index (it's not a row), and it inverts Tensor.iterrows() def ziprows(v1, v2, v3): """M = ziprrows(v1, v2, v3) stack Vector arguments into a Tensor/Matrix, M""" return Matrix(*itertools.chain(*map(components, (v1, v2, v3)))) ## Stack in right index (it's not a column), and it inverts Tensor.itercols() def zipcols(v1, v2, v3): """M = zipcols(v1, v2, v3) Transpose of ziprows """ return ziprows(v1, v2, v3).T ## metaclass computes indices from rank and assigns them to slots\n ## This is not for users. class ranked(type): """A metaclass-- used for classes with a rank static attribute that need slots derived from it. See the rank2slots static memthod""" ## Extend type.__new__ so that it add slots using rank2slots() def __new__(cls, *args, **kwargs): obj = type.__new__(cls, *args, **kwargs) obj.slots = cls.rank2slots(obj.rank) return obj ## A function that computes a tensor's attributes from it's rank\n Starting ## with "xyz" or "w". @staticmethod def rank2slots(rank): import string return ( tuple( map(partial(string.join, sep=""), itertools.product(*itertools.repeat(AXES, rank))) ) if rank else ('w',) ) pass ## This base class controls __getitem__ behavior and provised a comonenet ## iterator. class PolyMorphicNumpyMixIn(object): """This class is for classes that may have singelton on numpy array attributes (Vectors, Coordinates, ....). """ ## Object is iterbale ONLY if its attributes are, Use with care def __getitem__(self, index): """[] --> index over components' iterator, is NOT a tensor index""" return self.__class__(*[item[index] for item in self.iter()]) ## The iter() function: returns an instance that is an iterator def __iter__(self): """converts array_like comonents into iterators via iter() function""" return self.__class__(*map(iter, self.iter())) ## This allow you to send instance to the next function def next(self): """get's next Vector from iterator components-- you have to understand iterators to use this""" return self.__class__(*map(next, self.iter())) ## This allows you to use a static attribute other than "slots". def _attribute_list(self, attributes="slots"): """just an attr getter using slots""" return getattr(self.__class__, attributes) ## Note: This is JUST an iterator over components/coordinates --don't get ## confused. def iter(self, attributes="slots"): """return a generator that generates components """ return ( getattr(self, attr) for attr in self._attribute_list(attributes) ) ## Matches numpy's call --note: DO NOT DEFINE a __array__ method. def tolist(self): """return a list of componenets""" return list(self.iter()) ## historical assignement components = tolist ## This allows you to broadcast functions (numpy functions) to the ## attributes and rebuild a class def broadcast(self, func, *args, **kwargs): """vector.broadcast(func, *args, **kwargs) --> Vector(*map(func, vector.iter())) That is: apply func componenet wise, and return a new Vector """ f = partial(func, *args, **kwargs) return self.__class__(*map(f, self.iter())) ## \f$ T_{i...k} \rightarrow \frac{1}{n}\sum_0^{n-1}{T_{i...k}[n]} \f$ def mean(self, *args, **kwargs): """broadcast numpy.mean (see broadcast.__doc__)""" return self.broadcast(np.mean, *args, **kwargs) ## \f$ T_{i...k} \rightarrow \sum_0^{n-1}{T_{i...k}[n]} \f$ def sum(self, *args, **kwargs): """broadcast numpy.sum (see broadcast.__doc__)""" return self.broadcast(np.sum, *args, **kwargs) ## \f$ T_{i...k}[n] \rightarrow \sum_0^{n-1}{T_{i...k}[n]} \f$ def cumsum(self, *args, **kwargs): """broadcast numpy.cumsum (see broadcast.__doc__)""" return self.broadcast(np.cumsum, *args, **kwargs) ## experimantal method called-usage is tbd def numpy_method(self, method_name): """numpy.method(method_name) broadcast numpy.ndarray method (see broadcast.__doc__)""" return self.broadcast(operator.methodcaller(method_name)) ## For a tensor, chart, or coordinate made of array_like objects:\n append ## a like-wise object onto the end def append(self, other): """For array_like attributes, append and object into the end """ result = self.__class__( *[np.append(s, o) for s, o in zip(self.iter(), other.iter())] ) if hasattr(self, "peg_point") and self.peg_point is not None: result.peg_point = self.peg_point pass self = result return result ## The idea is to check equality for all tensor/coordinate types and for ## singleton and numpy arrays, \n so this is pretty concise-- the previous ## versior was 10 if-then blocks deep. def __eq__(self, other): """ check tensor equality, for each componenet """ if isinstance(other, self.__class__): for n, item in enumerate(zip(self.iter(), other.iter())): result = result * item[0]==item[1] if n else item[0]==item[1] pass return result else: return False pass ## not __eq__, while perserving array_like behavior- not easy -- note that ## function/statement calling enforces type checking (no array allowed), \n ## while method calling does not. def __ne__(self, other): inv = self.__eq__(other) try: result = operator.not_(inv) except ValueError: result = (1-inv).astype(bool) pass return result ## This covers < <= > >= and raises a RuntimeError, unless numpy ## calls it, and then that issue is addressed (and it's complicated) def __cmp__(self, other): """This method is called in 2 cases: Vector Comparison: if you compare (<, >, <=, >=) two non-scalar tensors, or rotations--that makes no sense and you get a TypeError. Left-Multiply with Numpy: this is a little more subtle. If you are working with array_like tensors, and say, do >>> (v**2).w*v instead of: >>> (v**2)*v (which works), numpy will take over and call __cmp__ inorder to figure how to do linear algebra on the array_like componenets of your tensor. The whole point of the Scalar class is to avoid this pitfall. Side Note: any vector operation should be manifestly covariant-- that is-- you don't need to access the "w"--so you should not get this error. But you might. """ raise TypeError( """comparision operation not permitted on %s Check for left mul by a numpy array. Right operaned is %s""" % (self.__class__.__name__, other.__class__.__name) ) ## In principle, we always want true division: this is here in case a ## client calls it def __truediv__(self, other): return self.__div__(other) ## In principle, we always want true division: this is here in case a ## client calls it def __rtruediv__(self, other): return self.__rdiv__(other) ## This is risky and pointless-- who calls float? def __float__(self): return abs(self).w ## +T is ## T --- is as in python\n This method is used in coordinate transforms ## when an identity transform is required. def __pos__(self): return self ## For container attributes: search, otherise return NotImplemented so ## that the object doesn't look like a container when intropspected. def __contains__(self, other): try: for item in self: if item == other: return True pass pass except TypeError: raise NotImplementedError pass ## A tensor/chart/coordinate object has a len() iff all its components ## have all the same length def __len__(self): for n, item in enumerate(self.iter()): if n: if len(item) != last: raise ValueError( "Length of %s object is ill defined" % self.__class__.__name ) pass else: try: last = len(item) except TypeError: raise TypeError( "%s object doesn't have a len()" % self.__class__.__name__ ) pass pass return last pass ## A temporaty class until I decide if ops are composed or convolved (brain ## freeze). class _LinearMap(object): """This class ensures linear map's compose method calls the colvolve method""" ## compose is convolve def compose(self, other): return self.convolve(other) ## convolve is compose def convolve(self, other): return self.compose(other) ## Chain together a serious of instances -- it's a static method @staticmethod def chain(*args): """chain(*args)-->reduce(compose, args)""" return reduce(compose, args) pass ## Mixin for Alias transformations rotate the coordinate system, leaving the ## object fixed class Alias(_LinearMap): """This mix-in class makes the rotation object a Alias transform: It rotates coordinate systems, leaving the vector unchanged. """ ## As a linear map, this is __call__ def __call__(self, other): return self.AliasTransform(other) ## \f$ (g(f(v))) = (fg)(v) \f$ def compose(self, other): return self*other ## This is an Alias Transform def AliasMatrix(self): return self ## Alibi Transform is the transpose def AlibiMatrix(self): return self.T ## Alias.Matrix is AliasMatrix def Matrix(self): """get equivalent Alias rotatation Matrix for rotation (chart) object""" return self.AliasMatrix() pass ## Mixin Alibi transformations rotate the object, leaving the coordinare ## system fixed class Alibi(_LinearMap): """This mix-in class makes the rotation object a Alibi transform: It rotates vectors with fixed coordinate systems. """ ## As a linear map, this is __call__ def __call__(self, other): return self.AlibiTransform(other) ## \f$ (g(f(v))) = (gf)(v) \f$ def compose(self, other): return other*self ## This is an Alibi Transform def AlibiMatrix(self): return self ## Alias Transform is the transpose def AliasMatrix(self): return self.T ## Alibi.Matrix is AlibiMatrix def Matrix(self): """get equivalent Alibi rotatation Matrix for rotation (chart) object""" return self.AlibiMatrix() pass ## Base class for animals living in \f$ R^3 \f$. class Geometric(PolyMorphicNumpyMixIn): """Base class for things that are: iterables over their slots may or may not have iterbable attributes """ ## neg is the same class with all components negated, use map and ## operator.neg with iter(). def __neg__(self): """-tensor maps components to neg and builds a new tensor.__class__""" return self.__class__(*map(operator.neg, self.iter())) ## Repr is as repr does def __repr__(self): guts = ",".join(map(str, self.iter())) return repr(self.__class__).lstrip("")+"("+guts+")" ## Sometimes I just want a list of componenets def components(self): """Return a list of slots attributes""" return super(Geometric, self).tolist() pass ## This decorator decorates element-wise operations on tensors def elementwise(op): """func = elementwise(op): op is a binary arithmetic operator. So is func, except it works on elements of the Tensor, returning a new tensor of the same type. """ from functools import wraps @wraps(op) def wrapped_op(self, other): try: result = self.__class__( *[op(*items) for items in itertools.zip_longest(self.iter(), other.iter())] ) except (TypeError, AttributeError) as err: from isceobj.Util.geo.exceptions import ( NonCovariantOperation, error_message ) x = ( NonCovariantOperation if isinstance(other, PolyMorphicNumpyMixIn) else TypeError ) raise x(error_message(op, self, other)) return result return wrapped_op ## Base class class Tensor_(Geometric): """Base class For Any Rank Tensor""" ## Get the rank of the other, and chose function from the mul_rule ## dictionary static attribute def __mul__(self, other): """Note to user: __mul__ is inherited for Tensor. self's mul_rule dictionary is keyed by other's rank inorder to get the correct function to multiply the 2 objects. """ return self.__class__.mul_rule[rank(other)](self, other) ## reflected mul is always a non-Tensor, so use the [None] value from the ## mul_rule dictionary def __rmul__(self, other): """rmul always selects self.__class__.mul_rule[None] to compute""" return self.__class__.mul_rule[None](self, other) ## elementwise() decorated addition @elementwise def __add__(self, other): """t3_i...k = t1_i...k + t2_i...k (elementwise add only)""" return operator.add(self, other) ## elementwise() decorated addition @elementwise def __sub__(self, other): """t3_i...k = t1_i...k - t2_i...k (elementwise sub only)""" return operator.sub(self, other) ## Division is pretty straigt forward def __div__(self, other): return self.__class__(*[item/other for item in self.iter()]) def __str__(self): return reduce(operator.add, map(lambda i: str(i)+"="+str(getattr(self, i))+"\n", self.__class__.slots) ) ## Reduce list of squared components- you can't use sum here if the ## components are basic.Arrays. def normsq(self, func=operator.add): return Scalar( reduce( func, [item*item for item in self.iter()] ) ) ## The L2 norm is a Scalar, from normsq()**0.5 def L2norm(self): """normsq()**0.5""" return self.normsq()**0.5 ## The unit vector def hat(self): """v.hat() is v's unit vector""" return self/(self.L2norm().w) ## Abs value is usually the L2-norm, though it might be the determiant for ## rank 2 objects. __abs__ = L2norm ## See __mul__ for dilation - this may be deprecated def dilation(self, other): """v.dilation(c) --> c*v with c a real number.""" return self.__class__.rank[None](self, other) ## Return True/False for singleton-like tensors, and bool arrays for ## array_like input. def __nonzero__(self): try: for item in self: break pass except TypeError: # deal with singelton tensor/coordiantes return any(map(bool, self.iter())) # Now deal with numpy array)like attributes return np.array(map(bool, self)) pass ## a decorator for __cmp__ operators ## to try scalars, and then do numbers, works for singeltons and numpy.ndarrays. def cmpdec(func): def cmp(self, other): try: result = func(self.w, other.w) except AttributeError: result = func(self.w, other) pass return result cmp.__doc__ = "a b ==> a.w b.w or a.w b" return cmp ## A decorator: "w" not "w" -- the purpose is to help scalar operatorions with ## numpy.arrays--it seems to be impossible to cover every case def wnotw(func): """elementwise should decorate just fine, but it fails if you add an plain nd array on the right -- should that be allowed?--it is if you decorate with this. """ def wfunc(self, right): """operator with Scalar checking""" try: result = ( func(self.w, right) if rank(right) is None else func(self.w, right.w) ) except AttributeError: from isceobj.Util.geo.exceptions import NonCovariantOperation from isceobj.Util.geo.exceptions import error_message raise NonCovariantOperation(error_message(func, self, right)) return Scalar(result) return wfunc ## Scalar ## class transforms as \f$ s' = s \f$ class Scalar(Tensor_): """s = Scalar(w) is a rank-0 tensor with one attribute: s.w which can be a signleton, array_like, or an iterator. You need Scalars because they now about Vector/Tensor operations, while singletons and numpy.ndarrays do not. ZERO ONE are module constants that are scalars. """ ## The ranked meta class figures out the indices # __metaclass__ = ranked slots = ('w',) ## Tensor rank rank = 0 ## The "rule" choses the multiply function accordinge to rank mul_rule = { None:scalar_dilation, 0:scalar_times_scalar, 1:scalar_times_vector, 2:scalar_times_matrix } ## explicity __init__ is just to be nice \n (and it checks for nested ## Scalars-- which should not happen). def __init__(self, w): ## "w" is the name of the scalar "axis" self.w = w.w if isinstance(w, Scalar) else w return None ## This is a problem--it's not polymorphic- Scalars are a pain. def __div__(self, other): try: result = super(Scalar, self).__div__(other) except (TypeError, AttributeError): try: result = super(Scalar, self).__div__(other.w) except AttributeError: from isceobj.Util.geo.exceptions import ( UndefinedGeometricOperation, error_message ) raise UndefinedGeometricOperation( error_message(self.__class__.__div__, self, other) ) pass return result ## note: rdiv does not perserve type. def __rdiv__(self, other): return other/(self.w) # ## 1/s need to be defined. Do not go here with numpy arrays. # def __rdiv__(self, other): # return self**(-1)*other @wnotw def __sub__(self, other): return operator.sub(self, other) @wnotw def __add__(self, other): return operator.add(self, other) ## pow is pretty regular def __pow__(self, other): try: result = (self.w)**other except TypeError: result = (self.w)**(other.w) pass return self.__class__(result) ## reflected pow is required, e.g: \f$ e^{i \vec{k}\vec{r}} \f$ is a Scalar ## in the exponent. def __rpow__(self, other): return other**(self.w) ## < ## decorated with cmpdec() . @cmpdec def __lt__(self, other): return operator.lt(self, other) ## <= ## decorated with cmpdec() . @cmpdec def __le__(self, other): return operator.le(self, other) ## > ## decorated with cmpdec() . @cmpdec def __gt__(self, other): return operator.gt(self, other) ## >= ## decorated with cmpdec() . @cmpdec def __ge__(self, other): return operator.ge(self, other) pass ## Scalar Null ZERO = Scalar(0.) ## Scalar Unit ONE = Scalar(1.) ## Vector ## class transforms as \f$ v_i' = M_{ij}v_j \f$ class Vector(Tensor_): """v = Vector(x, y, z) is a vector with 3 attributes: v.x, v.y, v.z Vector operations are overloaded: "*" does dilation by a scalar, dot product, matrix multiply for for rank 0, 1, 2 objects. For vector arguments, u: v*u --> v.dot(u) v^u --> v.cross(u) v&u --> v.outer(u) The methods cover all the manifestly covariant equation you can write down. abs(v) --> a scalar abs(v).w --> a regular number or array v.hat() is the unit vector along v. v.versor(angle) makes a versor that rotates around v by angle. v.Polar() makes a Polar object out of it. ~v --> v.dual() makes a matrix that does a cross product with v. v.right_quaterion() makes a right quaternion: q = (0, v) v.right_versor() makes a right verosr: q = (0, v.hat()) v.var() makes the covariance matrix of an array_like vector. """ ## the metaclass copmutes the attributes from rank # __metaclass__ = ranked slots = ('x', 'y', 'z') ## Vectors are rank 1 rank = 1 ## The hash table assigns multiplication mul_rule = { None:vector_dilation, 0:vector_times_scalar, 1:inner_product, 2:anterior_product } ## init is defined explicity, eventhough the metaclass can do it implicitly def __init__(self, x, y, z): ## x compnent self.x = x ## y compnent self.y = y ## z compnent self.z = z return None def __str__(self): return str(self.components()) ## ~v --> v.dual() See dual() , I couldn't resist. def __invert__(self): return self.dual() ## u^v --> cross_product() \n An irrestistable overload, given then wedge ## product on exterior algebrea-- watchout for presednece rules with this ## one. def __xor__(self, other): return cross_product(self, other) ## u&v --> outer_product() \n Wanton overloading with bad presednece rules, ## \n but it is the only operation that preserves everything about the ## arguments (syntactically AND). def __and__(self, other): return outer_product(self, other) ## Ths is a limited "pow"-- don't do silly exponents. def __pow__(self, n): try: result= reduce(operator.mul, itertools.repeat(self, n)) except TypeError as err: if isinstance(n, Geometric): from isceobj.Util.geo.exceptions import ( UndefinedGeometricOperation, error_message ) raise UndefinedGeometricOperation( error_message(self.__class__.__pow__, self, n) ) if n <= 1: raise ValueError("Vector exponent must be 1,2,3...,") raise err return result ## \f$ v_i u_j \f$ \n The Scalar, inner, dot product def dot(self, other): """scalar product""" return inner_product(self, other) ## \f$ c_{i} = \epsilon_{ijk}a_jb_k \f$ \n The (pseudo)Vector wedge, cross ## product def cross(self, other): """cross product""" return cross_product(self, other) ## \f$ m_{ij} = v_i u_j \f$ \n The dyadic, outer product def dyad(self, other): """Dyadic product""" return outer_product(self, other) outer = dyad ## Define a rotation about \f$ \hat{v} \f$ \n, realitve to kwarg: ## circumference = \f$2\pi\f$ def versor(self, angle, circumference=2*np.pi): """vector(angle, circumfrence=2*pi) return a unit quaternion (versor) that represents an alias rotation by angle about vector.hat() """ from isceobj.Util.geo.charts import Versor f = 2*np.pi/circumference return Versor( Scalar(self._ones_like(np.cos(f*angle/2.))), self.hat()*(np.sin(f*angle/2.)) ) ## Convert to a right versor after normalization. def right_versor(self): return self.hat().right_quaternion() ## Convert to a right versor (for transformation)\n That is: as add a ::ZERO Scalar part and don't normalize to unit hypr-sphere. def right_quaternion(self): from isceobj.Util.geo.charts import Versor return Versor(Scalar(self._ones_like(0.)), self) ## \f$ v_i \rightarrow \frac{1}{2} v_i \epsilon_{ijk} \f$ \n ## This method is used when converting a Versor to a rotation Matrix \n ## it's more of a cross_product partial function operator than a Hodge dual. def dual(self): """convert to antisymetrix matrix""" zero = self._ones_like(0) return Matrix(zero, self.z, -self.y, -self.z, zero, self.x, self.y, -self.x, zero) ## \f$ {\bf P} = \hat{v}\hat{v} \f$ \n ## The Projection Operator: Matrix for the orthogonal projection onto vector . def ProjectionOperator(self): """vector --> matrix that projects (via right mul) argument onto vector""" u = self.hat() return u&u ## \f$ \hat{v}(\hat{v}\cdot\vec{u}) \f$ \n ## Apply ProjectionOperator() to argument. def project_other_onto(self, other): return self.ProjectionOperator()*other ## \f$ {\bf R} = {\bf I} - 2\hat{v}\hat{v} \f$ \n ## Matrix reflecting vector about plane perpendicular to vector . def ReflectionOperator(self): """vector --> matrix that reflects argument about vector""" return IDEM - 2*self.ProjectionOperator() ## \f$ \vec{u} - \hat{v}(\hat{v}\cdot\vec{u}) \f$ \n ## Apply RelectionOperatior() to argument def reflect_other_about_orthogonal_plane(self, other): return self.ReflectionOperator()*other ## \f$ \vec{a}\cdot\hat{b} \f$ \n ## Scalar projection: a's projection onto b's unit vector. def ScalarProjection(self, other): """Scalar Projection onto another vector""" return self*(other.hat()) ## \f$ (\vec{a}\cdot\hat{b})\hat{b} \f$ \n ## Vector projection: a's projection onto b's unit vector times b's unit ## vector\n (aka: a's resolute on b). def VectorProjection(self, other): """Vector Projection onto another vector""" return self.ScalarProjection(other)*(other.hat()) ## Same thing, different name Resolute = VectorProjection ## \f$ \vec{a} - (\vec{a}\cdot\hat{b})\hat{b} \f$ \n Vector rejection ## (perpto) is the vector part of a orthogonal to its resolute on b. def VectorRejection(self, other): """Vector Rejection of another vector""" return self-(self.VectorProjection(other)) ## \f$ \cos^{-1}{\hat{a}\cdot\hat{b}} \f$ as a Scalar instance \n ## this farms out the trig call so the developer doesn't have to worry ## about it. def theta(self, other): """a.theta(b) --> Scalar( a*b / |a||b|) --for real, it's a rank-0 obj""" return (self.hat()*other.hat()).broadcast(np.acos) ## convert to ::Polar named tuple-- can make polar a class if needed. def Polar(self, look_only=False): """Convert to polar coordinates""" radius = abs(self).w theta = np.arccos(self.z/radius) phi = np.arctan2(self.y, self.x) return ( LookAngles(theta, phi) if look_only else Polar(radius, theta, phi) ) ##\f$\bar{(\vec{v}-\langle\bar{v}\rangle)(\vec{v}-\langle\bar{v}\rangle)}\f$ ## For an iterable Vector def var(self): """For an iterable vector, return a covariance matrix""" v = (self - self.mean()) return (v&v).mean() ## Get like wise zeros to fill in matrices and quaternions\n- ## this may not be the best way def _ones_like(self, constant=1): return constant + self.x*0 ## Make a Gaussian Random Vector @classmethod def grv(cls, n): """This class method does: Vector(*[item(n) for item in itertools.repeat(np.random.randn, 3)]) get it? That's a random vector. """ return cls( *[item(n) for item in itertools.repeat(np.random.randn, 3)] ) ## return self-- for polymorphism def vector(self): return self ## Upgrayed to a ::motion::SpaceCurve(). def space_curve(self, t=None): """For array_like vectors, make a full fledged motion.SpaceCurve: space_curve = vector.space_curve([t=None]) """ from isceobj.Util.geo.motion import SpaceCurve return SpaceCurve(*self.iter(), t=t) pass ## \f$ \hat{e}_x, \hat{e}_y, \hat{e}_z \f$ \n ## The standard basis, ## as a class attribute Redundant with module ::BASIS -- Vector.e = collections.namedtuple("standard_basis", "x y z")( Vector(1.,0.,0.), Vector(0.,1.,0.), Vector(0.,0.,1.) ) ## Limited Rank-2 Tensor ## class transforms as \f$ T_{ij}' = M_{ik}M_{jl}T_{jl} \f$ class Tensor(Tensor_, Alias): """T = Tensor( xx, xy, xz, yx, yy, yz, zx, zy, zz ) Is a cartesian tensor, and it's a function from E3-->E3 (a rotation matrix). As a rotation, it does either Alias or Alibi rotation-- it depends which class is in Tensor.__bases__[-1] -- it should be Alias, so it rotates coordinates and leaves vectors fixed. TRANSFORMING VECTORS: >>>v_prime = T(v) That is, the __call__ method does it for you. Use it-- do not multiply. You can multiply or do an explicit transformation with any of the following: T.AliasTransfomation(v) T.AlibiTransfomation(v) T*v v*T You can convert to other charts on SO(3) with: T.versor() T.YPR() Or get individual angles: T.yaw, T.pitch, T.roll Other matrix/tensor methods are: T.T (same as T.transpose()) ~T (same as T.I inverts it) abs(T) (same as T.det(), a Scalar) T.trace() trace (a Scalar) T.L2norm() L2-norm ( a Scalar) T.A() antisymmetric part T.S() symmetric part T.stf() symmetric trace free part T.dual() Antisymetric part (contracted with Levi-Civita tensor) Finally: T.row(n), T.col(n), T.iterrows(), T.itercols(), T.tolist() are all pretty simple. """ ## The meta class computes the attributes from ranked # __metaclass__ = ranked slots = ('xx', 'xy', 'xz', 'yx', 'yy', 'yz', 'zx', 'zy', 'zz') ## The rank is 2. rank = 2 ## The hash table assigns multiplication mul_rule = { None:matrix_dilation, 0:matrix_times_scalar, 1:posterior_product, 2:matrix_product } ## self.__class__.__bases__[2] rules for call, usage is TBD call_rule = { None: lambda x:x, 0: lambda x:x, 1: "vector_transform_by_matrix", 2: "tensor_transform_by_matrix" } ## explicit 9 argument init. def __init__(self, xx, xy, xz, yx, yy, yz, zx, zy, zz): ## Cartesian Componenet: ## \f$ m_{xx} = {\bf{T}}^{({\bf e_x})}_x \f$ self.xx = xx ## Cartesian Componenet: ## \f$ m_{xy} = {\bf{T}}^{({\bf e_y})}_x \f$ self.xy = xy ## Cartesian Componenet: ## \f$ m_{xz} = {\bf{T}}^{({\bf e_z})}_x \f$ self.xz = xz ## Cartesian Componenet: ## \f$ m_{yx} = {\bf{T}}^{({\bf e_x})}_y \f$ self.yx = yx ## Cartesian Componenet: ## \f$ m_{yy} = {\bf{T}}^{({\bf e_y})}_y \f$ self.yy = yy ## Cartesian Componenet: ## \f$ m_{yz} = {\bf{T}}^{({\bf e_z})}_y \f$ self.yz = yz ## Cartesian Componenet: ## \f$ m_{zx} = {\bf{T}}^{({\bf e_x})}_z \f$ self.zx = zx ## Cartesian Componenet: ## \f$ m_{zy} = {\bf{T}}^{({\bf e_y})}_z \f$ self.zy = zy ## Cartesian Componenet: ## \f$ m_{zz} = {\bf{T}}^{({\bf e_z})}_z \f$ self.zz = zz return None ## Alibi transforms are from the left def AlibiTransform(self, other): return posterior_product(self, other) ## Alias transforms are from the right def AliasTransform(self, other): return anterior_product(other, self) ## \f$ v_i = m_{ni} \f$ \n Get a "row", or, run over the 1st index def row(self, n): """M.row(n) --> Vector(M.nx, M.ny, M.nz) for n = (0,1,2) --> (x, y, z), so it's not a row, but a run on the 2nd index """ return Vector( *[getattr(self, attr) for attr in self.slots[n*DIMENSION:(n+1)*DIMENSION] ] ) ## \f$ v_i = m_{in} \f$ def col(self, n): """Run on 1st index. See row.__doc__ """ return self.T.row(n) ## iterate of rows def iterrows(self): """iterator over row(n) for n in 0,1,2 """ return map(self.row, range(DIMENSION)) ## make a list def tolist(self): """A list of components -- nested""" return [item.tolist() for item in self.iterrows()] ## \f$ m_{ij}^T = m_{ji} \f$ def transpose(self): """Transpose: M_ij --> M_ji """ return Matrix(self.xx, self.yx, self.zx, self.xy, self.yy, self.zy, self.xz, self.yz, self.zz) ## assign "T" to transpose() @property def T(self): return self.transpose() ## ~Matrix --> Matrix.I def __invert__(self): return self.I ## Matrix Inversion as a property to look like numpy @property def I(self): row0, row1, row2 = self.iterrows() return zipcols( cross(row1, row2), cross(row2, row0), cross(row0, row1) )/self.det().w ## \f$ m_{ii} \f$ \n Trace, is a Scalar def trace(self): return Scalar(self.xx + self.yy + self.zz) ## \f$ v_k \rightarrow \frac{1}{2} m_ij \epsilon_{ijk} \f$ \n ## Rank-1 part of Tensor def vector(self): """Convert to a vector w/o scaling""" return Vector(self.yz-self.zy, self.zx-self.xz, self.xy-self.yx) ## \f$ \frac{1}{2}[(m_{yz}-m_{zy}){\bf \hat{x}} + (m_{zx}-m_{xz}){\bf \hat{y}} + (m_{zx}-m_{xz}){\bf \hat{z}})] \f$ --normalization is under debate. def dual(self): """The dual is a vector""" return self.vector()/2. ## \f$ \frac{1}{2}(m_{ij} + m_{ji}) \f$ \n Symmetric part def S(self): """Symmeytic Part""" return (self + self.T)/2 ## \f$ \frac{1}{2}(m_{ij} - m_{ji}) \f$ \n Antisymmetric part def A(self): """Antisymmeytic Part""" return (self - self.T)/2 ## \f$ \frac{1}{2}(m_{ij} + m_{ji}) -\frac{1}{3}\delta_{ij}Tr{\bf m} \f$\n ## Symmetric Trace Free part def stf(self): """symmetric trace free part""" return self.S() - IDEM*self.trace()/3. ## Determinant as a scalar_triple_product as:\n ## \f$ m_{ix} (m_{jy}m_{kz}\epsilon_{ijk}) \f$. def det(self): """determinant as a Scalar""" return scalar_triple_product(*self.iterrows()) ## |M| is determinant--though it may be negative __abs__ = det ## not quite right def __str__(self): return "\n".join(map(str, self.iterrows())) ## does not enfore integer only, though non-integer is not supported def __pow__(self, n): if n < 0: return self.I.__pow__(-n) else: return reduce(operator.mul, itertools.repeat(self, n)) pass @property ## Get Yaw Angle (\f$ \alpha \f$ ) as a rotation (norm is NOT checked) ## via: \n \f$ \tan{\alpha} = \frac{M_{yx}}{M_{xx}} \f$ def yaw(self): from numpy import arctan2, degrees return degrees(arctan2(self.yx, self.xx)) ## Get Pitch Angle (\f$ \beta \f$ ) as a rotation (norm is NOT checked) ## via: \n ##\f$\tan{\beta}=\frac{M_{zy}}{(M_{zy}+M_{zz})/(\cos{\gamma}+\sin{\gamma})}\f$ def _pitch(self, roll=None): from numpy import arctan2, degrees, radians, cos, sin roll = radians(self.roll) if roll is None else radians(roll) cos_b = (self.zy + self.zz)/(cos(roll)+sin(roll)) return degrees(arctan2(-(self.zx), cos_b)) ## Use _pitch() @property def pitch(self): return self._pitch() @property ## Get Roll Angle (\f$ \gamma \f$ ) as a rotation (norm is NOT checked) ## via: \n \f$ \tan{\gamma} = \frac{M_{zy}}{M_{zx}} \f$ def roll(self): from numpy import arctan2, degrees return degrees(arctan2(self.zy, self.zz)) ## Convert to a tuple of ( Yaw(), Pitch(), Roll() ) def ypr(self): """compute to angle triplet""" roll = self.roll pitch = self._pitch(roll=roll) return (self.yaw, pitch, roll) ## Convert to a YPR instance via ypr() def YPR(self): """convert to YPR class""" from isceobj.Util.geo.charts import YPR return YPR(*(self.ypr())) def rpy(self): return NotImplemented RPY = rpy ## Convert to a rotation versor via YPR() def versor(self): """Convert to a rotation versor--w/o checking for viability. """ return self.YPR().versor() pass ## Synonym-- really should inherit from a biinear map and a tensor Matrix = Tensor ## The NULL vector NULL = Vector(0.,0.,0.) ## Idem tensor IDEM = Tensor( 1.,0.,0., 0.,1.,0., 0.,0.,1. ) ## \f$ R^3 \f$ basis vectors BASIS = collections.namedtuple("Basis", 'x y z')(*IDEM.iterrows()) ## The Tuple of Basis Vectors (is here, because EulerAngleBase needs it) X, Y, Z = BASIS ## A collections for orthonormal dyads. DYADS = collections.namedtuple("Dyads", 'xx xy xz yx yy yz zx zy zz')(*[left&right for left in BASIS for right in BASIS])