Developing Quaternion and Space-time Number Tools for iPython3

In this notebook, tools for working with quaternions for physics issues are developed. The class QH treat quaternions as Hamilton would have done: as a 4-vector over the real numbers.

In physics, group theory plays a central role in the fundamental forces of Nature via the standard model. The gauge symmetry U(1) a unit circle in the complex plane leads to electric charge conservation. The unit quaternions $SU(2)$ is the symmetry needed for the weak force which leads to beta decay. The group $SU(3)$ is the symmetry of the strong force that keeps a nucleus together.

The class Q8 was written in the hope that group theory would be written in first, not added as needed later. I call these "space-time numbers". The problem with such an approach is that one does not use the mathematical field of real numbers. Instead one relies on the set of positive reals. In some ways, this is like reverse engineering some basic computer science. Libraries written in C have a notion of a signed versus unsigned integer. The signed integer behaves like the familiar integers. The unsigned integer is like the positive integers. The difference between the two is whether there is a placeholder for the sign or not. All floats are signed. The modulo operations that work for unsigned integers does not work for floats.

This set of tools is done 4x:

  1. QH - Quaternions for Hamilton, can do symbolic manipulations
  2. QHa - Quaternions for Hamilton numpy arrays
  3. Q8 - Quaternions that are represented by 8 numbers
  4. Q8a - Quaternions that are represented by 8 numbers that are numpy arrays

Test driven development was used. The same tests were used for QH, QHa, Q8, and Q8a. Either class can be used to study quaternions in physics.


In [1]:
import IPython
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import math
import numpy as np
import random
import sympy as sp
import os
import unittest

from IPython.display import display
from os.path import basename
from glob import glob
%matplotlib inline

Define the stretch factor $\gamma$ and the $\gamma \beta$ used in special relativity.


In [2]:
def sr_gamma(beta_x=0, beta_y=0, beta_z=0):
    """The gamma used in special relativity using 3 velocites, some may be zero."""

    return 1 / (1 - beta_x ** 2 - beta_y ** 2 - beta_z ** 2) ** (1/2)

def sr_gamma_betas(beta_x=0, beta_y=0, beta_z=0):
    """gamma and the three gamma * betas used in special relativity."""

    g = sr_gamma(beta_x, beta_y, beta_z)
    
    return [g, g * beta_x, g * beta_y, g * beta_z]

Quaternions for Hamilton

Define a class QH to manipulate quaternions as Hamilton would have done it so many years ago. The "qtype" is a little bit of text to leave a trail of breadcrumbs about how a particular quaternion was generated.

class QH(object): """Quaternions as Hamilton would have defined them, on the manifold R^4.""" def __init__(self, values=None, qtype="Q", representation=""): if values is None: self.t, self.x, self.y, self.z = 0, 0, 0, 0 elif len(values) == 4: self.t, self.x, self.y, self.z = values[0], values[1], values[2], values[3] elif len(values) == 8: self.t, self.x = values[0] - values[1], values[2] - values[3] self.y, self.z = values[4] - values[5], values[6] - values[7] self.representation = representation if representation != "": self.t, self.x, self.y, self.z = self.representation_2_txyz(representation) self.qtype = qtype def __str__(self): """Customize the output.""" if len(self.qtype) > 66: report_qtype = "..." else: report_qtype = self.qtype if self.representation == "": string = "({t}, {x}, {y}, {z}) {qt}".format( t=self.t, x=self.x, y=self.y, z=self.z, qt=report_qtype) elif self.representation == "polar": rep = self.txyz_2_representation("polar") string = "({A} A, {thetaX} 𝜈x, {thetaY} 𝜈y, {thetaZ} 𝜈z) {qt}".format( A=rep[0], thetaX=rep[1], thetaY=rep[2], thetaZ=rep[3], qt=report_qtype) elif self.representation == "spherical": rep = self.txyz_2_representation("spherical") string = "({t} t, {R} R, {theta} θ, {phi} φ) {qt}".format( t=rep[0], R=rep[1], theta=rep[2], phi=rep[3], qt=report_qtype) return string def is_symbolic(self): """Figures out if an expression has symbolic terms.""" symbolic = False if hasattr(self.t, "free_symbols") or hasattr(self.x, "free_symbols") or \ hasattr(self.y, "free_symbols") or hasattr(self.z, "free_symbols"): symbolic = True return symbolic def txyz_2_representation(self, representation): """Converts Cartesian txyz into an array of 4 values in a different representation.""" symbolic = self.is_symbolic() if representation == "": rep = [self.t, self.x, self.y, self.z] elif representation == "polar": amplitude = (self.t ** 2 + self.x ** 2 + self.y **2 + self.z **2) ** (1/2) abs_v = self.abs_of_vector().t if symbolic: theta = sp.atan2(abs_v, self.t) else: theta = math.atan2(abs_v, self.t) if abs_v == 0: thetaX, thetaY, thetaZ = 0, 0, 0 else: thetaX = theta * self.x / abs_v thetaY = theta * self.y / abs_v thetaZ = theta * self.z / abs_v rep = [amplitude, thetaX, thetaY, thetaZ] elif representation == "spherical": t = self.t R = (self.x ** 2 + self.y **2 + self.z **2) ** (1/2) if R == 0: theta = 0 else: if symbolic: theta = sp.acos(self.z / R) else: theta = math.acos(self.z / R) if symbolic: phi = sp.atan2(self.y, self.x) else: phi = math.atan2(self.y, self.x) rep = [t, R, theta, phi] else: print("Oops, don't know representation: ", representation) return rep def representation_2_txyz(self, representation): """Convert from a representation to Cartesian txyz.""" symbolic = False if hasattr(self.t, "free_symbols") or hasattr(self.x, "free_symbols") or \ hasattr(self.y, "free_symbols") or hasattr(self.z, "free_symbols"): symbolic = True if representation == "": t, x, y, z = self.t, self.x, self.y, self.z elif representation == "polar": amplitude, thetaX, thetaY, thetaZ = self.t, self.x, self.y, self.z theta = (thetaX ** 2 + thetaY ** 2 + thetaZ ** 2) ** (1/2) if theta == 0: t = self.t x, y, z = 0, 0, 0 else: if symbolic: t = amplitude * sp.cos(theta) x = self.x / theta * amplitude * sp.sin(theta) y = self.y / theta * amplitude * sp.sin(theta) z = self.z / theta * amplitude * sp.sin(theta) else: t = amplitude * math.cos(theta) x = self.x / theta * amplitude * math.sin(theta) y = self.y / theta * amplitude * math.sin(theta) z = self.z / theta * amplitude * math.sin(theta) elif representation == "spherical": t, R, theta, phi = self.t, self.x, self.y, self.z if symbolic: x = R * sp.sin(theta) * sp.cos(phi) y = R * sp.sin(theta) * sp.sin(phi) z = R * sp.cos(theta) else: x = R * math.sin(theta) * math.cos(phi) y = R * math.sin(theta) * math.sin(phi) z = R * math.cos(theta) else: print("Oops, don't know representation: ", representation) txyz = [t, x, y, z] return txyz def check_representations(self, q1): """If they are the same, report true. If not, kick out an exception. Don't add apples to oranges.""" if self.representation == q1.representation: return True else: raise Exception("Oops, 2 quaternions have different representations: {}, {}".format(self.representation, q1.representation)) return False def display_q(self): """Display each terms in a pretty way.""" display(self.t) display(self.x) display(self.y) display(self.z) return def simple_q(self): """Simplify each term.""" self.t = sp.simplify(self.t) self.x = sp.simplify(self.x) self.y = sp.simplify(self.y) self.z = sp.simplify(self.z) return self def expand_q(self): """Expand each term.""" self.t = sp.expand(self.t) self.x = sp.expand(self.x) self.y = sp.expand(self.y) self.z = sp.expand(self.z) return self def q_0(self, qtype="0"): """Return a zero quaternion.""" q0 = QH([0, 0, 0, 0], qtype=qtype, representation=self.representation) return q0 def q_1(self, qtype="1"): """Return a multiplicative identity quaternion.""" q1 = QH([1, 0, 0, 0], qtype=qtype, representation=self.representation) return q1 def q_i(self, qtype="i"): """Return i.""" qi = QH([0, 1, 0, 0], qtype=qtype, representation=self.representation) return qi def q_j(self, qtype="j"): """Return j.""" qj = QH([0, 0, 1, 0], qtype=qtype, representation=self.representation) return qj def q_k(self, qtype="k"): """Return k.""" qk = QH([0, 0, 0, 1], qtype=qtype, representation=self.representation) return qk def q_random(self, qtype="?"): """Return a random-valued quaternion.""" qr = QH([random.random(), random.random(), random.random(), random.random()], qtype=qtype) return qr def dupe(self, qtype=""): """Return a duplicate copy, good for testing since qtypes persist""" du = QH([self.t, self.x, self.y, self.z], qtype=self.qtype, representation=self.representation) return du def equals(self, q1): """Tests if two quaternions are equal.""" self.check_representations(q1) self_t, self_x, self_y, self_z = sp.expand(self.t), sp.expand(self.x), sp.expand(self.y), sp.expand(self.z) q1_t, q1_x, q1_y, q1_z = sp.expand(q1.t), sp.expand(q1.x), sp.expand(q1.y), sp.expand(q1.z) if math.isclose(self_t, q1_t) and math.isclose(self_x, q1_x) and math.isclose(self_y, q1_y) and math.isclose(self_z, q1_z): return True else: return False def conj(self, conj_type=0, qtype="*"): """Three types of conjugates.""" t, x, y, z = self.t, self.x, self.y, self.z conj_q = QH() if conj_type == 0: conj_q.t = t if x != 0: conj_q.x = -1 * x if y != 0: conj_q.y = -1 * y if z != 0: conj_q.z = -1 * z elif conj_type == 1: if t != 0: conj_q.t = -1 * t conj_q.x = x if y != 0: conj_q.y = -1 * y if z != 0: conj_q.z = -1 * z qtype += "1" elif conj_type == 2: if t != 0: conj_q.t = -1 * t if x != 0: conj_q.x = -1 * x conj_q.y = y if z != 0: conj_q.z = -1 * z qtype += "2" conj_q.qtype = self.qtype + qtype conj_q.representation = self.representation return conj_q def flip_signs(self, qtype="-"): """Flip the signs of all terms.""" end_qtype = "-{}".format(self.qtype) t, x, y, z = self.t, self.x, self.y, self.z flip_q = QH(qtype=end_qtype, representation=self.representation) if t != 0: flip_q.t = -1 * t if x != 0: flip_q.x = -1 * x if y != 0: flip_q.y = -1 * y if z != 0: flip_q.z = -1 * z return flip_q def vahlen_conj(self, conj_type="-", qtype="vc"): """Three types of conjugates -'* done by Vahlen in 1901.""" t, x, y, z = self.t, self.x, self.y, self.z conj_q = QH() if conj_type == '-': conj_q.t = t if x != 0: conj_q.x = -1 * x if y != 0: conj_q.y = -1 * y if z != 0: conj_q.z = -1 * z qtype += "*-" if conj_type == "'": conj_q.t = t if x != 0: conj_q.x = -1 * x if y != 0: conj_q.y = -1 * y conj_q.z = z qtype += "*'" if conj_type == '*': conj_q.t = t conj_q.x = x conj_q.y = y if z != 0: conj_q.z = -1 * z qtype += "*" conj_q.qtype = self.qtype + qtype conj_q.representation = self.representation return conj_q def _commuting_products(self, q1): """Returns a dictionary with the commuting products.""" s_t, s_x, s_y, s_z = self.t, self.x, self.y, self.z q1_t, q1_x, q1_y, q1_z = q1.t, q1.x, q1.y, q1.z products = {'tt': s_t * q1_t, 'xx+yy+zz': s_x * q1_x + s_y * q1_y + s_z * q1_z, 'tx+xt': s_t * q1_x + s_x * q1_t, 'ty+yt': s_t * q1_y + s_y * q1_t, 'tz+zt': s_t * q1_z + s_z * q1_t} return products def _anti_commuting_products(self, q1): """Returns a dictionary with the three anti-commuting products.""" s_x, s_y, s_z = self.x, self.y, self.z q1_x, q1_y, q1_z = q1.x, q1.y, q1.z products = {'yz-zy': s_y * q1_z - s_z * q1_y, 'zx-xz': s_z * q1_x - s_x * q1_z, 'xy-yx': s_x * q1_y - s_y * q1_x, 'zy-yz': - s_y * q1_z + s_z * q1_y, 'xz-zx': - s_z * q1_x + s_x * q1_z, 'yx-xy': - s_x * q1_y + s_y * q1_x } return products def _all_products(self, q1): """Returns a dictionary with all possible products.""" products = self._commuting_products(q1) products.update(self._anti_commuting_products(q1)) return products def square(self, qtype="^2"): """Square a quaternion.""" end_qtype = "{}{}".format(self.qtype, qtype) qxq = self._commuting_products(self) sq_q = QH(qtype=end_qtype, representation=self.representation) sq_q.t = qxq['tt'] - qxq['xx+yy+zz'] sq_q.x = qxq['tx+xt'] sq_q.y = qxq['ty+yt'] sq_q.z = qxq['tz+zt'] return sq_q def norm_squared(self, qtype="|| ||^2"): """The norm_squared of a quaternion.""" end_qtype = "||{}||^2".format(self.qtype, qtype) qxq = self._commuting_products(self) n_q = QH(qtype=end_qtype, representation=self.representation) n_q.t = qxq['tt'] + qxq['xx+yy+zz'] return n_q def norm_squared_of_vector(self, qtype="|V( )|^2"): """The norm_squared of the vector of a quaternion.""" end_qtype = "|V({})|^2".format(self.qtype) qxq = self._commuting_products(self) nv_q = QH(qtype=end_qtype, representation=self.representation) nv_q.t = qxq['xx+yy+zz'] return nv_q def abs_of_q(self, qtype="||"): """The absolute value, the square root of the norm_squared.""" end_qtype = "|{}|".format(self.qtype) a = self.norm_squared() sqrt_t = a.t ** (1/2) a.t = sqrt_t a.qtype = end_qtype a.representation = self.representation return a def normalize(self, qtype="U"): """Normalize a quaternion""" end_qtype = "{}{}".format(self.qtype, qtype) abs_q_inv = self.abs_of_q().invert() n_q = self.product(abs_q_inv) n_q.qtype = end_qtype n_q.representation = self.representation return n_q def abs_of_vector(self, qtype="|V( )|"): """The absolute value of the vector, the square root of the norm_squared of the vector.""" end_qtype = "|V({})|".format(self.qtype) av = self.norm_squared_of_vector(qtype=end_qtype) sqrt_t = av.t ** (1/2) av.t = sqrt_t av.representation = self.representation return av def add(self, qh_1, qtype=""): """Form a add given 2 quaternions.""" self.check_representations(qh_1) end_qtype = "{f}+{s}".format(f=self.qtype, s=qh_1.qtype) t_1, x_1, y_1, z_1 = self.t, self.x, self.y, self.z t_2, x_2, y_2, z_2 = qh_1.t, qh_1.x, qh_1.y, qh_1.z add_q = QH(qtype=end_qtype, representation=self.representation) add_q.t = t_1 + t_2 add_q.x = x_1 + x_2 add_q.y = y_1 + y_2 add_q.z = z_1 + z_2 return add_q def dif(self, qh_1, qtype=""): """Form a add given 2 quaternions.""" self.check_representations(qh_1) end_qtype = "{f}-{s}".format(f=self.qtype, s=qh_1.qtype) t_1, x_1, y_1, z_1 = self.t, self.x, self.y, self.z t_2, x_2, y_2, z_2 = qh_1.t, qh_1.x, qh_1.y, qh_1.z dif_q = QH(qtype=end_qtype, representation=self.representation) dif_q.t = t_1 - t_2 dif_q.x = x_1 - x_2 dif_q.y = y_1 - y_2 dif_q.z = z_1 - z_2 return dif_q def product(self, q1, kind="", reverse=False, qtype=""): """Form a product given 2 quaternions. Kind can be '' aka standard, even, odd, or even_minus_odd. Setting reverse=True is like changing the order.""" self.check_representations(q1) commuting = self._commuting_products(q1) q_even = QH() q_even.t = commuting['tt'] - commuting['xx+yy+zz'] q_even.x = commuting['tx+xt'] q_even.y = commuting['ty+yt'] q_even.z = commuting['tz+zt'] anti_commuting = self._anti_commuting_products(q1) q_odd = QH() if reverse: q_odd.x = anti_commuting['zy-yz'] q_odd.y = anti_commuting['xz-zx'] q_odd.z = anti_commuting['yx-xy'] else: q_odd.x = anti_commuting['yz-zy'] q_odd.y = anti_commuting['zx-xz'] q_odd.z = anti_commuting['xy-yx'] if kind == "": result = q_even.add(q_odd) times_symbol = "x" elif kind.lower() == "even": result = q_even times_symbol = "xE" elif kind.lower() == "odd": result = q_odd times_symbol = "xO" elif kind.lower() == "even_minus_odd": result = q_even.dif(q_odd) times_symbol = "xE-O" else: raise Exception("Four 'kind' values are known: '', 'even', 'odd', and 'even_minus_odd'.") if reverse: times_symbol = times_symbol.replace('x', 'xR') if qtype: result.qtype = qtype else: result.qtype = "{f}{ts}{s}".format(f=self.qtype, ts=times_symbol, s=q1.qtype) result.representation = self.representation return result def Euclidean_product(self, q1, kind="", reverse=False, qtype=""): """Form a product p* q given 2 quaternions, not associative.""" self.check_representations(q1) pq = QH(qtype, representation=self.representation) pq = self.conj().product(q1, kind, reverse) return pq def invert(self, qtype="^-1"): """The inverse of a quaternion.""" end_qtype = "{}{}".format(self.qtype, qtype) q_conj = self.conj() q_norm_squared = self.norm_squared() if q_norm_squared.t == 0: print("oops, zero on the norm_squared.") return self.q0() q_norm_squared_inv = QH([1.0 / q_norm_squared.t, 0, 0, 0]) q_inv = q_conj.product(q_norm_squared_inv) q_inv.qtype = end_qtype q_inv.representation = self.representation return q_inv def divide_by(self, q1, qtype=""): """Divide one quaternion by another. The order matters unless one is using a norm_squared (real number).""" self.check_representations(q1) end_qtype = "{f}/{s}".format(f=self.qtype, s=q1.qtype) q1_inv = q1.invert() q_div = self.product(q1.invert()) q_div.qtype = end_qtype q_div.representation = self.representation return q_div def triple_product(self, q1, q2): """Form a triple product given 3 quaternions.""" self.check_representations(q1) self.check_representations(q2) triple = self.product(q1).product(q2) triple.representation = self.representation return triple # Quaternion rotation involves a triple product: UQU∗ # where the U is a unitary quaternion (having a norm_squared of one). def rotate(self, a_1=0, a_2=0, a_3=0, qtype="rot"): """Do a rotation given up to three angles.""" end_qtype = "{}{}".format(self.qtype, qtype) u = QH([0, a_1, a_2, a_3]) u_abs = u.abs_of_q() u_norm_squaredalized = u.divide_by(u_abs) q_rot = u_norm_squaredalized.triple_product(self, u_norm_squaredalized.conj()) q_rot.qtype = end_qtype q_rot.representation = self.representation return q_rot # A boost also uses triple products like a rotation, but more of them. # This is not a well-known result, but does work. # b -> b' = h b h* + 1/2 ((hhb)* -(h*h*b)*) # where h is of the form (cosh(a), sinh(a)) def boost(self, beta_x=0, beta_y=0, beta_z=0, qtype="boost"): """A boost along the x, y, and/or z axis.""" end_qtype = "{}{}".format(self.qtype, qtype) boost = QH(sr_gamma_betas(beta_x, beta_y, beta_z)) b_conj = boost.conj() triple_1 = boost.triple_product(self, b_conj) triple_2 = boost.triple_product(boost, self).conj() triple_3 = b_conj.triple_product(b_conj, self).conj() triple_23 = triple_2.dif(triple_3) half_23 = triple_23.product(QH([0.5, 0, 0, 0])) triple_123 = triple_1.add(half_23, qtype=end_qtype) triple_123.qtype = end_qtype triple_123.representation = self.representation return triple_123 # g_shift is a function based on the space-times-time invariance proposal for gravity, # which proposes that if one changes the distance from a gravitational source, then # squares a measurement, the observers at two different hieghts agree to their # space-times-time values, but not the intervals. # g_form is the form of the function, either minimal or exponential # Minimal is what is needed to pass all weak field tests of gravity def g_shift(self, dimensionless_g, g_form="exp", qtype="g_shift"): """Shift an observation based on a dimensionless GM/c^2 dR.""" end_qtype = "{}{}".format(self.qtype, qtype) if g_form == "exp": g_factor = sp.exp(dimensionless_g) elif g_form == "minimal": g_factor = 1 + 2 * dimensionless_g + 2 * dimensionless_g ** 2 else: print("g_form not defined, should be 'exp' or 'minimal': {}".format(g_form)) return self g_q = QH(qtype=end_qtype) g_q.t = self.t / g_factor g_q.x = self.x * g_factor g_q.y = self.y * g_factor g_q.z = self.z * g_factor g_q.qtype = end_qtype g_q.representation = self.representation return g_q def sin(self, qtype="sin"): """Take the sine of a quaternion, (sin(t) cosh(|R|), cos(t) sinh(|R|) R/|R|)""" end_qtype = "sin({sq})".format(sq=self.qtype) abs_v = self.abs_of_vector() if abs_v.t == 0: return QH([math.sin(self.t), 0, 0, 0], qtype=end_qtype) sint = math.sin(self.t) cost = math.cos(self.t) sinhR = math.sinh(abs_v.t) coshR = math.cosh(abs_v.t) k = cost * sinhR / abs_v.t q_out = QH() q_out.t = sint * coshR q_out.x = k * self.x q_out.y = k * self.y q_out.z = k * self.z q_out.qtype = end_qtype q_out.representation = self.representation return q_out def cos(self, qtype="sin"): """Take the cosine of a quaternion, (cos(t) cosh(|R|), sin(t) sinh(|R|) R/|R|)""" end_qtype = "cos({sq})".format(sq=self.qtype) abs_v = self.abs_of_vector() if abs_v.t == 0: return QH([math.cos(self.t), 0, 0, 0], qtype=end_qtype) sint = math.sin(self.t) cost = math.cos(self.t) sinhR = math.sinh(abs_v.t) coshR = math.cosh(abs_v.t) k = -1 * sint * sinhR / abs_v.t q_out = QH() q_out.t = cost * coshR q_out.x = k * self.x q_out.y = k * self.y q_out.z = k * self.z q_out.qtype = end_qtype q_out.representation = self.representation return q_out def tan(self, qtype="sin"): """Take the tan of a quaternion, sin/cos""" end_qtype = "tan({sq})".format(sq=self.qtype) abs_v = self.abs_of_vector() if abs_v.t == 0: return QH([math.tan(self.t), 0, 0, 0], qtype=end_qtype) sinq = self.sin() cosq = self.cos() q_out = sinq.divide_by(cosq) q_out.qtype = end_qtype q_out.representation = self.representation return q_out def sinh(self, qtype="sinh"): """Take the sinh of a quaternion, (sinh(t) cos(|R|), cosh(t) sin(|R|) R/|R|)""" end_qtype = "sinh({sq})".format(sq=self.qtype) abs_v = self.abs_of_vector() if abs_v.t == 0: return QH([math.sinh(self.t), 0, 0, 0], qtype=end_qtype) sinht = math.sinh(self.t) cosht = math.cosh(self.t) sinR = math.sin(abs_v.t) cosR = math.cos(abs_v.t) k = cosht * sinR / abs_v.t q_out = QH(qtype=end_qtype, representation=self.representation) q_out.t = sinht * cosR q_out.x = k * self.x q_out.y = k * self.y q_out.z = k * self.z return q_out def cosh(self, qtype="sin"): """Take the cosh of a quaternion, (cosh(t) cos(|R|), sinh(t) sin(|R|) R/|R|)""" end_qtype = "cosh({sq})".format(sq=self.qtype) abs_v = self.abs_of_vector() if abs_v.t == 0: return QH([math.cosh(self.t), 0, 0, 0], qtype=end_qtype) sinht = math.sinh(self.t) cosht = math.cosh(self.t) sinR = math.sin(abs_v.t) cosR = math.cos(abs_v.t) k = sinht * sinR / abs_v.t q_out = QH(qtype=end_qtype, representation=self.representation) q_out.t = cosht * cosR q_out.x = k * self.x q_out.y = k * self.y q_out.z = k * self.z return q_out def tanh(self, qtype="tanh"): """Take the tanh of a quaternion, sin/cos""" end_qtype = "tanh({sq})".format(sq=self.qtype) abs_v = self.abs_of_vector() if abs_v.t == 0: return QH([math.tanh(self.t), 0, 0, 0], qtype=end_qtype) sinhq = self.sinh() coshq = self.cosh() q_out = sinhq.divide_by(coshq) q_out.qtype = end_qtype q_out.representation = self.representation return q_out def exp(self, qtype="exp"): """Take the exponential of a quaternion.""" # exp(q) = (exp(t) cos(|R|, exp(t) sin(|R|) R/|R|) end_qtype = "exp({st})".format(st=self.qtype) abs_v = self.abs_of_vector() et = math.exp(self.t) if (abs_v.t == 0): return QH([et, 0, 0, 0], qtype=end_qtype) cosR = math.cos(abs_v.t) sinR = math.sin(abs_v.t) k = et * sinR / abs_v.t expq = QH([et * cosR, k * self.x, k * self.y, k * self.z], qtype=end_qtype, representation=self.representation) return expq def ln(self, qtype="ln"): """Take the natural log of a quaternion.""" # ln(q) = (0.5 ln t^2 + R.R, atan2(|R|, t) R/|R|) end_qtype = "ln({st})".format(st=self.qtype) abs_v = self.abs_of_vector() if (abs_v.t == 0): if self.t > 0: return(QH([math.log(self.t), 0, 0, 0], qtype=end_qtype)) else: # I don't understant this, but mathematica does the same thing. return(QH([math.log(-self.t), math.pi, 0, 0], qtype=end_type)) return QH([lt, 0, 0, 0]) t_value = 0.5 * math.log(self.t * self.t + abs_v.t * abs_v.t) k = math.atan2(abs_v.t, self.t) / abs_v.t expq = QH([t_value, k * self.x, k * self.y, k * self.z], qtype=end_qtype, representation=self.representation) return expq def q_2_q(self, q1, qtype="P"): """Take the natural log of a quaternion.""" # q^p = exp(ln(q) * p) self.check_representations(q1) end_qtype = "{st}^P".format(st=self.qtype) q2q = self.ln().product(q1).exp() q2q.qtype = end_qtype q2q.representation = self.representation return q2q def trunc(self): """Truncates values.""" self.t = math.trunc(self.t) self.x = math.trunc(self.x) self.y = math.trunc(self.y) self.z = math.trunc(self.z) return self

Write tests the QH class.


In [4]:
class TestQH(unittest.TestCase):
    """Class to make sure all the functions work as expected."""

    Q = QH([1, -2, -3, -4], qtype="Q")
    P = QH([0, 4, -3, 0], qtype="P")
    R = QH([3, 0, 0, 0], qtype="R")
    C = QH([2, 4, 0, 0], qtype="C")

    def test_qt(self):
        self.assertTrue(self.Q.t == 1)

    def test_q_0(self):
        q_z = self.Q.q_0()
        print("q_0: ", q_z)
        self.assertTrue(q_z.t == 0)
        self.assertTrue(q_z.x == 0)
        self.assertTrue(q_z.y == 0)
        self.assertTrue(q_z.z == 0)

    def test_q_1(self):
        q_z = self.Q.q_1()
        print("q_1: ", q_z)
        self.assertTrue(q_z.t == 1)
        self.assertTrue(q_z.x == 0)
        self.assertTrue(q_z.y == 0)
        self.assertTrue(q_z.z == 0)

    def test_q_i(self):
        q_z = self.Q.q_i()
        print("q_i: ", q_z)
        self.assertTrue(q_z.t == 0)
        self.assertTrue(q_z.x == 1)
        self.assertTrue(q_z.y == 0)
        self.assertTrue(q_z.z == 0)

    def test_q_j(self):
        q_z = self.Q.q_j()
        print("q_j: ", q_z)
        self.assertTrue(q_z.t == 0)
        self.assertTrue(q_z.x == 0)
        self.assertTrue(q_z.y == 1)
        self.assertTrue(q_z.z == 0)

    def test_q_k(self):
        q_z = self.Q.q_k()
        print("q_k: ", q_z)
        self.assertTrue(q_z.t == 0)
        self.assertTrue(q_z.x == 0)
        self.assertTrue(q_z.y == 0)
        self.assertTrue(q_z.z == 1)
        
    def test_q_random(self):
        q_z = QH().q_random()
        print("q_random():", q_z)
        self.assertTrue(q_z.t >= 0 and q_z.t <= 1)
        self.assertTrue(q_z.x >= 0 and q_z.x <= 1)
        self.assertTrue(q_z.y >= 0 and q_z.y <= 1)
        self.assertTrue(q_z.z >= 0 and q_z.z <= 1)
        
    def test_equals(self):
        self.assertTrue(self.Q.equals(self.Q))
        self.assertFalse(self.Q.equals(self.P))

    def test_conj_0(self):
        q_z = self.Q.conj()
        print("q_conj 0: ", q_z)
        self.assertTrue(q_z.t == 1)
        self.assertTrue(q_z.x == 2)
        self.assertTrue(q_z.y == 3)
        self.assertTrue(q_z.z == 4)

    def test_conj_1(self):
        q_z = self.Q.conj(1)
        print("q_conj 1: ", q_z)
        self.assertTrue(q_z.t == -1)
        self.assertTrue(q_z.x == -2)
        self.assertTrue(q_z.y == 3)
        self.assertTrue(q_z.z == 4)

    def test_conj_2(self):
        q_z = self.Q.conj(2)
        print("q_conj 2: ", q_z)
        self.assertTrue(q_z.t == -1)
        self.assertTrue(q_z.x == 2)
        self.assertTrue(q_z.y == -3)
        self.assertTrue(q_z.z == 4)
        
    def sign_flips(self):
        q_z = self.Q.sign_flips()
        print("sign_flips: ", q_z)
        self.assertTrue(q_z.t == -1)
        self.assertTrue(q_z.x == 2)
        self.assertTrue(q_z.y == 3)
        self.assertTrue(q_z.z == 4)
    
    def test_vahlen_conj_minus(self):
        q_z = self.Q.vahlen_conj()
        print("q_vahlen_conj -: ", q_z)
        self.assertTrue(q_z.t == 1)
        self.assertTrue(q_z.x == 2)
        self.assertTrue(q_z.y == 3)
        self.assertTrue(q_z.z == 4)

    def test_vahlen_conj_star(self):
        q_z = self.Q.vahlen_conj('*')
        print("q_vahlen_conj *: ", q_z)
        self.assertTrue(q_z.t == 1)
        self.assertTrue(q_z.x == -2)
        self.assertTrue(q_z.y == -3)
        self.assertTrue(q_z.z == 4)

    def test_vahlen_conj_prime(self):
        q_z = self.Q.vahlen_conj("'")
        print("q_vahlen_conj ': ", q_z)
        self.assertTrue(q_z.t == 1)
        self.assertTrue(q_z.x == 2)
        self.assertTrue(q_z.y == 3)
        self.assertTrue(q_z.z == -4)

    def test_square(self):
        q_z = self.Q.square()
        print("square: ", q_z)
        self.assertTrue(q_z.t == -28)
        self.assertTrue(q_z.x == -4)
        self.assertTrue(q_z.y == -6)
        self.assertTrue(q_z.z == -8)

    def test_norm_squared(self):
        q_z = self.Q.norm_squared()
        print("norm_squared: ", q_z)
        self.assertTrue(q_z.t == 30)
        self.assertTrue(q_z.x == 0)
        self.assertTrue(q_z.y == 0)
        self.assertTrue(q_z.z == 0)

    def test_norm_squared_of_vector(self):
        q_z = self.Q.norm_squared_of_vector()
        print("norm_squared_of_vector: ", q_z)
        self.assertTrue(q_z.t == 29)
        self.assertTrue(q_z.x == 0)
        self.assertTrue(q_z.y == 0)
        self.assertTrue(q_z.z == 0)
        
    def test_abs_of_q(self):
        q_z = self.P.abs_of_q()
        print("abs_of_q: ", q_z)
        self.assertTrue(q_z.t == 5)
        self.assertTrue(q_z.x == 0)
        self.assertTrue(q_z.y == 0)
        self.assertTrue(q_z.z == 0)
        
    def test_normalize(self):
        q_z = self.P.normalize()
        print("q_normalized: ", q_z)
        self.assertTrue(q_z.t == 0)
        self.assertTrue(q_z.x == 0.8)
        self.assertAlmostEqual(q_z.y, -0.6)
        self.assertTrue(q_z.z == 0)
        
    def test_abs_of_vector(self):
        q_z = self.P.abs_of_vector()
        print("abs_of_vector: ", q_z)
        self.assertTrue(q_z.t == 5)
        self.assertTrue(q_z.x == 0)
        self.assertTrue(q_z.y == 0)
        self.assertTrue(q_z.z == 0)
        
    def test_add(self):
        q_z = self.Q.add(self.P)
        print("add: ", q_z)
        self.assertTrue(q_z.t == 1)
        self.assertTrue(q_z.x == 2)
        self.assertTrue(q_z.y == -6)
        self.assertTrue(q_z.z == -4)
        
    def test_dif(self):
        q_z = self.Q.dif(self.P)
        print("dif: ", q_z)
        self.assertTrue(q_z.t == 1)
        self.assertTrue(q_z.x == -6)
        self.assertTrue(q_z.y == 0)
        self.assertTrue(q_z.z == -4) 

    def test_product(self):
        q_z = self.Q.product(self.P)
        print("product: ", q_z)
        self.assertTrue(q_z.t == -1)
        self.assertTrue(q_z.x == -8)
        self.assertTrue(q_z.y == -19)
        self.assertTrue(q_z.z == 18)
        
    def test_product_even(self):
        q_z = self.Q.product(self.P, kind="even")
        print("product, kind even: ", q_z)
        self.assertTrue(q_z.t == -1)
        self.assertTrue(q_z.x == 4)
        self.assertTrue(q_z.y == -3)
        self.assertTrue(q_z.z == 0)
        
    def test_product_odd(self):
        q_z = self.Q.product(self.P, kind="odd")
        print("product, kind odd: ", q_z)
        self.assertTrue(q_z.t == 0)
        self.assertTrue(q_z.x == -12)
        self.assertTrue(q_z.y == -16)
        self.assertTrue(q_z.z == 18)
        
    def test_product_even_minus_odd(self):
        q_z = self.Q.product(self.P, kind="even_minus_odd")
        print("product, kind even_minus_odd: ", q_z)
        self.assertTrue(q_z.t == -1)
        self.assertTrue(q_z.x == 16)
        self.assertTrue(q_z.y == 13)
        self.assertTrue(q_z.z == -18)
        
    def test_product_reverse(self):
        q1q2_rev = self.Q.product(self.P, reverse=True)
        q2q1 = self.P.product(self.Q)
        self.assertTrue(q1q2_rev.equals(q2q1))
        
    def test_Euclidean_product(self):
        q_z = self.Q.Euclidean_product(self.P)
        print("Euclidean product: ", q_z)
        self.assertTrue(q_z.t == 1)
        self.assertTrue(q_z.x == 16)
        self.assertTrue(q_z.y == 13)
        self.assertTrue(q_z.z == -18)
        
    def test_invert(self):
        q_z = self.P.invert()
        print("invert: ", q_z)
        self.assertTrue(q_z.t == 0)
        self.assertTrue(q_z.x == -0.16)
        self.assertTrue(q_z.y == 0.12)
        self.assertTrue(q_z.z == 0)
                
    def test_divide_by(self):
        q_z = self.Q.divide_by(self.Q)
        print("divide_by: ", q_z)
        self.assertTrue(q_z.t == 1)
        self.assertTrue(q_z.x == 0)
        self.assertTrue(q_z.y == 0)
        self.assertTrue(q_z.z == 0) 
        
    def test_triple_product(self):
        q_z = self.Q.triple_product(self.P, self.Q)
        print("triple product: ", q_z)
        self.assertTrue(q_z.t == -2)
        self.assertTrue(q_z.x == 124)
        self.assertTrue(q_z.y == -84)
        self.assertTrue(q_z.z == 8)
        
    def test_rotate(self):
        q_z = self.Q.rotate(1)
        print("rotate: ", q_z)
        self.assertTrue(q_z.t == 1)
        self.assertTrue(q_z.x == -2)
        self.assertTrue(q_z.y == 3)
        self.assertTrue(q_z.z == 4)
        
    def test_boost(self):
        q1_sq = self.Q.square()
        q_z = self.Q.boost(0.003)
        q_z2 = q_z.square()
        print("q1_sq: ", q1_sq)
        print("boosted: ", q_z)
        print("boosted squared: ", q_z2)
        self.assertTrue(round(q_z2.t, 5) == round(q1_sq.t, 5))

    def test_g_shift(self):
        q1_sq = self.Q.square()
        q_z = self.Q.g_shift(0.003)
        q_z2 = q_z.square()
        q_z_minimal = self.Q.g_shift(0.003, g_form="minimal")
        q_z2_minimal = q_z_minimal.square()
        print("q1_sq: ", q1_sq)
        print("g_shift: ", q_z)
        print("g squared: ", q_z2)
        self.assertTrue(q_z2.t != q1_sq.t)
        self.assertTrue(q_z2.x == q1_sq.x)
        self.assertTrue(q_z2.y == q1_sq.y)
        self.assertTrue(q_z2.z == q1_sq.z)
        self.assertTrue(q_z2_minimal.t != q1_sq.t)
        self.assertTrue(q_z2_minimal.x == q1_sq.x)
        self.assertTrue(q_z2_minimal.y == q1_sq.y)
        self.assertTrue(q_z2_minimal.z == q1_sq.z)
        
    def test_sin(self):
        self.assertTrue(QH([0, 0, 0, 0]).sin().equals(QH().q_0()))
        self.assertTrue(self.Q.sin().equals(QH([91.7837157840346691, -21.8864868530291758, -32.8297302795437673, -43.7729737060583517])))
        self.assertTrue(self.P.sin().equals(QH([0,  59.3625684622310033, -44.5219263466732542, 0])))
        self.assertTrue(self.R.sin().equals(QH([0.1411200080598672, 0, 0, 0])))
        self.assertTrue(self.C.sin().equals(QH([24.8313058489463785, -11.3566127112181743, 0, 0])))
     
    def test_cos(self):
        self.assertTrue(QH([0, 0, 0, 0]).cos().equals(QH().q_1()))
        self.assertTrue(self.Q.cos().equals(QH([58.9336461679439481, 34.0861836904655959, 51.1292755356983974, 68.1723673809311919])))
        self.assertTrue(self.P.cos().equals(QH([74.2099485247878476, 0, 0, 0])))
        self.assertTrue(self.R.cos().equals(QH([-0.9899924966004454, 0, 0, 0])))
        self.assertTrue(self.C.cos().equals(QH([-11.3642347064010600, -24.8146514856341867, 0, 0])))
                            
    def test_tan(self):
        self.assertTrue(QH([0, 0, 0, 0]).tan().equals(QH().q_0()))
        self.assertTrue(self.Q.tan().equals(QH([0.0000382163172501, -0.3713971716439372, -0.5570957574659058, -0.7427943432878743])))
        self.assertTrue(self.P.tan().equals(QH([0, 0.7999273634100760, -0.5999455225575570, 0])))
        self.assertTrue(self.R.tan().equals(QH([-0.1425465430742778, 0, 0, 0])))
        self.assertTrue(self.C.tan().equals(QH([-0.0005079806234700, 1.0004385132020521, 0, 0])))
        
    def test_sinh(self):
        self.assertTrue(QH([0, 0, 0, 0]).sinh().equals(QH().q_0()))
        self.assertTrue(self.Q.sinh().equals(QH([0.7323376060463428, 0.4482074499805421, 0.6723111749708131, 0.8964148999610841])))
        self.assertTrue(self.P.sinh().equals(QH([0, -0.7671394197305108, 0.5753545647978831, 0])))
        self.assertTrue(self.R.sinh().equals(QH([10.0178749274099026, 0, 0, 0])))
        self.assertTrue(self.C.sinh().equals(QH([-2.3706741693520015, -2.8472390868488278, 0, 0])))    
        
    def test_cosh(self):
        self.assertTrue(QH([0, 0, 0, 0]).cosh().equals(QH().q_1()))
        self.assertTrue(self.Q.cosh().equals(QH([0.9615851176369565, 0.3413521745610167, 0.5120282618415251, 0.6827043491220334])))
        self.assertTrue(self.P.cosh().equals(QH([0.2836621854632263, 0, 0, 0])))
        self.assertTrue(self.R.cosh().equals(QH([10.0676619957777653, 0, 0, 0])))
        self.assertTrue(self.C.cosh().equals(QH([-2.4591352139173837, -2.7448170067921538, 0, 0])))    
        
    def test_tanh(self):
        self.assertTrue(QH([0, 0, 0, 0]).tanh().equals(QH().q_0()))
        self.assertTrue(self.Q.tanh().equals(QH([1.0248695360556623, 0.1022956817887642, 0.1534435226831462, 0.2045913635775283])))
        self.assertTrue(self.P.tanh().equals(QH([0, -2.7044120049972684, 2.0283090037479505, 0])))
        self.assertTrue(self.R.tanh().equals(QH([0.9950547536867305, 0, 0, 0])))
        self.assertTrue(self.C.tanh().equals(QH([1.0046823121902353, 0.0364233692474038, 0, 0])))    
        
    def test_exp(self):
        self.assertTrue(QH([0, 0, 0, 0]).exp().equals(QH().q_1()))
        self.assertTrue(self.Q.exp().equals(QH([1.6939227236832994, 0.7895596245415588, 1.1843394368123383, 1.5791192490831176])))
        self.assertTrue(self.P.exp().equals(QH([0.2836621854632263, -0.7671394197305108, 0.5753545647978831, 0])))
        self.assertTrue(self.R.exp().equals(QH([20.0855369231876679, 0, 0, 0])))
        self.assertTrue(self.C.exp().equals(QH([-4.8298093832693851, -5.5920560936409816, 0, 0])))    
    
    def test_ln(self):
        self.assertTrue(self.Q.ln().exp().equals(self.Q))
        self.assertTrue(self.Q.ln().equals(QH([1.7005986908310777, -0.5151902926640850, -0.7727854389961275, -1.0303805853281700])))
        self.assertTrue(self.P.ln().equals(QH([1.6094379124341003, 1.2566370614359172, -0.9424777960769379, 0])))
        self.assertTrue(self.R.ln().equals(QH([1.0986122886681098, 0, 0, 0])))
        self.assertTrue(self.C.ln().equals(QH([1.4978661367769954, 1.1071487177940904, 0, 0])))    
        
    def test_q_2_q(self):
        self.assertTrue(self.Q.q_2_q(self.P).equals(QH([-0.0197219653530713, -0.2613955437374326, 0.6496281248064009, -0.3265786562423951])))

suite = unittest.TestLoader().loadTestsFromModule(TestQH())
unittest.TextTestRunner().run(suite);


...........................................
Euclidean product:  (1, 16, 13, -18) Q*xP
abs_of_q:  (5.0, 0, 0, 0) |P|
abs_of_vector:  (5.0, 0, 0, 0) |V(P)|^2
add:  (1, 2, -6, -4) Q+P
q1_sq:  (-28, -4, -6, -8) Q^2
boosted:  (1.0120181081629736, -2.006036054324489, -3.0, -4.0) Qboost
boosted squared:  (-28.0, -4.0602896252083704, -6.072108648977841, -8.096144865303788) Qboost^2
q_conj 0:  (1, 2, 3, 4) Q*
q_conj 1:  (-1, -2, 3, 4) Q*1
q_conj 2:  (-1, 2, -3, 4) Q*2
dif:  (1, -6, 0, -4) Q-P
divide_by:  (1.0, 0.0, 0.0, 0.0) Q/Q
q1_sq:  (-28, -4, -6, -8) Q^2
g_shift:  (0.997004495503373, -2.00600900900675, -3.00901351351013, -4.01201801801351) Qg_shift
g squared:  (-28.1805050815139, -4.00000000000000, -6.00000000000000, -8.00000000000000) Qg_shift^2
invert:  (0.0, -0.16, 0.12, 0.0) P^-1
norm_squared:  (30, 0, 0, 0) ||Q||^2
norm_squared_of_vector:  (29, 0, 0, 0) |V(Q)|^2
q_normalized:  (0.0, 0.8, -0.6000000000000001, 0.0) PU
product:  (-1, -8, -19, 18) QxP
product, kind even:  (-1, 4, -3, 0) QxEP
product, kind even_minus_odd:  (-1, 16, 13, -18) QxE-OP
product, kind odd:  (0, -12, -16, 18) QxOP
q_0:  (0, 0, 0, 0) 0
q_1:  (1, 0, 0, 0) 1
q_i:  (0, 1, 0, 0) i
q_j:  (0, 0, 1, 0) j
q_k:  (0, 0, 0, 1) k
q_random(): (0.22742587358987465, 0.07639695607462316, 0.1111897931332777, 0.549200518973763) ?
rotate:  (1.0, -2.0, 3.0, 4.0) Qrot
square:  (-28, -4, -6, -8) Q^2
triple product:  (-2, 124, -84, 8) QxPxQ
q_vahlen_conj -:  (1, 2, 3, 4) Qvc*-
q_vahlen_conj ':  (1, 2, 3, -4) Qvc*'
q_vahlen_conj *:  (1, -2, -3, 4) Qvc*
----------------------------------------------------------------------
Ran 43 tests in 0.080s

OK

In [5]:
class TestQHRep(unittest.TestCase):
    Q12 = QH([1, 2, 0, 0])
    Q1123 = QH([1, 1, 2, 3])
    Q11p = QH([1, 1, 0, 0], representation="polar")
    Q12p = QH([1, 2, 0, 0], representation="polar")
    Q12np = QH([1, -2, 0, 0], representation="polar")
    Q21p = QH([2, 1, 0, 0], representation="polar")
    Q23p = QH([2, 3, 0, 0], representation="polar")
    Q13p = QH([1, 3, 0, 0], representation="polar")
    Q5p = QH([5, 0, 0, 0], representation="polar")
    
    def test_txyz_2_representation(self):
        qr = QH(self.Q12.txyz_2_representation(""))
        self.assertTrue(qr.equals(self.Q12))
        qr = QH(self.Q12.txyz_2_representation("polar"))
        self.assertTrue(qr.equals(QH([2.23606797749979, 1.10714871779409, 0, 0])))
        qr = QH(self.Q1123.txyz_2_representation("spherical"))
        self.assertTrue(qr.equals(QH([1.0, 3.7416573867739413, 0.640522312679424, 1.10714871779409])))
        
    def test_representation_2_txyz(self):
        qr = QH(self.Q12.representation_2_txyz(""))
        self.assertTrue(qr.equals(self.Q12))
        qr = QH(self.Q12.representation_2_txyz("polar"))
        self.assertTrue(qr.equals(QH([-0.4161468365471424, 0.9092974268256817, 0, 0])))
        qr = QH(self.Q1123.representation_2_txyz("spherical"))
        self.assertTrue(qr.equals(QH([1.0, -0.9001976297355174, 0.12832006020245673, -0.4161468365471424])))
    
    def test_polar_products(self):
        qr = self.Q11p.product(self.Q12p)
        print("polar 1 1 0 0 * 1 2 0 0: ", qr)
        self.assertTrue(qr.equals(self.Q13p))
        qr = self.Q12p.product(self.Q21p)
        print("polar 1 2 0 0 * 2 1 0 0: ", qr)
        self.assertTrue(qr.equals(self.Q23p))

    def test_polar_conj(self):
        qr = self.Q12p.conj()
        print("polar conj of 1 2 0 0: ", qr)
        self.assertTrue(qr.equals(self.Q12np))
        
suite = unittest.TestLoader().loadTestsFromModule(TestQHRep())
unittest.TextTestRunner().run(suite);


....
----------------------------------------------------------------------
Ran 4 tests in 0.011s

OK
polar conj of 1 2 0 0:  (1.0 A, -2.0 𝜈x, 0.0 𝜈y, 0.0 𝜈z) Q*
polar 1 1 0 0 * 1 2 0 0:  (1.0 A, 3.0 𝜈x, 0.0 𝜈y, 0.0 𝜈z) QxQ
polar 1 2 0 0 * 2 1 0 0:  (2.0 A, 3.0 𝜈x, 0.0 𝜈y, 0.0 𝜈z) QxQ

Numpy Arrays for Hamilton

A separate class is needed for numpy array due to technical issues I have getting sympy and numpy to play nicely with each other...


In [6]:
class QHa(object):
    """Quaternions as nparrays."""

    def __init__(self, values=None, qtype="Q", representation=""):
        if values is None:
            self.a = np.array([0.0, 0.0, 0.0, 0.0])
        elif len(values) == 4:
            self.a = np.array([values[0], values[1], values[2], values[3]])

        elif len(values) == 8:
            self.a = np.array([values[0] - values[1], values[2] - values[3], values[4] - values[5], values[6] - values[7]])
        
        self.representation = representation
        
        if representation != "":
            txyz = self.representation_2_txyz(representation)
            self.a = np.array(txyz)
            
        self.qtype = qtype

    def __str__(self):
        """Customize the output."""
        
        if self.representation == "":
            string = "({t}, {x}, {y}, {z}) {qt}".format(t=self.a[0], x=self.a[1], y=self.a[2], z=self.a[3], qt=self.qtype)
    
        elif self.representation == "polar":
            rep = self.txyz_2_representation("polar")
            
            string = "({A} A, {thetaX} 𝜈x, {thetaY} 𝜈y, {thetaZ} 𝜈z) {qt}".format(
                A=rep[0], thetaX=rep[1], thetaY=rep[2], thetaZ=rep[3], qt=self.qtype)
 
        elif self.representation == "spherical":
            rep = self.txyz_2_representation("spherical")
            string = "({t} t, {R} R, {theta} θ, {phi} φ) {qt}".format(t=rep[0], R=rep[1], theta=rep[2], phi=rep[3], 
                                                                       qt=self.qtype)
        
        return string
    
    def is_symbolic(self):
        """Figures out if an expression is symbolic."""
        
        symbolic = False
        
        if hasattr(self.a[0], "free_symbols") or hasattr(self.a[1], "free_symbols") or \
            hasattr(self.a[2], "free_symbols") or hasattr(self.a[3], "free_symbols"): 
            symbolic = True
        
        return symbolic
    
    def txyz_2_representation(self, representation):
        """Converts Cartesian txyz into an array of 4 values in a different representation."""

        symbolic = self.is_symbolic()
        
        if representation == "":
            rep = [self.a[0], self.a[1], self.a[2], self.a[3]]
        
        elif representation == "polar":
            amplitude = (self.a[0] ** 2 + self.a[1] ** 2 + self.a[2] ** 2 + self.a[3] ** 2) ** (1/2)
            
            abs_v = self.abs_of_vector().a[0]
            
            if symbolic:
                theta = sp.atan2(abs_v, self.a[0])
            else:
                theta = math.atan2(abs_v, self.a[0])
                
            if abs_v == 0:
                thetaX, thetaY, thetaZ = 0, 0, 0
                
            else:
                thetaX = theta * self.a[1] / abs_v
                thetaY = theta * self.a[2] / abs_v
                thetaZ = theta * self.a[3] / abs_v
            
            rep = [amplitude, thetaX, thetaY, thetaZ]
        
        elif representation == "spherical":
            
            t = self.a[0]
            
            R = (self.a[1] ** 2 + self.a[2] **2 + self.a[3] **2) ** (1/2)
            
            if R == 0:
                theta = 0
            else:
                if symbolic:
                    theta = sp.acos(self.a[3] / R)
                
                else:
                    theta = math.acos(self.a[3] / R)
                
            if symbolic:
                phi = sp.atan2(self.a[2], self.a[1])
            else:
                phi = math.atan2(self.a[2], self.a[1])
                
            rep = [t, R, theta, phi]
        
        else:
            print("Oops, don't know representation: ", representation)
            
        return rep
    
    def representation_2_txyz(self, representation):
        """Convert from a representation to Cartesian txyz."""
        
        symbolic = False
        
        if hasattr(self.a[0], "free_symbols") or hasattr(self.a[1], "free_symbols") or \
            hasattr(self.a[2], "free_symbols") or hasattr(self.a[3], "free_symbols"): 
            symbolic = True

        if representation == "":
            t, x, y, z = self.a[0], self.a[1], self.a[2], self.a[3]
        
        elif representation == "polar":
            amplitude, thetaX, thetaY, thetaZ = self.a[0], self.a[1], self.a[2], self.a[3]
        
            theta = (thetaX ** 2 + thetaY ** 2 + thetaZ ** 2) ** (1/2)
            
            if theta == 0:
                t = amplitude
                x, y, z = 0, 0, 0
                
            else:
                if symbolic:
                    t = amplitude * sp.cos(theta)
                    x = self.a[1] / theta * amplitude * sp.sin(theta)
                    y = self.a[2] / theta * amplitude * sp.sin(theta)
                    z = self.a[3] / theta * amplitude * sp.sin(theta)
                
                else:
                    t = amplitude * math.cos(theta)
                    x = self.a[1] / theta * amplitude * math.sin(theta)
                    y = self.a[2] / theta * amplitude * math.sin(theta)
                    z = self.a[3] / theta * amplitude * math.sin(theta)
                
        elif representation == "spherical":
            t, R, theta, phi = self.a[0], self.a[1], self.a[2], self.a[3]

            if symbolic:
                x = R * sp.sin(theta) * sp.cos(phi)
                y = R * sp.sin(theta) * sp.sin(phi)
                z = R * sp.cos(theta)
            else:
                x = R * math.sin(theta) * math.cos(phi)
                y = R * math.sin(theta) * math.sin(phi)
                z = R * math.cos(theta)
            
        else:
            print("Oops, don't know representation: ", representation)
            
        txyz = [t, x, y, z]
        
        return txyz 
        
    def check_representations(self, q1):
        """If they are the same, report true. If not, kick out an exception. Don't add apples to oranges."""

        if self.representation == q1.representation:
            return True
        
        else:
            raise Exception("Oops, 2 quaternions have different representations: {}, {}".format(self.representation, q1.representation))
            return False
    
    def display_q(self):
        """display each terms in a pretty way."""

        display(self.a[0])
        display(self.a[1])
        display(self.a[2])
        display(self.a[3])
        return

    def simple_q(self):
        """display each terms in a pretty way."""
        
        self.a[0] = sp.simplify(self.a[0])
        self.a[1] = sp.simplify(self.a[1])
        self.a[2] = sp.simplify(self.a[2])
        self.a[3] = sp.simplify(self.a[3])
        return
    
    def q_0(self, qtype="0"):
        """Return a zero quaternion."""

        q0 = QHa(qtype=qtype, representation=self.representation)
        return q0

    def q_1(self, qtype="1"):
        """Return a multiplicative identity quaternion."""
    
        q1 = QHa([1.0, 0.0, 0.0, 0.0], qtype=qtype, representation=self.representation)
        return q1
    
    def q_i(self, qtype="i"):
        """Return i."""

        qi = QHa([0.0, 1.0, 0.0, 0.0], qtype=qtype, representation=self.representation)
        return qi

    def q_j(self, qtype="j"):
        """Return j."""
        
        qj = QHa([0.0, 0.0, 1.0, 0.0], qtype=qtype, representation=self.representation)
        return qj
    
    def q_k(self, qtype="k"):
        """Return k."""

        qk = QHa([0.0, 0.0, 0.0, 1.0], qtype=qtype, representation=self.representation)
        return qk

    def q_random(self, qtype="?"):
        """Return a random-valued quaternion."""

        qr = QHa([random.random(), random.random(), random.random(), random.random()], qtype=qtype, representation=self.representation)
        return qr
    
    def dupe(self, qtype=""):
        """Return a duplicate copy, good for testing since qtypes persist"""
        
        return QHa([self.a[0], self.a[1], self.a[2], self.a[3]], qtype=self.qtype, representation=self.representation)
    
    def equals(self, q1):
        """Tests if two quaternions are equal."""
        
        self.check_representations(q1)
        
        self_t, self_x, self_y, self_z = sp.expand(self.a[0]), sp.expand(self.a[1]), sp.expand(self.a[2]), sp.expand(self.a[3])
        q1_t, q1_x, q1_y, q1_z = sp.expand(q1.a[0]), sp.expand(q1.a[1]), sp.expand(q1.a[2]), sp.expand(q1.a[3])
        
        if math.isclose(self_t, q1_t) and math.isclose(self_x, q1_x) and math.isclose(self_y, q1_y) and math.isclose(self_z, q1_z):
            return True
        
        else:
            return False
        
    def conj(self, conj_type=0, qtype="*"):
        """Three types of conjugates."""

        t, x, y, z = self.a[0], self.a[1], self.a[2], self.a[3]
        conj_q = QHa()

        if conj_type == 0:
            conj_q.a[0] = 1.0 * t
            if x != 0:
                conj_q.a[1] = -1.0 * x
            if y != 0:
                conj_q.a[2] = -1.0 * y
            if z != 0:
                conj_q.a[3] = -1.0 * z

        if conj_type == 1:
            if t != 0:
                conj_q.a[0] = -1.0 * t
            conj_q.a[1] = 1.0 * x
            if y != 0:
                conj_q.a[2] = -1.0 * y
            if z != 0:
                conj_q.a[3] = -1.0 * z
            qtype += "1"
            
        if conj_type == 2:
            if t != 0:
                conj_q.a[0] = -1 * t
            if x != 0:
                conj_q.a[1] = -1 * x
            conj_q.a[2] = 1.0 * y
            if z != 0:
                conj_q.a[3] = -1 * z
            qtype += "2"
            
        conj_q.qtype = self.qtype + qtype
        conj_q.representation = self.representation
    
        return conj_q
    
    def flip_signs(self, conj_type=0, qtype="-"):
        """Flip all the signs, just like multipying by -1."""

        end_qtype = "-{}".format(self.qtype)
        
        t, x, y, z = self.a[0], self.a[1], self.a[2], self.a[3]
        flip_q = QHa(qtype=end_qtype)

        if t != 0:
            flip_q.a[0] = -1.0 * t
        if x != 0:
            flip_q.a[1] = -1.0 * x
        if y != 0:
            flip_q.a[2] = -1.0 * y
        if z != 0:
            flip_q.a[3] = -1.0 * z
        
        flip_q.qtype = end_qtype
        flip_q.representation = self.representation
        
        return flip_q
    
    def vahlen_conj(self, conj_type="-", qtype="vc"):
        """Three types of conjugates -'* done by Vahlen in 1901."""

        t, x, y, z = self.a[0], self.a[1], self.a[2], self.a[3]
        conj_q = QHa()

        if conj_type == '-':
            conj_q.a[0] = 1.0 * t
            if x != 0:
                conj_q.a[1] = -1.0 * x
            if y != 0:
                conj_q.a[2] = -1.0 * y
            if z != 0:
                conj_q.a[3] = -1.0 * z
            qtype += "*-"

        if conj_type == "'":
            conj_q.a[0] = 1.0 * t
            if x != 0:
                conj_q.a[1] = -1.0 * x
            if y != 0:
                conj_q.a[2] = -1.0 * y
            conj_q.a[3] = 1.0 * z
            qtype += "*'"
            
        if conj_type == '*':
            conj_q.a[0] = 1.0 * t
            conj_q.a[1] = 1.0 * x
            conj_q.a[2] = 1.0 * y
            if z != 0:
                conj_q.a[3] = -1.0 * z
            qtype += "*"
            
        conj_q.qtype = self.qtype + qtype
        conj_q.representation = self.representation
        
        return conj_q

    def _commuting_products(self, q1):
        """Returns a dictionary with the commuting products."""

        s_t, s_x, s_y, s_z = self.a[0], self.a[1], self.a[2], self.a[3]
        q1_t, q1_x, q1_y, q1_z = q1.a[0], q1.a[1], q1.a[2], q1.a[3]

        products = {'tt': s_t * q1_t,
                    'xx+yy+zz': s_x * q1_x + s_y * q1_y + s_z * q1_z,
                    'tx+xt': s_t * q1_x + s_x * q1_t,
                    'ty+yt': s_t * q1_y + s_y * q1_t,
                    'tz+zt': s_t * q1_z + s_z * q1_t}

        return products

    def _anti_commuting_products(self, q1):
        """Returns a dictionary with the three anti-commuting products."""

        s_x, s_y, s_z = self.a[1], self.a[2], self.a[3]
        q1_x, q1_y, q1_z = q1.a[1], q1.a[2], q1.a[3]

        products = {'yz-zy': s_y * q1_z - s_z * q1_y,
                    'zx-xz': s_z * q1_x - s_x * q1_z,
                    'xy-yx': s_x * q1_y - s_y * q1_x,
                    'zy-yz': - s_y * q1_z + s_z * q1_y,
                    'xz-zx': - s_z * q1_x + s_x * q1_z,
                    'yx-xy': - s_x * q1_y + s_y * q1_x,
                    }

        return products

    def _all_products(self, q1):
        """Returns a dictionary with all possible products."""

        products = self._commuting_products(q1)
        products.update(self._anti_commuting_products(q1))

        return products

    def square(self, qtype="^2"):
        """Square a quaternion."""

        end_qtype = "{}{}".format(self.qtype, qtype)
        
        qxq = self._commuting_products(self)

        sq_q = QHa(qtype=end_qtype, representation=self.representation)
        sq_q.a[0] = qxq['tt'] - qxq['xx+yy+zz']
        sq_q.a[1] = qxq['tx+xt']
        sq_q.a[2] = qxq['ty+yt']
        sq_q.a[3] = qxq['tz+zt']

        sq_q.qtype = end_qtype
        
        return sq_q

    def norm_squared(self, qtype="|| ||^2"):
        """The norm_squared of a quaternion."""

        end_qtype = "||{}||^2".format(self.qtype, qtype)
        
        qxq = self._commuting_products(self)

        n_q = QHa(qtype=end_qtype)
        n_q.a[0] = qxq['tt'] + qxq['xx+yy+zz']

        n_q.qtype = end_qtype
        n_q.representation = self.representation
        
        return n_q

    def norm_squared_of_vector(self, qtype="norm_squaredV"):
        """The norm_squared of the vector of a quaternion."""

        end_qtype = "||V({})||".format(self.qtype)
        
        qxq = self._commuting_products(self)

        nv_q = QHa(qtype=end_qtype)
        nv_q.a[0] = qxq['xx+yy+zz']

        nv_q.qtype = end_qtype
        nv_q.representation = self.representation
        
        return nv_q

    def abs_of_q(self, qtype="| |"):
        """The absolute value, the square root of the norm_squared."""

        end_qtype = "|{}|".format(self.qtype)
        
        ns = self.norm_squared()
        sqrt_t = ns.a[0] ** (1/2)
        ns.a[0] = sqrt_t

        ns.qtype = end_qtype
        ns.representation = self.representation
        
        return ns

    def abs_of_vector(self, qtype="|V()|"):
        """The absolute value of the vector, the square root of the norm_squared of the vector."""

        end_qtype = "|V({})|".format(self.qtype)
        
        av = self.norm_squared_of_vector()
        sqrt_t = av.a[0] ** (1/2)
        av.a[0] = sqrt_t

        av.qtype = end_qtype
        av.representation = self.representation
        
        return av

    def normalize(self, qtype="U"):
        """Normalize a quaternion"""
        
        end_qtype = "{}{}".format(self.qtype, qtype)
        
        abs_q_inv = self.abs_of_q().invert()
        n_q = self.product(abs_q_inv)

        n_q.qtype = end_qtype
        n_q.representation = self.representation
        
        return n_q
    
    def add(self, q1, qtype=""):
        """Form a add given 2 quaternions."""

        self.check_representations(q1)
        
        t_1, x_1, y_1, z_1 = self.a[0], self.a[1], self.a[2], self.a[3]
        t_2, x_2, y_2, z_2 = q1.a[0], q1.a[1], q1.a[2], q1.a[3]

        add_q = QHa()
        add_q.a[0] = t_1 + t_2
        add_q.a[1] = x_1 + x_2
        add_q.a[2] = y_1 + y_2
        add_q.a[3] = z_1 + z_2
        
        add_q.qtype = "{f}+{s}".format(f=self.qtype, s=q1.qtype)
        add_q.representation = self.representation
        
        return add_q    

    def dif(self, q1, qtype=""):
        """Form a add given 2 quaternions."""

        self.check_representations(q1)
        
        t_1, x_1, y_1, z_1 = self.a[0], self.a[1], self.a[2], self.a[3]
        t_2, x_2, y_2, z_2 = q1.a[0], q1.a[1], q1.a[2], q1.a[3]

        dif_q = QHa()
        dif_q.a[0] = t_1 - t_2
        dif_q.a[1] = x_1 - x_2
        dif_q.a[2] = y_1 - y_2
        dif_q.a[3] = z_1 - z_2

        dif_q.qtype = "{f}-{s}".format(f=self.qtype, s=q1.qtype)
        dif_q.representation = self.representation
            
        return dif_q

    def product(self, q1, kind="", reverse=False, qtype=""):
        """Form a product given 2 quaternions: standard, even, odd, and even_minus_odd. If reverse=True, reverses the order."""

        self.check_representations(q1)
        
        commuting = self._commuting_products(q1)
        q_even = QHa()
        q_even.a[0] = commuting['tt'] - commuting['xx+yy+zz']
        q_even.a[1] = commuting['tx+xt']
        q_even.a[2] = commuting['ty+yt']
        q_even.a[3] = commuting['tz+zt']
        qxq = self._all_products(q1)
        
        anti_commuting = self._anti_commuting_products(q1)
        q_odd = QHa()
        
        if reverse:
            q_odd.a[1] = anti_commuting['zy-yz']
            q_odd.a[2] = anti_commuting['xz-zx']
            q_odd.a[3] = anti_commuting['yx-xy']
            
        else:
            q_odd.a[1] = anti_commuting['yz-zy']
            q_odd.a[2] = anti_commuting['zx-xz']
            q_odd.a[3] = anti_commuting['xy-yx']
        
        result = QHa()
        
        if kind == "":
            result = q_even.add(q_odd)
            times_symbol = "x"
        elif kind.lower() == "even":
            result = q_even
            times_symbol = "xE"
        elif kind.lower() == "odd":
            result = q_odd
            times_symbol = "xO"
        elif kind.lower() == "even_minus_odd":
            result = q_even.dif(q_odd)
            times_symbol = "xE-O"
        else:
            raise Exception("Four 'kind' values are known: '', 'even', 'odd', and 'even_minus_odd'.")
            
        if reverse:
            times_symbol = times_symbol.replace('x', 'xR')
            
        result.qtype = "{f}{ts}{s}".format(f=self.qtype, ts=times_symbol, s=q1.qtype)
        result.representation = self.representation
            
        return result
    
    def Euclidean_product(self, q1, kind="", reverse=False, qtype=""):
        """Form a product p* q given 2 quaternions, not associative."""

        self.check_representations(q1)
        
        pq = QHa()
        pq = self.conj().product(q1, kind, reverse, qtype)
            
        return pq
    
    def invert(self, qtype="^-1"):
        """The inverse of a quaternion."""

        end_qtype = "{}{}".format(self.qtype, qtype)
        
        q_conj = self.conj()
        q_norm_squared = self.norm_squared()

        if q_norm_squared.a[0] == 0:
            print("oops, zero on the norm_squared.")
            return self.q0()

        q_norm_squared_inv = QHa([1.0 / q_norm_squared.a[0], 0, 0, 0])
        q_inv = q_conj.product(q_norm_squared_inv, qtype=self.qtype)
        
        q_inv.qtype = end_qtype
        q_inv.representation = self.representation
        
        return q_inv

    def divide_by(self, q1, qtype=""):
        """Divide one quaternion by another. The order matters unless one is using a norm_squared (real number)."""
        
        self.check_representations(q1)
        
        q1_inv = q1.invert()
        q_div = self.product(q1.invert())
        
        q_div.qtype = "{f}/{s}".format(f=self.qtype, s=q1.qtype)
        q_div.representation = self.representation
            
        return q_div

    def triple_product(self, q1, q2):
        """Form a triple product given 3 quaternions."""

        self.check_representations(q1)
        
        triple = self.product(q1).product(q2)
        triple.representation = self.representation
        
        return triple

    # Quaternion rotation involves a triple product:  UQU∗
    # where the U is a unitary quaternion (having a norm_squared of one).
    def rotate(self, a_1=0, a_2=0, a_3=0, qtype="rot"):
        """Do a rotation given up to three angles."""

        end_qtype = "{}{}".format(self.qtype, qtype)
        
        u = QHa([0, a_1, a_2, a_3])
        u_abs = u.abs_of_q()
        u_norm_squaredalized = u.divide_by(u_abs)

        q_rot = u_norm_squaredalized.triple_product(self, u_norm_squaredalized.conj())
  
        q_rot.qtype = end_qtype
        q_rot.representation = self.representation
        
        return q_rot

    # A boost also uses triple products like a rotation, but more of them.
    # This is not a well-known result, but does work.
    # b -> b' = h b h* + 1/2 ((hhb)* -(h*h*b)*)
    # where h is of the form (cosh(a), sinh(a))
    def boost(self, beta_x=0.0, beta_y=0.0, beta_z=0.0, qtype="boost"):
        """A boost along the x, y, and/or z axis."""

        end_qtype = "{}{}".format(self.qtype, qtype)
        
        boost = QHa(sr_gamma_betas(beta_x, beta_y, beta_z))      
        b_conj = boost.conj()

        triple_1 = boost.triple_product(self, b_conj)
        triple_2 = boost.triple_product(boost, self).conj()
        triple_3 = b_conj.triple_product(b_conj, self).conj()
      
        triple_23 = triple_2.dif(triple_3)
        half_23 = triple_23.product(QHa([0.5, 0.0, 0.0, 0.0]))
        triple_123 = triple_1.add(half_23, qtype=self.qtype)
        
        triple_123.qtype = end_qtype
        triple_123.representation = self.representation
        
        return triple_123

    # g_shift is a function based on the space-times-time invariance proposal for gravity,
    # which proposes that if one changes the distance from a gravitational source, then
    # squares a measurement, the observers at two different hieghts agree to their
    # space-times-time values, but not the intervals.
    # g_form is the form of the function, either minimal or exponential
    # Minimal is what is needed to pass all weak field tests of gravity
    def g_shift(self, dimensionless_g, g_form="exp", qtype="g_shift"):
        """Shift an observation based on a dimensionless GM/c^2 dR."""

        end_qtype = "{}{}".format(self.qtype, qtype)
        
        if g_form == "exp":
            g_factor = sp.exp(dimensionless_g)
        elif g_form == "minimal":
            g_factor = 1 + 2 * dimensionless_g + 2 * dimensionless_g ** 2
        else:
            print("g_form not defined, should be 'exp' or 'minimal': {}".format(g_form))
            return self

        g_q = QHa(qtype=end_qtype)
        g_q.a[0] = self.a[0] / g_factor
        g_q.a[1] = self.a[1] * g_factor
        g_q.a[2] = self.a[2] * g_factor
        g_q.a[3] = self.a[3] * g_factor
        g_q.qtype = end_qtype
        g_q.representation = self.representation
        
        return g_q
    
    def sin(self, qtype="sin"):
        """Take the sine of a quaternion, (sin(t) cosh(|R|), cos(t) sinh(|R|) R/|R|)"""

        end_qtype = "sin({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()
        
        if abs_v.a[0] == 0:    
            return QHa([math.sin(self.a[0]), 0, 0, 0], qtype=end_qtype)
            
        sint = math.sin(self.a[0])
        cost = math.cos(self.a[0])
        sinhR = math.sinh(abs_v.a[0])
        coshR = math.cosh(abs_v.a[0])
        
        k = cost * sinhR / abs_v.a[0]
            
        q_out = QHa(qtype=end_qtype, representation=self.representation)
        q_out.a[0] = sint * coshR
        q_out.a[1] = k * self.a[1]
        q_out.a[2] = k * self.a[2]
        q_out.a[3] = k * self.a[3]

        return q_out
     
    def cos(self, qtype="sin"):
        """Take the cosine of a quaternion, (cos(t) cosh(|R|), sin(t) sinh(|R|) R/|R|)"""

        end_qtype = "cos({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()
        
        if abs_v.a[0] == 0:    
            return QHa([math.cos(self.a[0]), 0, 0, 0], qtype=end_qtype)
            
        sint = math.sin(self.a[0])
        cost = math.cos(self.a[0])
        sinhR = math.sinh(abs_v.a[0])
        coshR = math.cosh(abs_v.a[0])
        
        k = -1 * sint * sinhR / abs_v.a[0]
            
        q_out = QHa(qtype=end_qtype, representation=self.representation)
        q_out.a[0] = cost * coshR
        q_out.a[1] = k * self.a[1]
        q_out.a[2] = k * self.a[2]
        q_out.a[3] = k * self.a[3]
        
        return q_out
    
    def tan(self, qtype="tan"):
        """Take the tan of a quaternion, sin/cos"""

        end_qtype = "tan({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()
        
        if abs_v.a[0] == 0:    
            return QHa([math.tan(self.a[0]), 0, 0, 0], qtype=end_qtype)
            
        sinq = self.sin()
        cosq = self.cos()
            
        q_out = sinq.divide_by(cosq) 
        q_out.qtype = end_qtype
        q_out.representation = self.representation
        
        return q_out
    
    def sinh(self, qtype="sinh"):
        """Take the sinh of a quaternion, (sinh(t) cos(|R|), cosh(t) sin(|R|) R/|R|)"""

        end_qtype = "sinh({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()
        
        if abs_v.a[0] == 0:    
            return QHa([math.sinh(self.a[0]), 0, 0, 0], qtype=end_qtype)
            
        sinht = math.sinh(self.a[0])
        cosht = math.cosh(self.a[0])
        sinR = math.sin(abs_v.a[0])
        cosR = math.cos(abs_v.a[0])
        
        k = cosht * sinR / abs_v.a[0]
        
        q_out = QHa(qtype=end_qtype, representation=self.representation)
        q_out.a[0] = sinht * cosR
        q_out.a[1] = k * self.a[1]
        q_out.a[2] = k * self.a[2]
        q_out.a[3] = k * self.a[3]

        return q_out
     
    def cosh(self, qtype="cosh"):
        """Take the cosh of a quaternion, (cosh(t) cos(|R|), sinh(t) sin(|R|) R/|R|)"""

        end_qtype = "cosh({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()
        
        if abs_v.a[0] == 0:    
            return QHa([math.cosh(self.a[0]), 0, 0, 0], qtype=end_qtype)
            
        sinht = math.sinh(self.a[0])
        cosht = math.cosh(self.a[0])
        sinR = math.sin(abs_v.a[0])
        cosR = math.cos(abs_v.a[0])
        
        k = sinht * sinR / abs_v.a[0]
            
        q_out = QHa(qtype=end_qtype, representation=self.representation)
        q_out.a[0] = cosht * cosR
        q_out.a[1] = k * self.a[1]
        q_out.a[2] = k * self.a[2]
        q_out.a[3] = k * self.a[3]

        return q_out
    
    def tanh(self, qtype="tanh"):
        """Take the tanh of a quaternion, sin/cos"""

        end_qtype = "tanh({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()
        
        if abs_v.a[0] == 0:    
            return QHa([math.tanh(self.a[0]), 0, 0, 0], qtype=end_qtype)
            
        sinhq = self.sinh()
        coshq = self.cosh()
            
        q_out = sinhq.divide_by(coshq) 
        q_out.qtype = end_qtype
        q_out.representation = self.representation
        
        return q_out
    
    def exp(self, qtype="exp"):
        """Take the exponential of a quaternion."""
        # exp(q) = (exp(t) cos(|R|, exp(t) sin(|R|) R/|R|)
        
        end_qtype = "exp({st})".format(st=self.qtype)
        
        abs_v = self.abs_of_vector()
        et = math.exp(self.a[0])
        
        if (abs_v.a[0] == 0):
            return QHa([et, 0, 0, 0], qtype=end_qtype, representation=self.representation)
        
        cosR = math.cos(abs_v.a[0])
        sinR = math.sin(abs_v.a[0])
        k = et * sinR / abs_v.a[0]
                       
        expq = QHa([et * cosR, k * self.a[1], k * self.a[2], k * self.a[3]], qtype=end_qtype, representation=self.representation)
        
        return expq
    
    def ln(self, qtype="ln"):
        """Take the natural log of a quaternion."""
        # ln(q) = (0.5 ln t^2 + R.R, atan2(|R|, t) R/|R|)
        
        end_qtype = "ln({st})".format(st=self.qtype)
        
        abs_v = self.abs_of_vector()
        
        if (abs_v.a[0] == 0):
            if self.a[0] > 0:
                return(QHa([math.log(self.a[0]), 0, 0, 0], qtype=end_qtype, representation=self.representation))
            else:
                # I don't understant this, but mathematica does the same thing.
                return(QHa([math.log(-self.a[0]), math.pi, 0, 0], qtype=end_qtype, representation=self.representation))   
            
            return QHa([lt, 0, 0, 0])
        
        t_value = 0.5 * math.log(self.a[0] * self.a[0] + abs_v.a[0] * abs_v.a[0])
        k = math.atan2(abs_v.a[0], self.a[0]) / abs_v.a[0]
                       
        expq = QHa([t_value, k * self.a[1], k * self.a[2], k * self.a[3]], qtype=end_qtype)
        
        return expq
    
    def q_2_q(self, q1, qtype="^P"):
        """Take the natural log of a quaternion."""
        # q^p = exp(ln(q) * p)
        
        self.check_representations(q1)
        
        end_qtype = "{st}^P".format(st=self.qtype)
        
        q2q = self.ln().product(q1).exp()
        q2q.qtype = end_qtype
        q2q.representation = self.representation
        
        return q2q
    
    def trunc(self):
        """Truncates values."""
        
        self.a[0] = math.trunc(self.a[0])
        self.a[1] = math.trunc(self.a[1])
        self.a[2] = math.trunc(self.a[2])
        self.a[3] = math.trunc(self.a[3])
        
        return self

In [7]:
class TestQHa(unittest.TestCase):
    """Class to make sure all the functions work as expected."""

    Q = QHa([1.0, -2.0, -3.0, -4.0], qtype="Q")
    P = QHa([0.0, 4.0, -3.0, 0.0], qtype="P")
    R = QHa([3.0, 0, 0, 0])
    C = QHa([2.0, 4.0, 0, 0])
    
    def test_qt(self):
        self.assertTrue(self.Q.a[0] == 1)

    def test_q_0(self):
        q_z = self.Q.q_0()
        print("q_0: ", q_z)
        self.assertTrue(q_z.a[0] == 0)
        self.assertTrue(q_z.a[1] == 0)
        self.assertTrue(q_z.a[2] == 0)
        self.assertTrue(q_z.a[3] == 0)

    def test_q_1(self):
        q_z = self.Q.q_1()
        print("q_1: ", q_z)
        self.assertTrue(q_z.a[0] == 1)
        self.assertTrue(q_z.a[1] == 0)
        self.assertTrue(q_z.a[2] == 0)
        self.assertTrue(q_z.a[3] == 0)
    
    def test_q_i(self):
        q_z = self.Q.q_i()
        print("q_i: ", q_z)
        self.assertTrue(q_z.a[0] == 0)
        self.assertTrue(q_z.a[1] == 1)
        self.assertTrue(q_z.a[2] == 0)
        self.assertTrue(q_z.a[3] == 0)

    def test_q_j(self):
        q_z = self.Q.q_j()
        print("q_1: ", q_z)
        self.assertTrue(q_z.a[0] == 0)
        self.assertTrue(q_z.a[1] == 0)
        self.assertTrue(q_z.a[2] == 1)
        self.assertTrue(q_z.a[3] == 0)

    def test_q_k(self):
        q_z = self.Q.q_k()
        print("q_k: ", q_z)
        self.assertTrue(q_z.a[0] == 0)
        self.assertTrue(q_z.a[1] == 0)
        self.assertTrue(q_z.a[2] == 0)
        self.assertTrue(q_z.a[3] == 1)
    
    def test_q_random(self):
        q_z = self.Q.q_random()
        print("q_random(): ", q_z)
        self.assertTrue(q_z.a[0] >= 0 and q_z.a[0] <= 1)
        self.assertTrue(q_z.a[1] >= 0 and q_z.a[1] <= 1)
        self.assertTrue(q_z.a[2] >= 0 and q_z.a[2] <= 1)
        self.assertTrue(q_z.a[3] >= 0 and q_z.a[3] <= 1)
            
    def test_equals(self):
        self.assertTrue(self.Q.equals(self.Q))
        self.assertFalse(self.Q.equals(self.P))

    def test_conj_0(self):
        q_z = self.Q.conj()
        print("q_conj 0: ", q_z)
        self.assertTrue(q_z.a[0] == 1)
        self.assertTrue(q_z.a[1] == 2)
        self.assertTrue(q_z.a[2] == 3)
        self.assertTrue(q_z.a[3] == 4)

    def test_conj_1(self):
        q_z = self.Q.conj(1)
        print("q_conj 1: ", q_z)
        self.assertTrue(q_z.a[0] == -1)
        self.assertTrue(q_z.a[1] == -2)
        self.assertTrue(q_z.a[2] == 3)
        self.assertTrue(q_z.a[3] == 4)

    def test_conj_2(self):
        q_z = self.Q.conj(2)
        print("q_conj 2: ", q_z)
        self.assertTrue(q_z.a[0] == -1)
        self.assertTrue(q_z.a[1] == 2)
        self.assertTrue(q_z.a[2] == -3)
        self.assertTrue(q_z.a[3] == 4)

    def test_vahlen_conj_minus(self):
        q_z = self.Q.vahlen_conj()
        print("q_vahlen_conj -: ", q_z)
        self.assertTrue(q_z.a[0] == 1)
        self.assertTrue(q_z.a[1] == 2)
        self.assertTrue(q_z.a[2] == 3)
        self.assertTrue(q_z.a[3] == 4)

    def test_vahlen_conj_star(self):
        q_z = self.Q.vahlen_conj('*')
        print("q_vahlen_conj *: ", q_z)
        self.assertTrue(q_z.a[0] == 1)
        self.assertTrue(q_z.a[1] == -2)
        self.assertTrue(q_z.a[2] == -3)
        self.assertTrue(q_z.a[3] == 4)

    def test_vahlen_conj_prime(self):
        q_z = self.Q.vahlen_conj("'")
        print("q_vahlen_conj ': ", q_z)
        self.assertTrue(q_z.a[0] == 1)
        self.assertTrue(q_z.a[1] == 2)
        self.assertTrue(q_z.a[2] == 3)
        self.assertTrue(q_z.a[3] == -4)

    def test_square(self):
        q_z = self.Q.square()
        print("square: ", q_z)
        self.assertTrue(q_z.a[0] == -28)
        self.assertTrue(q_z.a[1] == -4)
        self.assertTrue(q_z.a[2] == -6)
        self.assertTrue(q_z.a[3] == -8)

    def test_norm_squared(self):
        q_z = self.Q.norm_squared()
        print("norm_squared: ", q_z)
        self.assertTrue(q_z.a[0] == 30)
        self.assertTrue(q_z.a[1] == 0)
        self.assertTrue(q_z.a[2] == 0)
        self.assertTrue(q_z.a[3] == 0)

    def test_norm_squared_of_vector(self):
        q_z = self.Q.norm_squared_of_vector()
        print("norm_squared_of_vector: ", q_z)
        self.assertTrue(q_z.a[0] == 29)
        self.assertTrue(q_z.a[1] == 0)
        self.assertTrue(q_z.a[2] == 0)
        self.assertTrue(q_z.a[3] == 0)
        
    def test_abs_of_q(self):
        q_z = self.P.abs_of_q()
        print("abs_of_q: ", q_z)
        self.assertTrue(q_z.a[0] == 5.0)
        self.assertTrue(q_z.a[1] == 0.0)
        self.assertTrue(q_z.a[2] == 0.0)
        self.assertTrue(q_z.a[3] == 0.0)
        
    def test_abs_of_vector(self):
        q_z = self.P.abs_of_vector()
        print("abs_of_vector: ", q_z)
        self.assertTrue(q_z.a[0] == 5)
        self.assertTrue(q_z.a[1] == 0)
        self.assertTrue(q_z.a[2] == 0)
        self.assertTrue(q_z.a[3] == 0)
    
    def test_normalize(self):
        q_z = self.P.normalize()
        print("q_normalized: ", q_z)
        self.assertTrue(q_z.a[0] == 0)
        self.assertTrue(q_z.a[1] == 0.8)
        self.assertAlmostEqual(q_z.a[2], -0.6)
        self.assertTrue(q_z.a[3] == 0)
    
    def test_add(self):
        q_z = self.Q.add(self.P)
        print("add: ", q_z)
        self.assertTrue(q_z.a[0] == 1)
        self.assertTrue(q_z.a[1] == 2)
        self.assertTrue(q_z.a[2] == -6)
        self.assertTrue(q_z.a[3] == -4)
        
    def test_dif(self):
        q_z = self.Q.dif(self.P)
        print("dif: ", q_z)
        self.assertTrue(q_z.a[0] == 1)
        self.assertTrue(q_z.a[1] == -6)
        self.assertTrue(q_z.a[2] == 0)
        self.assertTrue(q_z.a[3] == -4) 

    def test_product(self):
        q_z = self.Q.product(self.P)
        print("product: ", q_z)
        self.assertTrue(q_z.a[0] == -1)
        self.assertTrue(q_z.a[1] == -8)
        self.assertTrue(q_z.a[2] == -19)
        self.assertTrue(q_z.a[3] == 18)
        
    def test_product_even(self):
        q_z = self.Q.product(self.P, kind="even")
        print("product even: ", q_z)
        self.assertTrue(q_z.a[0] == -1)
        self.assertTrue(q_z.a[1] == 4)
        self.assertTrue(q_z.a[2] == -3)
        self.assertTrue(q_z.a[3] == 0)
        
    def test_product_odd(self):
        q_z = self.Q.product(self.P, kind="odd")
        print("product odd: ", q_z)
        self.assertTrue(q_z.a[0] == 0)
        self.assertTrue(q_z.a[1] == -12)
        self.assertTrue(q_z.a[2] == -16)
        self.assertTrue(q_z.a[3] == 18)
        
    def test_product_reverse(self):
        q1q2_rev = self.Q.product(self.P, reverse=True)
        q2q1 = self.P.product(self.Q)
        self.assertTrue(q1q2_rev.equals(q2q1))

    def test_product_even_minus_odd(self):
        q_z = self.Q.product(self.P, kind="even_minus_odd")
        print("product even_minus_odd: ", q_z)
        self.assertTrue(q_z.a[0] == -1)
        self.assertTrue(q_z.a[1] == 16)
        self.assertTrue(q_z.a[2] == 13)
        self.assertTrue(q_z.a[3] == -18)
        
    def test_Euclidean_product(self):
        q_z = self.Q.Euclidean_product(self.P)
        print("Euclidean product: ", q_z)
        self.assertTrue(q_z.a[0] == 1)
        self.assertTrue(q_z.a[1] == 16)
        self.assertTrue(q_z.a[2] == 13)
        self.assertTrue(q_z.a[3] == -18)

    def test_invert(self):
        q_z = self.P.invert()
        print("invert: ", q_z)
        self.assertTrue(q_z.a[0] == 0)
        self.assertTrue(q_z.a[1] == -0.16)
        self.assertTrue(q_z.a[2] == 0.12)
        self.assertTrue(q_z.a[3] == 0)
                
    def test_divide_by(self):
        q_z = self.Q.divide_by(self.Q)
        print("divide_by: ", q_z)
        self.assertTrue(q_z.a[0] == 1)
        self.assertTrue(q_z.a[1] == 0)
        self.assertTrue(q_z.a[2] == 0)
        self.assertTrue(q_z.a[3] == 0) 
        
    def test_triple_product(self):
        q_z = self.Q.triple_product(self.P, self.Q)
        print("triple product: ", q_z)
        self.assertTrue(q_z.a[0] == -2)
        self.assertTrue(q_z.a[1] == 124)
        self.assertTrue(q_z.a[2] == -84)
        self.assertTrue(q_z.a[3] == 8)
        
    def test_rotate(self):
        q_z = self.Q.rotate(1)
        print("rotate: ", q_z)
        self.assertTrue(q_z.a[0] == 1)
        self.assertTrue(q_z.a[1] == -2)
        self.assertTrue(q_z.a[2] == 3)
        self.assertTrue(q_z.a[3] == 4)
        
    def test_boost(self):
        q1_sq = self.Q.square()
        q_z = self.Q.boost(0.003)
        q_z2 = q_z.square()
        print("q1_sq: ".format(q1_sq))
        print("boosted: ", q_z)
        print("boosted squared: ".format(q_z2))
        print("{}")
        self.assertTrue(round(q_z2.a[0], 5) == round(q1_sq.a[0], 5))

    def test_g_shift(self):
        q1_sq = self.Q.square()
        q_z = self.Q.g_shift(0.003)
        q_z2 = q_z.square()
        q_z_minimal = self.Q.g_shift(0.003, g_form="minimal")
        q_z2_minimal = q_z_minimal.square()
        print("q1_sq: ".format(q1_sq))
        print("g_shift: ", q_z)
        print("g squared: ".format(q_z2))
        self.assertTrue(q_z2.a[0] != q1_sq.a[0])
        self.assertTrue(q_z2.a[1] == q1_sq.a[1])
        self.assertTrue(q_z2.a[2] == q1_sq.a[2])
        self.assertTrue(q_z2.a[3] == q1_sq.a[3])
        self.assertTrue(q_z2_minimal.a[0] != q1_sq.a[0])
        self.assertTrue(q_z2_minimal.a[1] == q1_sq.a[1])
        self.assertTrue(q_z2_minimal.a[2] == q1_sq.a[2])
        self.assertTrue(q_z2_minimal.a[3] == q1_sq.a[3])
        
    def test_sin(self):
        self.assertTrue(QHa([0, 0, 0, 0]).sin().equals(QHa().q_0()))
        self.assertTrue(self.Q.sin().equals(QHa([91.7837157840346691, -21.8864868530291758, -32.8297302795437673, -43.7729737060583517])))
        self.assertTrue(self.P.sin().equals(QHa([0,  59.3625684622310033, -44.5219263466732542, 0])))
        self.assertTrue(self.R.sin().equals(QHa([0.1411200080598672, 0, 0, 0])))
        self.assertTrue(self.C.sin().equals(QHa([24.8313058489463785, -11.3566127112181743, 0, 0])))
     
    def test_cos(self):
        self.assertTrue(QHa([0, 0, 0, 0]).cos().equals(QHa().q_1()))
        self.assertTrue(self.Q.cos().equals(QHa([58.9336461679439481, 34.0861836904655959, 51.1292755356983974, 68.1723673809311919])))
        self.assertTrue(self.P.cos().equals(QHa([74.2099485247878476, 0, 0, 0])))
        self.assertTrue(self.R.cos().equals(QHa([-0.9899924966004454, 0, 0, 0])))
        self.assertTrue(self.C.cos().equals(QHa([-11.3642347064010600, -24.8146514856341867, 0, 0])))
                            
    def test_tan(self):
        self.assertTrue(QHa([0, 0, 0, 0]).tan().equals(QHa().q_0()))
        self.assertTrue(self.Q.tan().equals(QHa([0.0000382163172501, -0.3713971716439372, -0.5570957574659058, -0.7427943432878743])))
        self.assertTrue(self.P.tan().equals(QHa([0, 0.7999273634100760, -0.5999455225575570, 0])))
        self.assertTrue(self.R.tan().equals(QHa([-0.1425465430742778, 0, 0, 0])))
        self.assertTrue(self.C.tan().equals(QHa([-0.0005079806234700, 1.0004385132020521, 0, 0])))
        
    def test_sinh(self):
        self.assertTrue(QHa([0, 0, 0, 0]).sinh().equals(QHa().q_0()))
        self.assertTrue(self.Q.sinh().equals(QHa([0.7323376060463428, 0.4482074499805421, 0.6723111749708131, 0.8964148999610841])))
        self.assertTrue(self.P.sinh().equals(QHa([0, -0.7671394197305108, 0.5753545647978831, 0])))
        self.assertTrue(self.R.sinh().equals(QHa([10.0178749274099026, 0, 0, 0])))
        self.assertTrue(self.C.sinh().equals(QHa([-2.3706741693520015, -2.8472390868488278, 0, 0])))    
        
    def test_cosh(self):
        self.assertTrue(QHa([0, 0, 0, 0]).cosh().equals(QHa().q_1()))
        self.assertTrue(self.Q.cosh().equals(QHa([0.9615851176369565, 0.3413521745610167, 0.5120282618415251, 0.6827043491220334])))
        self.assertTrue(self.P.cosh().equals(QHa([0.2836621854632263, 0, 0, 0])))
        self.assertTrue(self.R.cosh().equals(QHa([10.0676619957777653, 0, 0, 0])))
        self.assertTrue(self.C.cosh().equals(QHa([-2.4591352139173837, -2.7448170067921538, 0, 0])))    
        
    def test_tanh(self):
        self.assertTrue(QHa([0, 0, 0, 0]).tanh().equals(QHa().q_0()))
        self.assertTrue(self.Q.tanh().equals(QHa([1.0248695360556623, 0.1022956817887642, 0.1534435226831462, 0.2045913635775283])))
        self.assertTrue(self.P.tanh().equals(QHa([0, -2.7044120049972684, 2.0283090037479505, 0])))
        self.assertTrue(self.R.tanh().equals(QHa([0.9950547536867305, 0, 0, 0])))
        self.assertTrue(self.C.tanh().equals(QHa([1.0046823121902353, 0.0364233692474038, 0, 0])))    
        
    def test_exp(self):
        self.assertTrue(QHa([0, 0, 0, 0]).exp().equals(QHa().q_1()))
        self.assertTrue(self.Q.exp().equals(QHa([1.6939227236832994, 0.7895596245415588, 1.1843394368123383, 1.5791192490831176])))
        self.assertTrue(self.P.exp().equals(QHa([0.2836621854632263, -0.7671394197305108, 0.5753545647978831, 0])))
        self.assertTrue(self.R.exp().equals(QHa([20.0855369231876679, 0, 0, 0])))
        self.assertTrue(self.C.exp().equals(QHa([-4.8298093832693851, -5.5920560936409816, 0, 0])))    
    
    def test_ln(self):
        self.assertTrue(self.Q.ln().exp().equals(self.Q))
        self.assertTrue(self.Q.ln().equals(QHa([1.7005986908310777, -0.5151902926640850, -0.7727854389961275, -1.0303805853281700])))
        self.assertTrue(self.P.ln().equals(QHa([1.6094379124341003, 1.2566370614359172, -0.9424777960769379, 0])))
        self.assertTrue(self.R.ln().equals(QHa([1.0986122886681098, 0, 0, 0])))
        self.assertTrue(self.C.ln().equals(QHa([1.4978661367769954, 1.1071487177940904, 0, 0])))    
        
    def test_q_2_q(self):
        self.assertTrue(self.Q.q_2_q(self.P).equals(QHa([-0.0197219653530713, -0.2613955437374326, 0.6496281248064009, -0.3265786562423951])))

suite = unittest.TestLoader().loadTestsFromModule(TestQHa())
unittest.TextTestRunner().run(suite);


...........................................
Euclidean product:  (1.0, 16.0, 13.0, -18.0) Q*xP
abs_of_q:  (5.0, 0.0, 0.0, 0.0) |P|
abs_of_vector:  (5.0, 0.0, 0.0, 0.0) |V(P)|
add:  (1.0, 2.0, -6.0, -4.0) Q+P
q1_sq: 
boosted:  (1.0120181081629736, -2.006036054324489, -3.0, -4.0) Qboost
boosted squared: 
{}
q_conj 0:  (1.0, 2.0, 3.0, 4.0) Q*
q_conj 1:  (-1.0, -2.0, 3.0, 4.0) Q*1
q_conj 2:  (-1.0, 2.0, -3.0, 4.0) Q*2
dif:  (1.0, -6.0, 0.0, -4.0) Q-P
divide_by:  (1.0, 0.0, 0.0, 0.0) Q/Q
q1_sq: 
g_shift:  (0.9970044955033729, -2.006009009006754, -3.0090135135101312, -4.012018018013508) Qg_shift
g squared: 
invert:  (0.0, -0.16, 0.12, 0.0) P^-1
norm_squared:  (30.0, 0.0, 0.0, 0.0) ||Q||^2
norm_squared_of_vector:  (29.0, 0.0, 0.0, 0.0) ||V(Q)||
q_normalized:  (0.0, 0.8, -0.6000000000000001, 0.0) PU
product:  (-1.0, -8.0, -19.0, 18.0) QxP
product even:  (-1.0, 4.0, -3.0, 0.0) QxEP
product even_minus_odd:  (-1.0, 16.0, 13.0, -18.0) QxE-OP
product odd:  (0.0, -12.0, -16.0, 18.0) QxOP
q_0:  (0.0, 0.0, 0.0, 0.0) 0
q_1:  (1.0, 0.0, 0.0, 0.0) 1
q_i:  (0.0, 1.0, 0.0, 0.0) i
q_1:  (0.0, 0.0, 1.0, 0.0) j
q_k:  (0.0, 0.0, 0.0, 1.0) k
q_random():  (0.8329465195276711, 0.5093547292621532, 0.527419681336517, 0.4273757400810684) ?
rotate:  (1.0, -2.0, 3.0, 4.0) Qrot
square:  (-28.0, -4.0, -6.0, -8.0) Q^2
triple product:  (-2.0, 124.0, -84.0, 8.0) QxPxQ
q_vahlen_conj -:  (1.0, 2.0, 3.0, 4.0) Qvc*-
q_vahlen_conj ':  (1.0, 2.0, 3.0, -4.0) Qvc*'
q_vahlen_conj *:  (1.0, -2.0, -3.0, 4.0) Qvc*
----------------------------------------------------------------------
Ran 43 tests in 0.083s

OK

In [8]:
class TestQHaRep(unittest.TestCase):
    Q12 = QHa([1, 2, 0, 0])
    Q1123 = QHa([1, 1, 2, 3])
    Q11p = QHa([1, 1, 0, 0], representation="polar")
    Q12p = QHa([1, 2, 0, 0], representation="polar")
    Q12np = QHa([1, -2, 0, 0], representation="polar")
    Q21p = QHa([2, 1, 0, 0], representation="polar")
    Q23p = QHa([2, 3, 0, 0], representation="polar")
    Q13p = QHa([1, 3, 0, 0], representation="polar")
    Q5p = QHa([5, 0, 0, 0], representation="polar")
    
    def test_txyz_2_representation(self):
        qr = QHa(self.Q12.txyz_2_representation(""))
        self.assertTrue(qr.equals(self.Q12))
        qr = QHa(self.Q12.txyz_2_representation("polar"))
        self.assertTrue(qr.equals(QHa([2.23606797749979, 1.10714871779409, 0, 0])))
        qr = QHa(self.Q1123.txyz_2_representation("spherical"))
        self.assertTrue(qr.equals(QHa([1.0, 3.7416573867739413, 0.640522312679424, 1.10714871779409])))
        
    def test_representation_2_txyz(self):
        qr = QHa(self.Q12.representation_2_txyz(""))
        self.assertTrue(qr.equals(self.Q12))
        qr = QHa(self.Q12.representation_2_txyz("polar"))
        self.assertTrue(qr.equals(QHa([-0.4161468365471424, 0.9092974268256817, 0, 0])))
        qr = QHa(self.Q1123.representation_2_txyz("spherical"))
        self.assertTrue(qr.equals(QHa([1.0, -0.9001976297355174, 0.12832006020245673, -0.4161468365471424])))
    
    def test_polar_products(self):
        qr = self.Q11p.product(self.Q12p)
        print("polar 1 1 0 0 * 1 2 0 0: ", qr)
        self.assertTrue(qr.equals(self.Q13p))
        qr = self.Q12p.product(self.Q21p)
        print("polar 1 2 0 0 * 2 1 0 0: ", qr)
        self.assertTrue(qr.equals(self.Q23p))

    def test_polar_conj(self):
        qr = self.Q12p.conj()
        print("polar conj of {}: {}", self.Q12p, qr)
        print("Q12np: ", self.Q12np)
        self.assertTrue(qr.equals(self.Q12np))
        
suite = unittest.TestLoader().loadTestsFromModule(TestQHaRep())
unittest.TextTestRunner().run(suite);


....
----------------------------------------------------------------------
Ran 4 tests in 0.016s

OK
polar conj of {}: {} (1.0 A, 2.0 𝜈x, 0.0 𝜈y, 0.0 𝜈z) Q (1.0 A, -2.0 𝜈x, 0.0 𝜈y, 0.0 𝜈z) Q*
Q12np:  (1.0 A, -2.0 𝜈x, 0.0 𝜈y, 0.0 𝜈z) Q
polar 1 1 0 0 * 1 2 0 0:  (1.0 A, 3.0 𝜈x, 0.0 𝜈y, 0.0 𝜈z) QxQ
polar 1 2 0 0 * 2 1 0 0:  (2.0 A, 3.0 𝜈x, 0.0 𝜈y, 0.0 𝜈z) QxQ

Using More Numbers via Doublets

My long term goal is to deal with quaternions on a quaternion manifold. This will have 4 pairs of doublets. Each doublet is paired with its additive inverse. Instead of using real numbers, one uses (3, 0) and (0, 2) to represent +3 and -2 respectively. Numbers such as (5, 6) are allowed. That can be "reduced" to (0, 1). My sense is that somewhere deep in the depths of relativistic quantum field theory, this will be a "good thing". For now, it is a minor pain to program.


In [9]:
class Doublet(object):
    """A pair of number that are additive inverses. It can take
    ints, floats, Symbols, or strings."""
    
    def __init__(self, numbers=None):
        
        if numbers is None:
            self.p = 0
            self.n = 0
            
        elif isinstance(numbers, (int, float)):
            if numbers < 0:
                self.n = -1 * numbers
                self.p = 0
            else:
                self.p = numbers
                self.n = 0
        
        elif isinstance(numbers, sp.Symbol):
            self.p = numbers
            self.n = 0
            
        elif isinstance(numbers, list):
            if len(numbers) == 2:
                self.p, self.n = numbers[0], numbers[1]
                      
        elif isinstance(numbers, str):
            n_list = numbers.split()
            
            if (len(n_list) == 1):
                if n_list.isnumeric():
                    n_value = float(numbers)
                      
                    if n_value < 0:
                        self.n = -1 * n_list[0]
                        self.p = 0
                      
                    else:
                        self.p = n_list[0]
                        self.n = 0
                        
                else:
                    self.p = sp.Symbol(n_list[0])
                    self.n = 0
                      
            if (len(n_list) == 2):
                if n_list[0].isnumeric():
                    self.p = float(n_list[0])
                else:
                    self.p = sp.Symbol(n_list[0])
                    
                if n_list[1].isnumeric():
                    self.n = float(n_list[1])
                else:
                    self.n = sp.Symbol(n_list[1])
        else:
            print ("unable to parse this Double.")

    def __str__(self):
        """Customize the output."""
        return "{p}p  {n}n".format(p=self.p, n=self.n)
        
    def d_add(self, d1):
        """Add a doublet to another."""
                        
        pa0, n0 = self.p, self.n
        p1, n1 = d1.p, d1.n
                        
        return Doublet([pa0 + p1, n0 + n1])

    def d_reduce(self):
        """If p and n are not zero, subtract """
        if self.p == 0 or self.n == 0:
            return Doublet([self.p, self.n])
        
        elif self.p > self.n:
            return Doublet([self.p - self.n, 0])
        
        elif self.p < self.n:
            return Doublet([0, self.n - self.p])
        
        else:
            return Doublet()
        
    def d_additive_inverse_up_to_an_automorphism(self, n=0):
        """Creates one additive inverses up to an arbitrary positive n."""
        
        if n == 0:
            return Doublet([self.n + n, self.p + n])
        else:
            red = self.d_reduce()
            return Doublet([red.n + n, red.p + n])
                        
    def d_dif(self, d1, n=0):
        """Take the difference by flipping and adding."""
        d2 = d1.d_additive_inverse_up_to_an_automorphism(n)
                        
        return self.d_add(d2)

    def d_equals(self, d1):
        """Figure out if two doublets are equal up to an equivalence relation."""
        
        self_red = self.d_reduce()
        d1_red = d1.d_reduce()
        
        if math.isclose(self_red.p, d1_red.p) and math.isclose(self_red.n, d1_red.n):
            return True
        
        else:
            return False
        
    def Z2_product(self, d1):
        """Uset the Abelian cyclic group Z2 to form the product of 2 doublets."""
        p1 = self.p * d1.p + self.n * d1.n
        n1 = self.p * d1.n + self.n * d1.p
        
        return Doublet([p1, n1])

In [10]:
class TestDoublet(unittest.TestCase):
    """Class to make sure all the functions work as expected."""
    
    d1 = Doublet()
    d2 = Doublet(2)
    d3 = Doublet(-3)
    d4 = Doublet([5, 3])
    dstr12 = Doublet("1 2")
    dstr13 = Doublet("3 2")
    
    def test_null(self):
        self.assertTrue(self.d1.p == 0)
        self.assertTrue(self.d1.n == 0)
       
    def test_2(self):
        self.assertTrue(self.d2.p == 2)
        self.assertTrue(self.d2.n == 0)
        
    def test_3(self):
        self.assertTrue(self.d3.p == 0)
        self.assertTrue(self.d3.n == 3)
    
    def test_str12(self):
        self.assertTrue(self.dstr12.p == 1)
        self.assertTrue(self.dstr12.n == 2)
    
    def test_add(self):
        d_add = self.d2.d_add(self.d3)
        self.assertTrue(d_add.p == 2)
        self.assertTrue(d_add.n == 3)
        
    def test_d_additive_inverse_up_to_an_automorphism(self):
        d_f = self.d2.d_additive_inverse_up_to_an_automorphism()
        self.assertTrue(d_f.p == 0)
        self.assertTrue(d_f.n == 2)
        
    def test_dif(self):
        d_d = self.d2.d_dif(self.d3)
        self.assertTrue(d_d.p == 5)
        self.assertTrue(d_d.n == 0)
            
    def test_reduce(self):
        d_add = self.d2.d_add(self.d3)
        d_r = d_add.d_reduce()
        self.assertTrue(d_r.p == 0)
        self.assertTrue(d_r.n == 1)
        
    def test_Z2_product(self):
        Z2p = self.dstr12.Z2_product(self.dstr13)
        self.assertTrue(Z2p.p == 7)
        self.assertTrue(Z2p.n == 8)

    def test_d_equals(self):
        self.assertTrue(self.d2.d_equals(self.d4))
        self.assertFalse(self.d2.d_equals(self.d1))
        
    def test_reduced_product(self):
        """Reduce before or after, should make no difference."""
        Z2p_1 = self.dstr12.Z2_product(self.dstr13)
        Z2p_red = Z2p_1.d_reduce()
        d_r_1 = self.dstr12.d_reduce()
        d_r_2 = self.dstr13.d_reduce()
        Z2p_2 = d_r_1.Z2_product(d_r_2)
        self.assertTrue(Z2p_red.p == Z2p_2.p)
        self.assertTrue(Z2p_red.n == Z2p_2.n)
        
suite = unittest.TestLoader().loadTestsFromModule(TestDoublet())
unittest.TextTestRunner().run(suite);


...........
----------------------------------------------------------------------
Ran 11 tests in 0.012s

OK

Repeat the exercise for arrays.


In [11]:
class Doubleta(object):
    """A pair of number that are additive inverses. It can take
    ints, floats, Symbols, or strings."""
    
    def __init__(self, numbers=None):
        
        if numbers is None:
            self.d = np.array([0.0, 0.0])
            
        elif isinstance(numbers, (int, float)):
            if numbers < 0:
                self.d = np.array([0, -1 * numbers])
            else:
                self.d = np.array([numbers, 0])
                        
        elif isinstance(numbers, sp.Symbol):
            self.d = np.array([numbers, 0])
            
        elif isinstance(numbers, list):
            
            if len(numbers) == 2:
                self.d = np.array([numbers[0], numbers[1]])
                      
        elif isinstance(numbers, str):
            n_list = numbers.split()
            
            if (len(n_list) == 1):
                if n_list.isnumeric():
                    n_value = float(numbers)
                      
                    if n_value < 0:
                        self.d = np.array([0, -1 * n_list[0]])
                      
                    else:
                        self.d = np.array([n_list[0], 0])
                        
                else:
                    self.d = np.array([sp.Symbol(n_list[0]), 0])
                      
            if (len(n_list) == 2):
                if n_list[0].isnumeric():
                    self.d = np.array([float(n_list[0]), float(n_list[1])])
                else:
                    self.d = np.array([sp.Symbol(n_list[0]), sp.Symbol(n_list[1])]) 
        else:
            print ("unable to parse this Double.")

    def __str__(self):
        """Customize the output."""
        return "{p}p  {n}n".format(p=self.d[0], n=self.d[1])
        
    def d_add(self, d1):
        """Add a doublet to another."""
                        
        pa0, n0 = self.d[0], self.d[1]
        p1, n1 = d1.d[0], d1.d[1]
                        
        return Doubleta([pa0 + p1, n0 + n1])

    def d_reduce(self):
        """If p and n are not zero, subtract """
        if self.d[0] == 0 or self.d[1] == 0:
            return Doubleta([self.d[0], self.d[1]])
        
        elif self.d[0] > self.d[1]:
            return Doubleta([self.d[0] - self.d[1], 0])
        
        elif self.d[0] < self.d[1]:
            return Doubleta([0, self.d[1] - self.d[0]])
        
        else:
            return Doubleta()
        
    def d_additive_inverse_up_to_an_automorphism(self, n=0):
        """Creates one additive inverses up to an arbitrary positive n."""
        
        if n == 0:
            return Doubleta([self.d[1], self.d[0]])
        else:
            red = self.d_reduce()
            return Doubleta([red.d[1] + n, red.d[0] + n])
                        
    def d_dif(self, d1, n=0):
        """Take the difference by flipping and adding."""
        d2 = d1.d_additive_inverse_up_to_an_automorphism(n)
                        
        return self.d_add(d2)

    def d_equals(self, d1):
        """See if two are equals up to an constant value."""
        
        self_red = self.d_reduce()
        d1_red = d1.d_reduce()
        
        if math.isclose(self_red.d[0], d1_red.d[0]) and math.isclose(self_red.d[1], d1_red.d[1]):
            return True
        
        else:
            return False
    
    def Z2_product(self, d1):
        """Uset the Abelian cyclic group Z2 to form the product of 2 doublets."""
        p1 = self.d[0] * d1.d[0] + self.d[1] * d1.d[1]
        n1 = self.d[0] * d1.d[1] + self.d[1] * d1.d[0]
        
        return Doubleta([p1, n1])

In [12]:
class TestDoubleta(unittest.TestCase):
    """Class to make sure all the functions work as expected."""
    
    d1 = Doubleta()
    d2 = Doubleta(2)
    d3 = Doubleta(-3)
    d4 = Doubleta([5, 3])
    dstr12 = Doubleta("1 2")
    dstr13 = Doubleta("3 2")
    
    def test_null(self):
        self.assertTrue(self.d1.d[0] == 0)
        self.assertTrue(self.d1.d[1] == 0)
       
    def test_2(self):
        self.assertTrue(self.d2.d[0] == 2)
        self.assertTrue(self.d2.d[1] == 0)
        
    def test_3(self):
        self.assertTrue(self.d3.d[0] == 0)
        self.assertTrue(self.d3.d[1] == 3)
    
    def test_str12(self):
        self.assertTrue(self.dstr12.d[0] == 1)
        self.assertTrue(self.dstr12.d[1] == 2)
    
    def test_add(self):
        d_add = self.d2.d_add(self.d3)
        self.assertTrue(d_add.d[0] == 2)
        self.assertTrue(d_add.d[1] == 3)
        
    def test_d_additive_inverse_up_to_an_automorphism(self):
        d_f = self.d2.d_additive_inverse_up_to_an_automorphism()
        self.assertTrue(d_f.d[0] == 0)
        self.assertTrue(d_f.d[1] == 2)
        
    def test_dif(self):
        d_d = self.d2.d_dif(self.d3)
        self.assertTrue(d_d.d[0] == 5)
        self.assertTrue(d_d.d[1] == 0)
            
    def test_reduce(self):
        d_add = self.d2.d_add(self.d3)
        d_r = d_add.d_reduce()
        self.assertTrue(d_r.d[0] == 0)
        self.assertTrue(d_r.d[1] == 1)
        
    def test_Z2_product(self):
        Z2p = self.dstr12.Z2_product(self.dstr13)
        self.assertTrue(Z2p.d[0] == 7)
        self.assertTrue(Z2p.d[1] == 8)

    def test_d_equals(self):
        self.assertTrue(self.d2.d_equals(self.d4))
        self.assertFalse(self.d2.d_equals(self.d1))
        
    def test_reduced_product(self):
        """Reduce before or after, should make no difference."""
        Z2p_1 = self.dstr12.Z2_product(self.dstr13)
        Z2p_red = Z2p_1.d_reduce()
        d_r_1 = self.dstr12.d_reduce()
        d_r_2 = self.dstr13.d_reduce()
        Z2p_2 = d_r_1.Z2_product(d_r_2)
        self.assertTrue(Z2p_red.d[0] == Z2p_2.d[0])
        self.assertTrue(Z2p_red.d[1] == Z2p_2.d[1])

In [13]:
suite = unittest.TestLoader().loadTestsFromModule(TestDoubleta())
unittest.TextTestRunner().run(suite);


...........
----------------------------------------------------------------------
Ran 11 tests in 0.013s

OK

Quaternion Group Q8

Write a class to handle quaternions given 8 numbers.


In [14]:
class Q8(object):
    """Quaternions on a quaternion manifold or space-time numbers."""

    def __init__(self, values=None, qtype="Q", representation=""):
        if values is None:
            self.dt, self.dx, self.dy, self.dz = Doublet(), Doublet(),Doublet(), Doublet()
        elif isinstance(values, list):
            if len(values) == 4:
                self.dt = Doublet(values[0])
                self.dx = Doublet(values[1])
                self.dy = Doublet(values[2])
                self.dz = Doublet(values[3])
        
            if len(values) == 8:
                self.dt = Doublet([values[0], values[1]])
                self.dx = Doublet([values[2], values[3]])
                self.dy = Doublet([values[4], values[5]])
                self.dz = Doublet([values[6], values[7]])
                
        self.representation = representation
        
        if representation != "":
            self.dt.p, self.dt.n, self.dx.p, self.dx.n, self.dy.p, self.dy.n, self.dz.p, self.dz.n = self.representation_2_txyz(representation)
                
        self.qtype=qtype
                
    def __str__(self):
        """Customize the output."""
        
        if self.representation == "":
            string = "(({tp}, {tn}), ({xp}, {xn}), ({yp}, {yn}), ({zp}, {zn})) {qt}".format(tp=self.dt.p, tn=self.dt.n, 
                                                             xp=self.dx.p, xn=self.dx.n, 
                                                             yp=self.dy.p, yn=self.dy.n, 
                                                             zp=self.dz.p, zn=self.dz.n,
                                                             qt=self.qtype)
    
        elif self.representation == "polar":
            rep = self.txyz_2_representation("polar")
            string = "(({Ap}, {An}) A, ({thetaXp}, {thetaXn})  𝜈x, ({thetaYp}, {thetaYn}) 𝜈y, ({thetaZp}, {thetaZn}) 𝜈z) {qt}".format(
                Ap=rep[0], An=rep[1], 
                thetaXp=rep[2], thetaXn=rep[3], 
                thetaYp=rep[4], thetaYn=rep[5], 
                thetaZp=rep[6], thetaZn=rep[7], qt=self.qtype)
 
        elif self.representation == "spherical":
            rep = self.txyz_2_representation("spherical")
            string = "(({tp}, {tn}) t, ({Rp}, {Rn}) R, ({thetap}, {thetan}) θ , ({phip}, {phin}) φ) {qt}".format(
                tp=rep[0], tn=rep[1], 
                Rp=rep[2], Rn=rep[3], 
                thetap=rep[4], thetan=rep[5], 
                phip=rep[6], phin=rep[7], qt=self.qtype)
            
        return string 

    def is_symbolic(self):
        """Looks to see if a symbol is inside one of the terms."""
        
        symbolic = False
        
        if hasattr(self.dt.p, "free_symbols") or hasattr(self.dt.n, "free_symbols") or \
            hasattr(self.dx.p, "free_symbols") or hasattr(self.dx.n, "free_symbols") or \
            hasattr(self.dy.p, "free_symbols") or hasattr(self.dy.n, "free_symbols") or \
            hasattr(self.dz.p, "free_symbols") or hasattr(self.dz.n, "free_symbols"): 
            symbolic = True
            
        return symbolic
        
    def txyz_2_representation(self, representation):
        """Converts Cartesian txyz into an array of 4 values in a different representation."""

        symbolic = self.is_symbolic()
                
        if representation == "":
            rep = [self.dt.p, self.dt.n, self.dx.p, self.dx.n, self.dy.p, self.dy.n, self.dz.p, self.dz.n]
            return rep
        
        elif representation == "polar":
            
            dtr = self.dt.p - self.dt.n
            dxr = self.dx.p - self.dx.n
            dyr = self.dy.p - self.dy.n
            dzr = self.dz.p - self.dz.n
            
            amplitude = (dtr ** 2 + dxr ** 2 + dyr **2 + dzr **2) ** (1/2)
            damp = Doublet(amplitude)
            
            abs_v = self.abs_of_vector().dt.p
            
            if symbolic:
                theta = sp.atan2(abs_v, dtr)
            else:
                theta = math.atan2(abs_v, dtr)
                
            if abs_v == 0:
                dthetaX, dthetaY, dthetaZ = Doublet(), Doublet(), Doublet()
                
            else:
                thetaX = dxr / abs_v * theta
                thetaY = dyr / abs_v * theta
                thetaZ = dzr / abs_v * theta
                
                dthetaX = Doublet(thetaX)
                dthetaY = Doublet(thetaY)
                dthetaZ = Doublet(thetaZ)
                
            rep = [damp.p, damp.n, dthetaX.p, dthetaX.n, dthetaY.p, dthetaY.n, dthetaZ.p, dthetaZ.n]
            return rep
        
        elif representation == "spherical":
            
            dtr = self.dt.p - self.dt.n
            dxr = self.dx.p - self.dx.n
            dyr = self.dy.p - self.dy.n
            dzr = self.dz.p - self.dz.n
            
            dt = self.dt
            
            R =(dxr ** 2 + dyr **2 + dzr**2) ** (1/2)
            
            if symbolic:
                theta = sp.acos(dzr / R)
                phi = sp.atan2(dyr, dxr)
            
            else:
                theta = math.acos(dzr / R)
                phi = math.atan2(dyr, dxr)

            dR = Doublet(R)
            dtheta = Doublet(theta)
            dphi = Doublet(phi)
            
            rep = [dt.p, dt.n, dR.p, dR.n, dtheta.p, dtheta.n, dphi.p, dphi.n] 
            return rep
        
        else:
            print("Oops, don't know representation: ", representation)
        
    def representation_2_txyz(self, representation):
        """Convert from a representation to Cartesian txyz."""
        
        symbolic = self.is_symbolic()

        if representation == "":
            dt, dx, dy, dz = self.dt, self.dx, self.dy, self.dz
        
        elif representation == "polar":
                                
            amplitude, thetaX, thetaY, thetaZ = self.dt, self.dx, self.dy, self.dz
    
            amp = amplitude.p - amplitude.n
            thetaXr = thetaX.p - thetaX.n
            thetaYr = thetaY.p - thetaY.n
            thetaZr = thetaZ.p - thetaZ.n
                                
            theta = (thetaXr ** 2 + thetaYr ** 2 + thetaZr ** 2) ** (1/2)
                
            if theta == 0:
                dt = amplitude
                dx, dy, dz = Doublet(), Doublet(), Doublet()
            
            else:
                if symbolic:
                    t = amp * sp.cos(theta)
                    x = thetaXr / theta * amp * sp.sin(theta)
                    y = thetaYr / theta * amp * sp.sin(theta)
                    z = thetaZr / theta * amp * sp.sin(theta)
                else:
                    t = amp * math.cos(theta)
                    x = thetaXr / theta * amp * math.sin(theta)
                    y = thetaYr / theta * amp * math.sin(theta)
                    z = thetaZr / theta * amp * math.sin(theta)
                    
                dt = Doublet(t)
                dx = Doublet(x)
                dy = Doublet(y)
                dz = Doublet(z)
                                
        elif representation == "spherical":
            dt, R, theta, phi = self.dt, self.dx, self.dy, self.dz

            Rr = R.p - R.n
            thetar = theta.p - theta.n
            phir = phi.p - phi.n
            
            if symbolic:
                x = Rr * sp.sin(thetar) * sp.cos(phir)
                y = Rr * sp.sin(thetar) * sp.sin(phir)
                z = Rr * sp.cos(thetar)
                
            else:
                x = Rr * math.sin(thetar) * math.cos(phir)
                y = Rr * math.sin(thetar) * math.sin(phir)
                z = Rr * math.cos(thetar)

                dx = Doublet(x)
                dy = Doublet(y)
                dz = Doublet(z)

        else:
            print("Oops, don't know representation: ", representation)
            
        txyz = [dt.p, dt.n, dx.p, dx.n, dy.p, dy.n, dz.p, dz.n]
        
        return txyz 
        
    def check_representations(self, q1):
        """If they are the same, report true. If not, kick out an exception. Don't add apples to oranges."""

        if self.representation == q1.representation:
            return True
        
        else:
            raise Exception("Oops, 2 quaternions have different representations: {}, {}".format(self.representation, q1.representation))
            return False
        
    def q4(self):
        """Return a 4 element array."""
        return [self.dt.p - self.dt.n, self.dx.p - self.dx.n, self.dy.p - self.dy.n, self.dz.p - self.dz.n]
            
    def q_0(self, qtype="0"):
        """Return a zero quaternion."""
        
        return Q8(qtype=qtype, representation=self.representation)
      
    def q_1(self, qtype="1"):
        """Return a multiplicative identity quaternion."""
        
        return Q8([1, 0, 0, 0], qtype=qtype, representation=self.representation)
    
    def q_i(self, qtype="i"):
        """Return i."""
        
        return Q8([0, 1, 0, 0], qtype=qtype, representation=self.representation)
    
    def q_j(self, qtype="j"):
        """Return j."""
        
        return Q8([0, 0, 1, 0], qtype=qtype, representation=self.representation)
    
    def q_k(self, qtype="k"):
        """Return k."""
        
        return Q8([0, 0, 0, 1], qtype=qtype, representation=self.representation)
    
    def q_random(self, qtype="?"):
        """Return a random-valued quaternion."""

        return Q8([random.random(), random.random(), random.random(), random.random()], qtype=qtype, representation=self.representation)
    
    def equals(self, q1):
        """Tests if two quaternions are equal."""    
        
        if self.dt.d_equals(q1.dt) and self.dx.d_equals(q1.dx) and \
            self.dy.d_equals(q1.dy) and self.dz.d_equals(q1.dz):
            return True
        
        else:
            return False
    
    def conj(self, conj_type=0, qtype="*"):
        """Three types of conjugates."""
        
        end_qtype = "{st}{qt}".format(st=self.qtype, qt=qtype)
        
        conj_q = Q8()

        if conj_type == 0:
            conj_q.dt = self.dt
            conj_q.dx = self.dx.d_additive_inverse_up_to_an_automorphism()
            conj_q.dy = self.dy.d_additive_inverse_up_to_an_automorphism()
            conj_q.dz = self.dz.d_additive_inverse_up_to_an_automorphism()
        
        if conj_type == 1:
            conj_q.dt = self.dt.d_additive_inverse_up_to_an_automorphism()
            conj_q.dx = self.dx
            conj_q.dy = self.dy.d_additive_inverse_up_to_an_automorphism()
            conj_q.dz = self.dz.d_additive_inverse_up_to_an_automorphism()
            end_qtype += "1"
            
        if conj_type == 2:
            conj_q.dt = self.dt.d_additive_inverse_up_to_an_automorphism()
            conj_q.dx = self.dx.d_additive_inverse_up_to_an_automorphism()
            conj_q.dy = self.dy
            conj_q.dz = self.dz.d_additive_inverse_up_to_an_automorphism()
            end_qtype += "2"
                   
        conj_q.qtype = end_qtype
        conj_q.representation = self.representation
        
        return conj_q
    
    def vahlen_conj(self, conj_type="-", qtype="vc"):
        """Three types of conjugates -'* done by Vahlen in 1901."""
        
        end_qtype = "{st}{qt}".format(st=self.qtype, qt=qtype)
        conj_q = Q8()

        if conj_type == "-":
            conj_q.dt = self.dt
            conj_q.dx = self.dx.d_additive_inverse_up_to_an_automorphism()
            conj_q.dy = self.dy.d_additive_inverse_up_to_an_automorphism()
            conj_q.dz = self.dz.d_additive_inverse_up_to_an_automorphism()
            end_qtype += "-"
            
        if conj_type == "'":
            conj_q.dt = self.dt
            conj_q.dx = self.dx.d_additive_inverse_up_to_an_automorphism()
            conj_q.dy = self.dy.d_additive_inverse_up_to_an_automorphism()
            conj_q.dz = self.dz
            end_qtype += "'"
            
        if conj_type == "*":
            conj_q.dt = self.dt
            conj_q.dx = self.dx
            conj_q.dy = self.dy
            conj_q.dz = self.dz.d_additive_inverse_up_to_an_automorphism()
            end_qtype += "*"

        conj_q.qtype = end_qtype
        conj_q.representation = self.representation
        
        return conj_q

    def flip_signs(self, qtype=""):
        """Flip all the signs, just like multipying by -1."""

        end_qtype = "-{}".format(self.qtype)
        
        dt, dx, dy, dz = self.dt, self.dx, self.dy, self.dz
        flip_q = Q8(qtype=end_qtype)
        
        flip_q.dt.p = dt.n
        flip_q.dt.n = dt.p
        flip_q.dx.p = dx.n
        flip_q.dx.n = dx.p
        flip_q.dy.p = dy.n
        flip_q.dy.n = dy.p
        flip_q.dz.p = dz.n
        flip_q.dz.n = dz.p
        
        flip_q.qtype = end_qtype
        flip_q.representation = self.representation
        
        return flip_q
    
    
    def _commuting_products(self, q1):
        """Returns a dictionary with the commuting products."""

        products = {'tt': self.dt.Z2_product(q1.dt),
                    'xx+yy+zz': self.dx.Z2_product(q1.dx).d_add(self.dy.Z2_product(q1.dy)).d_add(self.dz.Z2_product(q1.dz)),
        
                    'tx+xt': self.dt.Z2_product(q1.dx).d_add(self.dx.Z2_product(q1.dt)),
                    'ty+yt': self.dt.Z2_product(q1.dy).d_add(self.dy.Z2_product(q1.dt)),
                    'tz+zt': self.dt.Z2_product(q1.dz).d_add(self.dz.Z2_product(q1.dt))}
        
        return products
    
    def _anti_commuting_products(self, q1):
        """Returns a dictionary with the three anti-commuting products."""

        products = {'yz-zy': self.dy.Z2_product(q1.dz).d_dif(self.dz.Z2_product(q1.dy)),
                    'zx-xz': self.dz.Z2_product(q1.dx).d_dif(self.dx.Z2_product(q1.dz)),
                    'xy-yx': self.dx.Z2_product(q1.dy).d_dif(self.dy.Z2_product(q1.dx)),
                    'zy-yz': self.dz.Z2_product(q1.dy).d_dif(self.dy.Z2_product(q1.dz)),
                    'xz-zx': self.dx.Z2_product(q1.dz).d_dif(self.dz.Z2_product(q1.dx)),
                    'yx-xy': self.dy.Z2_product(q1.dx).d_dif(self.dx.Z2_product(q1.dy))
                   }
        
        return products
    
    def _all_products(self, q1):
        """Returns a dictionary with all possible products."""

        products = self._commuting_products(q1)
        products.update(self.anti_commuting_products(q1))
        
        return products
    
    def square(self, qtype="^2"):
        """Square a quaternion."""
        
        end_qtype = "{st}{qt}".format(st=self.qtype, qt=qtype)
        
        qxq = self._commuting_products(self)
        
        sq = Q8(qtype=end_qtype, representation=self.representation)
        sq.dt = qxq['tt'].d_dif(qxq['xx+yy+zz'])
        sq.dx = qxq['tx+xt']
        sq.dy = qxq['ty+yt']
        sq.dz = qxq['tz+zt']
        
        return sq
    
    def reduce(self, qtype="reduced"):
        """Put all doublets into the reduced form so one of each pair is zero."""

        end_qtype = "{st}-{qt}".format(st=self.qtype, qt=qtype)
        
        q_red = Q8(qtype=end_qtype)
        q_red.dt = self.dt.d_reduce()
        q_red.dx = self.dx.d_reduce()
        q_red.dy = self.dy.d_reduce()
        q_red.dz = self.dz.d_reduce()
        
        q_red.representation = self.representation
        
        return q_red
    
    def norm_squared(self, qtype="|| ||^2"):
        """The norm_squared of a quaternion."""
        
        end_qtype = "||{st}||^2".format(st=self.qtype)
        
        qxq = self._commuting_products(self)
        n_q = Q8()
        n_q.dt = qxq['tt'].d_add(qxq['xx+yy+zz'])
        result = n_q.reduce()
        result.qtype = end_qtype
        result.representation = self.representation
        
        return result
    
    def norm_squared_of_vector(self, qtype="V(|| ||)^2"):
        """The norm_squared of the vector of a quaternion."""
        
        end_qtype = "||{st}||^2".format(st=self.qtype)
        
        qxq = self._commuting_products(self)
        nv_q = Q8()
        nv_q.dt = qxq['xx+yy+zz']
        result = nv_q.reduce()
        result.qtype = end_qtype
        result.representation = self.representation

        return result
    
    def abs_of_q(self, qtype="| |"):
        """The absolute value, the square root of the norm_squared."""

        end_qtype = "|{st}|".format(st=self.qtype, qt=qtype)
        
        a = self.norm_squared()
        sqrt_t = a.dt.p ** (1/2)
        a.dt.p = sqrt_t
        a.qtype = end_qtype
        a.representation = self.representation
        
        return a

    def abs_of_vector(self, qtype="|V|"):
        """The absolute value of the vector, the square root of the norm_squared of the vector."""

        end_qtype = "|{st}|".format(st=self.qtype, qt=qtype)
        
        av = self.norm_squared_of_vector()
        sqrt_t = av.dt.p ** (1/2)
        av.dt.p = sqrt_t
        
        av.qtype = end_qtype
        av.representation = self.representation
        
        return av
    
    def normalize(self, qtype="U"):
        """Normalize a quaternion"""
        
        end_qtype = "{st}U".format(st=self.qtype)
        
        abs_q_inv = self.abs_of_q().invert()
        n_q = self.product(abs_q_inv)
        
        n_q.qtype = end_qtype
        n_q.representation = self.representation
        
        return n_q
    
    def add(self, q1, qtype=""):
        """Form a add given 2 quaternions."""

        self.check_representations(q1)
        
        end_qtype = "{f}+{s}".format(f=self.qtype, s=q1.qtype)
        
        add_q = Q8(qtype=end_qtype, representation=self.representation)
        add_q.dt = self.dt.d_add(q1.dt)
        add_q.dx = self.dx.d_add(q1.dx)
        add_q.dy = self.dy.d_add(q1.dy)
        add_q.dz = self.dz.d_add(q1.dz)
        
        return add_q
        
    def dif(self, q1, qtype=""):
        """Form a add given 2 quaternions."""

        self.check_representations(q1)
        
        end_qtype = "{f}-{s}".format(f=self.qtype, s=q1.qtype)
            
        dif_q = Q8(qtype=end_qtype, representation=self.representation)
        dif_q.dt = self.dt.d_dif(q1.dt)
        dif_q.dx = self.dx.d_dif(q1.dx)
        dif_q.dy = self.dy.d_dif(q1.dy)
        dif_q.dz = self.dz.d_dif(q1.dz)
              
        return dif_q
    
    def product(self, q1, kind="", reverse=False, qtype=""):
        """Form a product given 2 quaternions: standard, even, odd, and even_minus_odd."""
    
        self.check_representations(q1)
    
        commuting = self._commuting_products(q1)
        q_even = Q8()
        q_even.dt = commuting['tt'].d_dif(commuting['xx+yy+zz'])
        q_even.dx = commuting['tx+xt']
        q_even.dy = commuting['ty+yt']
        q_even.dz = commuting['tz+zt']
        
        anti_commuting = self._anti_commuting_products(q1)
        q_odd = Q8()
        
        if reverse:
            q_odd.dx = anti_commuting['zy-yz']
            q_odd.dy = anti_commuting['xz-zx']
            q_odd.dz = anti_commuting['yx-xy']
            
        else:
            q_odd.dx = anti_commuting['yz-zy']
            q_odd.dy = anti_commuting['zx-xz']
            q_odd.dz = anti_commuting['xy-yx']
        
        result = Q8(representation=self.representation)
        
        if kind == "":
            result = q_even.add(q_odd)
            times_symbol = "x"
        elif kind.lower() == "even":
            result = q_even
            times_symbol = "xE"
        elif kind.lower() == "odd":
            result = q_odd
            times_symbol = "xO"
        elif kind.lower() == "even_minus_odd":
            result = q_even.dif(q_odd)
            times_symbol = "xE-O"
        else:
            raise Exception("Fouf 'kind' values are known: '', 'even', 'odd', and 'even_minus_odd'")
            
        if reverse:
            times_symbol = times_symbol.replace('x', 'xR')
            
        if qtype:
            result.qtype = qtype
        else:
            result.qtype = "{f}{ts}{s}".format(f=self.qtype, ts=times_symbol, s=q1.qtype)
        
        return result
    
    def Euclidean_product(self, q1, kind="", reverse=False, qtype=""):
        """Form a product p* q given 2 quaternions, not associative."""

        self.check_representations(q1)
        
        pq = Q8(representation=self.representation)
        pq = self.conj().product(q1, kind, reverse, qtype)
            
        return pq
    
    def invert(self, qtype="^-1"):
        """Invert a quaternion."""
        
        end_qtype = "{st}{qt}".format(st=self.qtype, qt=qtype)
        
        q_conj = self.conj()
        q_norm_squared = self.norm_squared().reduce()
        
        if q_norm_squared.dt.p == 0:
            return self.q0()
        
        q_norm_squared_inv = Q8([1.0 / q_norm_squared.dt.p, 0, 0, 0, 0, 0, 0, 0])

        q_inv = q_conj.product(q_norm_squared_inv, qtype=self.qtype)
        q_inv.qtype = end_qtype
        q_inv.representation = self.representation
        
        return q_inv

    def divide_by(self, q1, qtype=""):
        """Divide one quaternion by another. The order matters unless one is using a norm_squared (real number)."""

        self.check_representations(q1)
        
        end_qtype = "{f}/{s}".format(f=self.qtype, s=q1.qtype)
            
        q_inv = q1.invert()
        q_div = self.product(q_inv) 
        q_div.qtype = end_qtype
        q_div.representation = self.representation
        
        return q_div
    
    def triple_product(self, q1, q2):
        """Form a triple product given 3 quaternions."""
        
        self.check_representations(q1)
        self.check_representations(q2)
        
        triple = self.product(q1).product(q2)
        triple.representation = self.representation
        
        return triple
    
    # Quaternion rotation involves a triple product:  UQU∗
    # where the U is a unitary quaternion (having a norm_squared of one).
    def rotate(self, a_1p=0, a_1n=0, a_2p=0, a_2n=0, a_3p=0, a_3n=0):
        """Do a rotation given up to three angles."""
    
        u = Q8([0, 0, a_1p, a_1n, a_2p, a_2n, a_3p, a_3n])
        u_abs = u.abs_of_q()
        u_norm_squaredalized = u.divide_by(u_abs)
        q_rot = u_norm_squaredalized.triple_product(self, u_norm_squaredalized.conj())
        q_rot.representation = self.representation
        
        return q_rot
    
    # A boost also uses triple products like a rotation, but more of them.
    # This is not a well-known result, but does work.
    def boost(self, beta_x=0, beta_y=0, beta_z=0, qtype="Boost!"):
        """A boost along the x, y, and/or z axis."""
    
        end_qtype = "{st}{qt}".format(st=self.qtype, qt=qtype)
        
        boost = Q8(sr_gamma_betas(beta_x, beta_y, beta_z))
        b_conj = boost.conj()
        
        triple_1 = boost.triple_product(self, b_conj)
        triple_2 = boost.triple_product(boost, self).conj()
        triple_3 = b_conj.triple_product(b_conj, self).conj()
              
        triple_23 = triple_2.dif(triple_3)
        half_23 = triple_23.product(Q8([0.5, 0, 0, 0, 0, 0, 0, 0]))
        triple_123 = triple_1.add(half_23, qtype=self.qtype)
        triple_123.qtype = end_qtype
        triple_123.representation = self.representation
        
        return triple_123
    
    # g_shift is a function based on the space-times-time invariance proposal for gravity,
    # which proposes that if one changes the distance from a gravitational source, then
    # squares a measurement, the observers at two different hieghts agree to their
    # space-times-time values, but not the intervals.
    def g_shift(self, dimensionless_g, g_form="exp", qtype="G_shift"):
        """Shift an observation based on a dimensionless GM/c^2 dR."""
        
        end_qtype = "{st}{qt}".format(st=self.qtype, qt=qtype)
        
        if g_form == "exp":
            g_factor = sp.exp(dimensionless_g)
            if qtype == "g_shift":
                qtype = "g_exp"
        elif g_form == "minimal":
            g_factor = 1 + 2 * dimensionless_g + 2 * dimensionless_g ** 2
            if qtype == "g_shift":
                qtype = "g_minimal"
        else:
            print("g_form not defined, should be 'exp' or 'minimal': {}".format(g_form))
            return self
        exp_g = sp.exp(dimensionless_g)
        
        g_q = Q8(qtype=end_qtype, representation=self.representation)
        g_q.dt = Doublet([self.dt.p / exp_g, self.dt.n / exp_g])
        g_q.dx = Doublet([self.dx.p * exp_g, self.dx.n * exp_g])
        g_q.dy = Doublet([self.dy.p * exp_g, self.dy.n * exp_g])
        g_q.dz = Doublet([self.dz.p * exp_g, self.dz.n * exp_g])
        
        return g_q
    
    def sin(self, qtype="sin"):
        """Take the sine of a quaternion, (sin(t) cosh(|R|), cos(t) sinh(|R|) R/|R|)"""

        end_qtype = "sin({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()
        red_t = self.dt.d_reduce()

        if red_t.p == 0 and red_t.n != 0:
            if abs_v.dt.p == 0:    
                return Q8([-1 * math.sin(red_t.n), 0, 0, 0], qtype=end_qtype)
            
            sint = math.sin(-1 * red_t.n)
            cost = math.cos(-1 * red_t.n)    
            
        else:
            if abs_v.dt.p == 0:    
                return Q8([math.sin(red_t.p), 0, 0, 0], qtype=end_qtype)

            sint = math.sin(red_t.p)
            cost = math.cos(red_t.p)    
            
        sinhR = math.sinh(abs_v.dt.p)
        coshR = math.cosh(abs_v.dt.p)
        
        k = cost * sinhR / abs_v.dt.p

        q_out = Q8(qtype=end_qtype, representation=self.representation)
        q_out.dt = Doublet(sint * coshR)
        q_out.dx = Doublet(k * (self.dx.p - self.dx.n))
        q_out.dy = Doublet(k * (self.dy.p - self.dy.n))
        q_out.dz = Doublet(k * (self.dz.p - self.dz.n))

        return q_out
     
    def cos(self, qtype="cos"):
        """Take the cosine of a quaternion, (cos(t) cosh(|R|), sin(t) sinh(|R|) R/|R|)"""
        
        end_qtype = "cos({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()
        red_t = self.dt.d_reduce()
       
        if red_t.p == 0 and red_t.n != 0:
            if abs_v.dt.p == 0:    
                return Q8([math.cos(-1 * red_t.n), 0, 0, 0], qtype=end_qtype)
        
            sint = math.sin(-1 * red_t.n)
            cost = math.cos(-1 * red_t.n) 
            
        else:
            if abs_v.dt.p == 0:    
                return Q8([math.cos(red_t.p), 0, 0, 0], qtype=end_qtype)
        
            sint = math.sin(red_t.p)
            cost = math.cos(red_t.p)
            
        sinhR = math.sinh(abs_v.dt.p)
        coshR = math.cosh(abs_v.dt.p)
        
        k = -1 * sint * sinhR / abs_v.dt.p
        
        q_out = Q8(qtype=end_qtype, representation=self.representation)
        q_out.dt = Doublet(cost * coshR)
        q_out.dx = Doublet(k * (self.dx.p - self.dx.n))
        q_out.dy = Doublet(k * (self.dy.p - self.dy.n))
        q_out.dz = Doublet(k * (self.dz.p - self.dz.n))

        return q_out
    
    def tan(self, qtype="sin"):
        """Take the tan of a quaternion, sin/cos"""

        end_qtype = "tan({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()        
        red_t = self.dt.d_reduce()
        
        if red_t.p == 0 and red_t.n != 0:
            if abs_v.dt == 0:    
                return Q8([math.tan(-1 * red_t.n), 0, 0, 0], qtype=end_qtype)
        else:
            if abs_v.dt.p == 0:    
                return Q8([math.tan(red_t.p), 0, 0, 0], qtype=end_qtype)
            
        sinq = self.sin()
        cosq = self.cos()
            
        q_out = sinq.divide_by(cosq) 
        q_out.qtype = end_qtype
        q_out.representation = self.representation
        
        return q_out
    
    def sinh(self, qtype="sin"):
        """Take the sinh of a quaternion, (sinh(t) cos(|R|), cosh(t) sin(|R|) R/|R|)"""

        end_qtype = "sinh({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()
        red_t = self.dt.d_reduce()
        
        if red_t.p == 0 and red_t.n != 0: 
            if abs_v.dt.p == 0:    
                return Q8([math.sinh(-1 * red_t.n), 0, 0, 0], qtype=end_qtype)
            
            sinht = math.sinh(-1 * red_t.n)
            cosht = math.cosh(-1 * red_t.n)
            
        else: 
            if abs_v.dt.p == 0:    
                return Q8([math.sinh(red_t.p), 0, 0, 0], qtype=end_qtype)
            
            sinht = math.sinh(red_t.p)
            cosht = math.cosh(red_t.p)
            
        sinR = math.sin(abs_v.dt.p)
        cosR = math.cos(abs_v.dt.p)
        
        k = cosht * sinR / abs_v.dt.p
        
        q_out = Q8(qtype=end_qtype, representation=self.representation)
        q_out.dt = Doublet(sinht * cosR)
        q_out.dx = Doublet(k * (self.dx.p - self.dx.n))
        q_out.dy = Doublet(k * (self.dy.p - self.dy.n))
        q_out.dz = Doublet(k * (self.dz.p - self.dz.n))

        return q_out
    
    def cosh(self, qtype="cosh"):
        """Take the cosh of a quaternion, (cosh(t) cos(|R|), sinh(t) sin(|R|) R/|R|)"""

        end_qtype = "cosh({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()        
        red_t = self.dt.d_reduce()
        
        if red_t.p == 0 and red_t.n != 0:
            if abs_v.dt.p == 0:    
                return Q8([math.cosh(-1 * red_t.n), 0, 0, 0], qtype=end_qtype)
            
            sinht = math.sinh(-1 * red_t.n)
            cosht = math.cosh(-1 * red_t.n)
        
        else:
            if abs_v.dt.p == 0:    
                return Q8([math.cosh(red_t.p), 0, 0, 0], qtype=end_qtype)
            
            sinht = math.sinh(red_t.p)
            cosht = math.cosh(red_t.p)
             
        sinR = math.sin(abs_v.dt.p)
        cosR = math.cos(abs_v.dt.p)
        
        k = sinht * sinR / abs_v.dt.p
            
        q_out = Q8(qtype=end_qtype, representation=self.representation)
        q_out.dt = Doublet(cosht * cosR)
        q_out.dx = Doublet(k * (self.dx.p - self.dx.n))
        q_out.dy = Doublet(k * (self.dy.p - self.dy.n))
        q_out.dz = Doublet(k * (self.dz.p - self.dz.n))
        q_out.qtype = end_qtype
        
        return q_out
    
    def tanh(self, qtype="sin"):
        """Take the tanh of a quaternion, sin/cos"""

        end_qtype = "tanh({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()        
        red_t = self.dt.d_reduce()
        
        if abs_v.dt.p == 0:
            if red_t.p == 0 and red_t.n != 0:
                return Q8([-1 * math.tanh(self.dt.n), 0, 0, 0], qtype=end_qtype)
            
            else:
                return Q8([math.tanh(self.dt.p), 0, 0, 0], qtype=end_qtype)
            
        sinhq = self.sinh()
        coshq = self.cosh()
            
        q_out = sinhq.divide_by(coshq) 
        q_out.qtype = end_qtype
        q_out.representation = self.representation
        
        return q_out
    
    def exp(self, qtype="exp"):
        """Take the exponential of a quaternion."""
        # exp(q) = (exp(t) cos(|R|, exp(t) sin(|R|) R/|R|)
        
        end_qtype = "exp({st})".format(st=self.qtype)
        
        abs_v = self.abs_of_vector()
        red_t = self.dt.d_reduce()
        
        if red_t.p == 0 and red_t.n != 0:
            et = math.exp(-1 * red_t.n)
            
            if (abs_v.dt.p == 0):
                return Q8([et, 0, 0, 0], qtype=end_qtype)
            
            cosR = math.cos(abs_v.dt.p)
            sinR = math.sin(abs_v.dt.p)
    
        else:
            et = math.exp(red_t.p)
            
            if (abs_v.dt.p == 0):
                return Q8([et, 0, 0, 0], qtype=end_qtype)
            
            cosR = math.cos(abs_v.dt.p)
            sinR = math.sin(abs_v.dt.p)
            
        k = et * sinR / abs_v.dt.p
             
        expq = Q8(qtype=end_qtype, representation=self.representation)
        expq.dt = Doublet(et * cosR)
        expq.dx = Doublet(k * (self.dx.p - self.dx.n))
        expq.dy = Doublet(k * (self.dy.p - self.dy.n))
        expq.dz = Doublet(k * (self.dz.p - self.dz.n))
                       
        return expq
    
    def ln(self, qtype="ln"):
        """Take the natural log of a quaternion."""
        # ln(q) = (0.5 ln t^2 + R.R, atan2(|R|, t) R/|R|)
        
        end_qtype = "ln({st})".format(st=self.qtype)
        
        abs_v = self.abs_of_vector()
        red_t = self.dt.d_reduce()
        
        if red_t.p == 0 and red_t.n != 0:
            if (abs_v.dt.p == 0):
                # I don't understant this, but mathematica does the same thing, but it looks wrong to me.
                return(Q8([math.log(-self.dt.n), math.pi, 0, 0], qtype=end_qtype))   
            
            t_value = 0.5 * math.log(red_t.n * red_t.n + abs_v.dt.p * abs_v.dt.p)
            k = math.atan2(abs_v.dt.p, red_t.n) / abs_v.dt.p
        
        else:
            if (abs_v.dt.p == 0):
                return(Q8([math.log(self.dt.p), 0, 0, 0], qtype=end_qtype))
                
            t_value = 0.5 * math.log(red_t.p * red_t.p + abs_v.dt.p * abs_v.dt.p)
            k = math.atan2(abs_v.dt.p, red_t.p) / abs_v.dt.p
            
        lnq = Q8(qtype=end_qtype, representation=self.representation)
        lnq.dt = Doublet(t_value)
        lnq.dx = Doublet(k * (self.dx.p - self.dx.n))
        lnq.dy = Doublet(k * (self.dy.p - self.dy.n))
        lnq.dz = Doublet(k * (self.dz.p - self.dz.n))
                       
        return lnq
    
    def q_2_q(self, q1, qtype="P"):
        """Take the natural log of a quaternion."""
        # q^p = exp(ln(q) * p)
        
        self.check_representations(q1)
        
        end_qtype = "{st}^P".format(st=self.qtype)
        
        q2q = self.ln().product(q1).reduce().exp()           
        q2q.qtype = end_qtype
        q2q.representation = self.representation
        
        return q2q
    
    def trunc(self):
        """Truncates values."""
        
        self.dt = math.trunc(self.dt)
        self.dx = math.trunc(self.dx)
        self.dy = math.trunc(self.dy)
        self.dz = math.trunc(self.dz)
        
        return self

In [15]:
class TestQ8(unittest.TestCase):
    """Class to make sure all the functions work as expected."""
    
    Q = Q8([1, 0, 0, 2, 0, 3, 0, 4])
    P = Q8([0, 0, 4, 0, 0, 3, 0, 0])
    R = Q8([3, 0, 0, 0, 0, 0, 0, 0])
    C = Q8([2, 0, 4, 0, 0, 0, 0, 0])
    q_big = Q8([1, 2, 3, 4, 5, 6, 7, 8])
    
    def test_qt(self):
        self.assertTrue(self.Q.dt.p == 1)
    
    def test_q_0(self):
        q_z = self.Q.q_0()
        print("q_0: {}", q_z)
        self.assertTrue(q_z.dt.p == 0)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dy.n == 0)
        self.assertTrue(q_z.dz.p == 0)
        
    def test_q_1(self):
        q_z = self.Q.q_1()
        print("q_1: {}", q_z)
        self.assertTrue(q_z.dt.p == 1)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dy.p == 0)
        self.assertTrue(q_z.dz.p == 0)
        
    def test_q_i(self):
        q_z = self.Q.q_i()
        print("q_i: {}", q_z)
        self.assertTrue(q_z.dt.p == 0)
        self.assertTrue(q_z.dx.p == 1)
        self.assertTrue(q_z.dy.p == 0)
        self.assertTrue(q_z.dz.p == 0)
        
    def test_q_j(self):
        q_z = self.Q.q_j()
        print("q_j: {}", q_z)
        self.assertTrue(q_z.dt.p == 0)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dy.p == 1)
        self.assertTrue(q_z.dz.p == 0)
                
    def test_q_k(self):
        q_z = self.Q.q_k()
        print("q_k: {}", q_z)
        self.assertTrue(q_z.dt.p == 0)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dy.p == 0)
        self.assertTrue(q_z.dz.p == 1)
        
    def test_q_random(self):
        q_z = self.Q.q_random()
        print("q_random():", q_z)
        self.assertTrue(q_z.dt.p >= 0 and q_z.dt.p <= 1)
        self.assertTrue(q_z.dx.p >= 0 and q_z.dx.p <= 1)
        self.assertTrue(q_z.dy.p >= 0 and q_z.dy.p <= 1)
        self.assertTrue(q_z.dz.p >= 0 and q_z.dz.p <= 1)
        
    def test_equals(self):
        self.assertTrue(self.Q.equals(self.Q))
        self.assertFalse(self.Q.equals(self.P))
        
    def test_conj_0(self):
        q_z = self.Q.conj()
        print("conj 0: {}", q_z)
        self.assertTrue(q_z.dt.p == 1)
        self.assertTrue(q_z.dx.p == 2)
        self.assertTrue(q_z.dy.p == 3)
        self.assertTrue(q_z.dz.p == 4)
                 
    def test_conj_1(self):
        q_z = self.Q.conj(1)
        print("conj 1: {}", q_z)
        self.assertTrue(q_z.dt.n == 1)
        self.assertTrue(q_z.dx.n == 2)
        self.assertTrue(q_z.dy.p == 3)
        self.assertTrue(q_z.dz.p == 4)
                 
    def test_conj_2(self):
        q_z = self.Q.conj(2)
        print("conj 2: {}", q_z)
        self.assertTrue(q_z.dt.n == 1)
        self.assertTrue(q_z.dx.p == 2)
        self.assertTrue(q_z.dy.n == 3)
        self.assertTrue(q_z.dz.p == 4)
        
    def test_vahlen_conj_0(self):
        q_z = self.Q.vahlen_conj()
        print("vahlen conj -: {}", q_z)
        self.assertTrue(q_z.dt.p == 1)
        self.assertTrue(q_z.dx.p == 2)
        self.assertTrue(q_z.dy.p == 3)
        self.assertTrue(q_z.dz.p == 4)
                 
    def test_vahlen_conj_1(self):
        q_z = self.Q.vahlen_conj("'")
        print("vahlen conj ': {}", q_z)
        self.assertTrue(q_z.dt.p == 1)
        self.assertTrue(q_z.dx.p == 2)
        self.assertTrue(q_z.dy.p == 3)
        self.assertTrue(q_z.dz.n == 4)
                 
    def test_vahlen_conj_2(self):
        q_z = self.Q.vahlen_conj('*')
        print("vahlen conj *: {}", q_z)
        self.assertTrue(q_z.dt.p == 1)
        self.assertTrue(q_z.dx.n == 2)
        self.assertTrue(q_z.dy.n == 3)
        self.assertTrue(q_z.dz.p == 4)
        
    def test_square(self):
        q_sq = self.Q.square()
        q_sq_red = q_sq.reduce()
        print("square: {}".format(q_sq))
        print("square reduced: {}".format(q_sq_red))
        self.assertTrue(q_sq.dt.p == 1)
        self.assertTrue(q_sq.dt.n == 29)
        self.assertTrue(q_sq.dx.n == 4)
        self.assertTrue(q_sq.dy.n == 6)
        self.assertTrue(q_sq.dz.n == 8)
        self.assertTrue(q_sq_red.dt.p == 0)
        self.assertTrue(q_sq_red.dt.n == 28)
                
    def test_reduce(self):
        q_red = self.q_big.reduce()
        print("q_big reduced: {}".format(q_red))
        self.assertTrue(q_red.dt.p == 0)
        self.assertTrue(q_red.dt.n == 1)
        self.assertTrue(q_red.dx.p == 0)
        self.assertTrue(q_red.dx.n == 1)
        self.assertTrue(q_red.dy.p == 0)
        self.assertTrue(q_red.dy.n == 1)
        self.assertTrue(q_red.dz.p == 0)
        self.assertTrue(q_red.dz.n == 1)
        
    def test_norm_squared(self):
        q_z = self.Q.norm_squared()
        print("norm_squared: {}", q_z)
        self.assertTrue(q_z.dt.p == 30)
        self.assertTrue(q_z.dt.n == 0)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dx.n == 0)
        self.assertTrue(q_z.dy.p == 0)
        self.assertTrue(q_z.dy.n == 0)
        self.assertTrue(q_z.dz.p == 0)
        self.assertTrue(q_z.dz.n == 0)
        
    def test_norm_squared_of_vector(self):
        q_z = self.Q.norm_squared_of_vector()
        print("norm_squared_of_vector: {}", q_z)
        self.assertTrue(q_z.dt.p == 29)
        self.assertTrue(q_z.dt.n == 0)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dx.n == 0)
        self.assertTrue(q_z.dy.p == 0)
        self.assertTrue(q_z.dy.n == 0)
        self.assertTrue(q_z.dz.p == 0)
        self.assertTrue(q_z.dz.n == 0)
        
    def test_abs_of_q(self):
        q_z = self.P.abs_of_q()
        print("abs_of_q: {}", q_z)
        self.assertTrue(q_z.dt.p == 5)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dy.p == 0)
        self.assertTrue(q_z.dz.p == 0)
        self.assertTrue(q_z.dt.n == 0)
        self.assertTrue(q_z.dx.n == 0)
        self.assertTrue(q_z.dy.n == 0)
        self.assertTrue(q_z.dz.n == 0)
        
    def test_abs_of_vector(self):
        q_z = self.P.abs_of_vector()
        print("abs_of_vector: {}", q_z)
        self.assertTrue(q_z.dt.p == 5)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dy.p == 0)
        self.assertTrue(q_z.dz.p == 0)
        self.assertTrue(q_z.dt.n == 0)
        self.assertTrue(q_z.dx.n == 0)
        self.assertTrue(q_z.dy.n == 0)
        self.assertTrue(q_z.dz.n == 0)
        
    def test_normalize(self):
        q_z = self.P.normalize()
        print("q_normalized: {}", q_z)
        self.assertTrue(q_z.dt.p == 0)
        self.assertTrue(q_z.dt.n == 0)
        self.assertTrue(q_z.dx.p == 0.8)
        self.assertTrue(q_z.dx.n == 0)
        self.assertTrue(q_z.dy.p == 0)
        self.assertAlmostEqual(q_z.dy.n, 0.6)
        self.assertTrue(q_z.dz.p == 0)    
        self.assertTrue(q_z.dz.n == 0)    
        
    def test_add(self):
        q_z = self.Q.add(self.P)
        print("add: {}", q_z)
        self.assertTrue(q_z.dt.p == 1)
        self.assertTrue(q_z.dt.n == 0)
        self.assertTrue(q_z.dx.p == 4)
        self.assertTrue(q_z.dx.n == 2)
        self.assertTrue(q_z.dy.p == 0)
        self.assertTrue(q_z.dy.n == 6)
        self.assertTrue(q_z.dz.p == 0)
        self.assertTrue(q_z.dz.n == 4)
        
    def test_add_reduce(self):
        q_z_red = self.Q.add(self.P).reduce()
        print("add reduce: {}".format(q_z_red))
        self.assertTrue(q_z_red.dt.p == 1)
        self.assertTrue(q_z_red.dt.n == 0)
        self.assertTrue(q_z_red.dx.p == 2)
        self.assertTrue(q_z_red.dx.n == 0)
        self.assertTrue(q_z_red.dy.p == 0)
        self.assertTrue(q_z_red.dy.n == 6)
        self.assertTrue(q_z_red.dz.p == 0)
        self.assertTrue(q_z_red.dz.n == 4)
        
    def test_dif(self):
        q_z = self.Q.dif(self.P)
        print("dif: {}", q_z)
        self.assertTrue(q_z.dt.p == 1)
        self.assertTrue(q_z.dt.n == 0)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dx.n == 6) 
        self.assertTrue(q_z.dy.p == 3)
        self.assertTrue(q_z.dy.n == 3)
        self.assertTrue(q_z.dz.p == 0)
        self.assertTrue(q_z.dz.n == 4) 

    def test_product(self):
        q_z = self.Q.product(self.P).reduce()
        print("product: {}", q_z)
        self.assertTrue(q_z.dt.p == 0)
        self.assertTrue(q_z.dt.n == 1)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dx.n == 8)
        self.assertTrue(q_z.dy.p == 0)
        self.assertTrue(q_z.dy.n == 19)
        self.assertTrue(q_z.dz.p == 18)
        self.assertTrue(q_z.dz.n == 0)
        
    def test_product_even(self):
        q_z = self.Q.product(self.P, kind="even").reduce()
        print("product, kind even: {}", q_z)
        self.assertTrue(q_z.dt.p == 0)
        self.assertTrue(q_z.dt.n == 1)
        self.assertTrue(q_z.dx.p == 4)
        self.assertTrue(q_z.dx.n == 0)
        self.assertTrue(q_z.dy.p == 0)
        self.assertTrue(q_z.dy.n == 3)
        self.assertTrue(q_z.dz.p == 0)
        self.assertTrue(q_z.dz.n == 0)
        
    def test_product_odd(self):
        q_z = self.Q.product(self.P, kind="odd").reduce()
        print("product, kind odd: {}", q_z)
        self.assertTrue(q_z.dt.p == 0)
        self.assertTrue(q_z.dt.n == 0)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dx.n == 12)
        self.assertTrue(q_z.dy.p == 0)
        self.assertTrue(q_z.dy.n == 16)
        self.assertTrue(q_z.dz.p == 18)
        self.assertTrue(q_z.dz.n == 0)
    
    def test_product_even_minus_odd(self):
        q_z = self.Q.product(self.P, kind="even_minus_odd").reduce()
        print("product, kind odd: {}", q_z)
        self.assertTrue(q_z.dt.p == 0)
        self.assertTrue(q_z.dt.n == 1)
        self.assertTrue(q_z.dx.p == 16)
        self.assertTrue(q_z.dx.n == 0)
        self.assertTrue(q_z.dy.p == 13)
        self.assertTrue(q_z.dy.n == 0)
        self.assertTrue(q_z.dz.p == 0)
        self.assertTrue(q_z.dz.n == 18)
        
    def test_product_reverse(self):
        QP_rev = self.Q.product(self.P, reverse=True)
        PQ = self.P.product(self.Q)
        self.assertTrue(QP_rev.equals(PQ))
        
    def test_Euclidean_product(self):
        q_z = self.Q.Euclidean_product(self.P).reduce()
        print("Euclidean product: {}", q_z)
        self.assertTrue(q_z.dt.p == 1)
        self.assertTrue(q_z.dt.n == 0)
        self.assertTrue(q_z.dx.p == 16)
        self.assertTrue(q_z.dx.n == 0)
        self.assertTrue(q_z.dy.p == 13)
        self.assertTrue(q_z.dy.n == 0)
        self.assertTrue(q_z.dz.p == 0)
        self.assertTrue(q_z.dz.n == 18)
        
    def test_invert(self):
        q_z = self.P.invert().reduce()
        print("inverse: {}", q_z)
        self.assertTrue(q_z.dt.p == 0)
        self.assertTrue(q_z.dt.n == 0)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dx.n == 0.16)
        self.assertTrue(q_z.dy.p == 0.12)
        self.assertTrue(q_z.dy.n == 0)
        self.assertTrue(q_z.dz.p == 0)
        self.assertTrue(q_z.dz.n == 0)

    def test_divide_by(self):
        q_z = self.Q.divide_by(self.Q).reduce()
        print("inverse: {}", q_z)
        self.assertTrue(q_z.dt.p == 1)
        self.assertTrue(q_z.dt.n == 0)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dx.n == 0)
        self.assertTrue(q_z.dy.p == 0)
        self.assertTrue(q_z.dy.n == 0)
        self.assertTrue(q_z.dz.p == 0)
        self.assertTrue(q_z.dz.n == 0) 
        
    def test_triple_product(self):
        q_z = self.Q.triple_product(self.P, self.Q).reduce()
        print("triple: {}", q_z)
        self.assertTrue(q_z.dt.p == 0)
        self.assertTrue(q_z.dt.n == 2)
        self.assertTrue(q_z.dx.p == 124)
        self.assertTrue(q_z.dx.n == 0)
        self.assertTrue(q_z.dy.p == 0)
        self.assertTrue(q_z.dy.n == 84)
        self.assertTrue(q_z.dz.p == 8)
        self.assertTrue(q_z.dz.n == 0)
        
    def test_rotate(self):
        q_z = self.Q.rotate(1).reduce()
        print("rotate: {}", q_z)
        self.assertTrue(q_z.dt.p == 1)
        self.assertTrue(q_z.dt.n == 0)
        self.assertTrue(q_z.dx.p == 0)
        self.assertTrue(q_z.dx.n == 2)
        self.assertTrue(q_z.dy.p == 3)
        self.assertTrue(q_z.dy.n == 0)
        self.assertTrue(q_z.dz.p == 4)
        self.assertTrue(q_z.dz.n == 0)
        
    def test_boost(self):
        Q_sq = self.Q.square().reduce()
        q_z = self.Q.boost(0.003)
        q_z2 = q_z.square().reduce()
        print("Q_sq: {}".format(Q_sq))
        print("boosted: {}", q_z)
        print("b squared: {}".format(q_z2))
        self.assertTrue(round(q_z2.dt.n, 12) == round(Q_sq.dt.n, 12))
        
    def test_g_shift(self):
        Q_sq = self.Q.square().reduce()
        q_z = self.Q.g_shift(0.003)
        q_z2 = q_z.square().reduce()
        print("Q_sq: {}".format(Q_sq))
        print("g_shift: {}", q_z)
        print("g squared: {}".format(q_z2))
        self.assertTrue(q_z2.dt.n != Q_sq.dt.n)
        self.assertTrue(q_z2.dx.p == Q_sq.dx.p)
        self.assertTrue(q_z2.dx.n == Q_sq.dx.n)
        self.assertTrue(q_z2.dy.p == Q_sq.dy.p)
        self.assertTrue(q_z2.dy.n == Q_sq.dy.n)
        self.assertTrue(q_z2.dz.p == Q_sq.dz.p)
        self.assertTrue(q_z2.dz.n == Q_sq.dz.n)
        
    def test_sin(self):
        self.assertTrue(Q8([0, 0, 0, 0]).sin().reduce().equals(Q8().q_0()))
        self.assertTrue(self.Q.sin().reduce().equals(Q8([91.7837157840346691, -21.8864868530291758, -32.8297302795437673, -43.7729737060583517])))
        self.assertTrue(self.P.sin().reduce().equals(Q8([0,  59.3625684622310033, -44.5219263466732542, 0])))
        self.assertTrue(self.R.sin().reduce().equals(Q8([0.1411200080598672, 0, 0, 0])))
        self.assertTrue(self.C.sin().reduce().equals(Q8([24.8313058489463785, -11.3566127112181743, 0, 0])))
     
    def test_cos(self):
        self.assertTrue(Q8([0, 0, 0, 0]).cos().equals(Q8().q_1()))
        self.assertTrue(self.Q.cos().equals(Q8([58.9336461679439481, 34.0861836904655959, 51.1292755356983974, 68.1723673809311919])))
        self.assertTrue(self.P.cos().equals(Q8([74.2099485247878476, 0, 0, 0])))
        self.assertTrue(self.R.cos().equals(Q8([-0.9899924966004454, 0, 0, 0])))
        self.assertTrue(self.C.cos().equals(Q8([-11.3642347064010600, -24.8146514856341867, 0, 0])))
                            
    def test_tan(self):
        self.assertTrue(Q8([0, 0, 0, 0]).tan().equals(Q8().q_0()))
        self.assertTrue(self.Q.tan().equals(Q8([0.0000382163172501, -0.3713971716439372, -0.5570957574659058, -0.7427943432878743])))
        self.assertTrue(self.P.tan().equals(Q8([0, 0.7999273634100760, -0.5999455225575570, 0])))
        self.assertTrue(self.R.tan().equals(Q8([-0.1425465430742778, 0, 0, 0])))
        self.assertTrue(self.C.tan().equals(Q8([-0.0005079806234700, 1.0004385132020521, 0, 0])))
        
    def test_sinh(self):
        self.assertTrue(Q8([0, 0, 0, 0]).sinh().equals(Q8().q_0()))
        self.assertTrue(self.Q.sinh().equals(Q8([0.7323376060463428, 0.4482074499805421, 0.6723111749708131, 0.8964148999610841])))
        self.assertTrue(self.P.sinh().equals(Q8([0, -0.7671394197305108, 0.5753545647978831, 0])))
        self.assertTrue(self.R.sinh().equals(Q8([10.0178749274099026, 0, 0, 0])))
        self.assertTrue(self.C.sinh().equals(Q8([-2.3706741693520015, -2.8472390868488278, 0, 0])))    
        
    def test_cosh(self):
        self.assertTrue(Q8([0, 0, 0, 0]).cosh().equals(Q8().q_1()))
        self.assertTrue(self.Q.cosh().equals(Q8([0.9615851176369565, 0.3413521745610167, 0.5120282618415251, 0.6827043491220334])))
        self.assertTrue(self.P.cosh().equals(Q8([0.2836621854632263, 0, 0, 0])))
        self.assertTrue(self.R.cosh().equals(Q8([10.0676619957777653, 0, 0, 0])))
        self.assertTrue(self.C.cosh().equals(Q8([-2.4591352139173837, -2.7448170067921538, 0, 0])))    
        
    def test_tanh(self):
        self.assertTrue(Q8([0, 0, 0, 0]).tanh().equals(Q8().q_0()))
        self.assertTrue(self.Q.tanh().equals(Q8([1.0248695360556623, 0.1022956817887642, 0.1534435226831462, 0.2045913635775283])))
        self.assertTrue(self.P.tanh().equals(Q8([0, -2.7044120049972684, 2.0283090037479505, 0])))
        self.assertTrue(self.R.tanh().equals(Q8([0.9950547536867305, 0, 0, 0])))
        self.assertTrue(self.C.tanh().equals(Q8([1.0046823121902353, 0.0364233692474038, 0, 0])))    
        
    def test_exp(self):
        self.assertTrue(Q8([0, 0, 0, 0]).exp().equals(Q8().q_1()))
        self.assertTrue(self.Q.exp().equals(Q8([1.6939227236832994, 0.7895596245415588, 1.1843394368123383, 1.5791192490831176])))
        self.assertTrue(self.P.exp().equals(Q8([0.2836621854632263, -0.7671394197305108, 0.5753545647978831, 0])))
        self.assertTrue(self.R.exp().equals(Q8([20.0855369231876679, 0, 0, 0])))
        self.assertTrue(self.C.exp().equals(Q8([-4.8298093832693851, -5.5920560936409816, 0, 0])))    
    
    def test_ln(self):
        self.assertTrue(self.Q.ln().exp().equals(self.Q))
        self.assertTrue(self.Q.ln().equals(Q8([1.7005986908310777, -0.5151902926640850, -0.7727854389961275, -1.0303805853281700])))
        self.assertTrue(self.P.ln().equals(Q8([1.6094379124341003, 1.2566370614359172, -0.9424777960769379, 0])))
        self.assertTrue(self.R.ln().equals(Q8([1.0986122886681098, 0, 0, 0])))
        self.assertTrue(self.C.ln().equals(Q8([1.4978661367769954, 1.1071487177940904, 0, 0])))    
        
    def test_q_2_q(self):
        self.assertTrue(self.Q.q_2_q(self.P).equals(Q8([-0.0197219653530713, -0.2613955437374326, 0.6496281248064009, -0.3265786562423951])))

suite = unittest.TestLoader().loadTestsFromModule(TestQ8())
unittest.TextTestRunner().run(suite);


.............................................
Euclidean product: {} ((1, 0), (16, 0), (13, 0), (0, 18)) Q*xQ-reduced
abs_of_q: {} ((5.0, 0), (0, 0), (0, 0), (0, 0)) |Q|
abs_of_vector: {} ((5.0, 0), (0, 0), (0, 0), (0, 0)) |Q|
add: {} ((1, 0), (4, 2), (0, 6), (0, 4)) Q+Q
add reduce: ((1, 0), (2, 0), (0, 6), (0, 4)) Q+Q-reduced
Q_sq: ((0, 28), (0, 4), (0, 6), (0, 8)) Q^2-reduced
boosted: {} ((1.51802716224446, 0.5060090540814867), (1.0030180271622444, 3.0090540814867333), (1.5240542164879485, 4.524054216487949), (2.0180721626494638, 6.018072162649464)) QBoost!
b squared: ((0, 28.000000000000007), (0, 4.06028962520837), (0, 6.072108648977842), (0, 8.096144865303788)) QBoost!^2-reduced
conj 0: {} ((1, 0), (2, 0), (3, 0), (4, 0)) Q*
conj 1: {} ((0, 1), (0, 2), (3, 0), (4, 0)) Q*1
conj 2: {} ((0, 1), (2, 0), (0, 3), (4, 0)) Q*2
dif: {} ((1, 0), (0, 6), (3, 3), (0, 4)) Q-Q
inverse: {} ((1.0, 0.0), (0, 0), (0, 0), (0, 0)) Q/Q-reduced
Q_sq: ((0, 28), (0, 4), (0, 6), (0, 8)) Q^2-reduced
g_shift: {} ((0.997004495503373, 0), (0, 2.00600900900675), (0, 3.00901351351013), (0, 4.01201801801351)) QG_shift
g squared: ((0, 28.1805050815139), (0, 4.00000000000000), (0, 6.00000000000000), (0, 8.00000000000000)) QG_shift^2-reduced
inverse: {} ((0.0, 0.0), (0.0, 0.16), (0.12, 0.0), (0.0, 0.0)) Q^-1-reduced
norm_squared: {} ((30, 0), (0, 0), (0, 0), (0, 0)) ||Q||^2
norm_squared_of_vector: {} ((29, 0), (0, 0), (0, 0), (0, 0)) ||Q||^2
q_normalized: {} ((0.0, 0.0), (0.8, 0.0), (0.0, 0.6000000000000001), (0.0, 0.0)) QU
product: {} ((0, 1), (0, 8), (0, 19), (18, 0)) QxQ-reduced
product, kind even: {} ((0, 1), (4, 0), (0, 3), (0, 0)) QxEQ-reduced
product, kind odd: {} ((0, 1), (16, 0), (13, 0), (0, 18)) QxE-OQ-reduced
product, kind odd: {} ((0, 0), (0, 12), (0, 16), (18, 0)) QxOQ-reduced
q_0: {} ((0, 0), (0, 0), (0, 0), (0, 0)) 0
q_1: {} ((1, 0), (0, 0), (0, 0), (0, 0)) 1
q_i: {} ((0, 0), (1, 0), (0, 0), (0, 0)) i
q_j: {} ((0, 0), (0, 0), (1, 0), (0, 0)) j
q_k: {} ((0, 0), (0, 0), (0, 0), (1, 0)) k
q_random(): ((0.4223541664091415, 0), (0.7377423919696418, 0), (0.3964313487306321, 0), (0.8980359267305139, 0)) ?
q_big reduced: ((0, 1), (0, 1), (0, 1), (0, 1)) Q-reduced
rotate: {} ((1.0, 0.0), (0.0, 2.0), (3.0, 0.0), (4.0, 0.0)) Q/|Q|xQxQ/|Q|*-reduced
square: ((1, 29), (0, 4), (0, 6), (0, 8)) Q^2
square reduced: ((0, 28), (0, 4), (0, 6), (0, 8)) Q^2-reduced
triple: {} ((0, 2), (124, 0), (0, 84), (8, 0)) QxQxQ-reduced
vahlen conj -: {} ((1, 0), (2, 0), (3, 0), (4, 0)) Qvc-
vahlen conj ': {} ((1, 0), (2, 0), (3, 0), (0, 4)) Qvc'
vahlen conj *: {} ((1, 0), (0, 2), (0, 3), (4, 0)) Qvc*
----------------------------------------------------------------------
Ran 45 tests in 0.058s

OK

In [16]:
class TestQ8Rep(unittest.TestCase):
    Q12 = Q8([1.0, 2.0, 0, 0])
    Q1123 = Q8([1.0, 1.0, 2, 3])
    Q11p = Q8([1.0, 1.0, 0, 0], representation="polar")
    Q12p = Q8([1.0, 2.0, 0, 0], representation="polar")
    Q12np = Q8([1.0, -2.0, 0, 0], representation="polar")
    Q21p = Q8([2.0, 1.0, 0, 0], representation="polar")
    Q23p = Q8([2.0, 3.0, 0, 0], representation="polar")
    Q13p = Q8([1.0, 3.0, 0, 0], representation="polar")
    Q5p = Q8([5.0, 0, 0, 0], representation="polar")
    
    # @unittest.skip("problems implementing")
    def test_txyz_2_representation(self):
        qr = Q8(self.Q12.txyz_2_representation("")).reduce()
        self.assertTrue(qr.equals(self.Q12))
        qr = Q8(self.Q12.txyz_2_representation("polar")).reduce()
        self.assertTrue(qr.equals(Q8([2.23606797749979, 1.10714871779409, 0, 0])))
        qr = Q8(self.Q1123.txyz_2_representation("spherical")).reduce()
        self.assertTrue(qr.equals(Q8([1.0, 3.7416573867739413, 0.640522312679424, 1.10714871779409])))

        
    # @unittest.skip("problems implementing")    
    def test_representation_2_txyz(self):
        qr = Q8(self.Q12.representation_2_txyz("")).reduce()
        self.assertTrue(qr.equals(self.Q12))
        qr = Q8(self.Q12.representation_2_txyz("polar")).reduce()
        self.assertTrue(qr.equals(Q8([-0.4161468365471424, 0.9092974268256817, 0, 0])))
        qr = Q8(self.Q1123.representation_2_txyz("spherical")).reduce()
        self.assertTrue(qr.equals(Q8([1.0, -0.9001976297355174, 0.12832006020245673, -0.4161468365471424])))
    
    def test_polar_products(self):
        qr = self.Q11p.product(self.Q12p).reduce()
        print("polar 1 1 0 0 * 1 2 0 0: ", qr)
        self.assertTrue(qr.equals(self.Q13p))
        qr = self.Q12p.product(self.Q21p).reduce()
        print("polar 1 2 0 0 * 2 1 0 0: ", qr)
        self.assertTrue(qr.equals(self.Q23p))

    def test_polar_conj(self):
        qr = self.Q12p.conj().reduce()
        print("polar conj of 1 2 0 0: ", qr)
        self.assertTrue(qr.equals(self.Q12np))
        
suite = unittest.TestLoader().loadTestsFromModule(TestQ8Rep())
unittest.TextTestRunner().run(suite);


....
polar conj of 1 2 0 0:  ((1.0, 0) A, (0, 2.0)  𝜈x, (0.0, 0) 𝜈y, (0.0, 0) 𝜈z) Q*-reduced
polar 1 1 0 0 * 1 2 0 0:  ((0.0, 0.9899924966004455), (0.1411200080598673, 0), (0.0, 0.0), (0.0, 0.0)) QxQ-reduced
polar 1 2 0 0 * 2 1 0 0:  ((0.0, 1.979984993200891), (0.2822400161197346, 0), (0.0, 0.0), (0.0, 0.0)) QxQ-reduced
----------------------------------------------------------------------
Ran 4 tests in 0.005s

OK

Class Q8a as nparrays


In [17]:
class Q8a(Doubleta):
    """Quaternions on a quaternion manifold or space-time numbers."""

    def __init__(self, values=None, qtype="Q", representation=""):
        if values is None:
            d_zero = Doubleta()
            self.a = np.array([d_zero.d[0], d_zero.d[0], d_zero.d[0], d_zero.d[0], d_zero.d[0], d_zero.d[0], d_zero.d[0], d_zero.d[0]])
     
        elif isinstance(values, list):
            if len(values) == 4:
                self.a = np.array([Doubleta(values[0]).d[0], Doubleta(values[0]).d[1], 
                                   Doubleta(values[1]).d[0], Doubleta(values[1]).d[1], 
                                   Doubleta(values[2]).d[0], Doubleta(values[2]).d[1], 
                                   Doubleta(values[3]).d[0], Doubleta(values[3]).d[1]])
        
            if len(values) == 8:
                self.a = np.array([Doubleta([values[0], values[1]]).d[0], Doubleta([values[0], values[1]]).d[1],
                                   Doubleta([values[2], values[3]]).d[0], Doubleta([values[2], values[3]]).d[1],
                                   Doubleta([values[4], values[5]]).d[0], Doubleta([values[4], values[5]]).d[1],
                                   Doubleta([values[6], values[7]]).d[0], Doubleta([values[6], values[7]]).d[1]])
        
        self.representation = representation
        
        if representation != "":
            rep = self.representation_2_txyz(representation)
            self.a = np.array(rep)
        
        self.qtype=qtype
                
    def __str__(self):
        """Customize the output."""
        
        
        if self.representation == "":
            string = "(({tp}, {tn}), ({xp}, {xn}), ({yp}, {yn}), ({zp}, {zn})) {qt}".format(tp=self.a[0], tn=self.a[1], 
                                                             xp=self.a[2], xn=self.a[3], 
                                                             yp=self.a[4], yn=self.a[5], 
                                                             zp=self.a[6], zn=self.a[7],
                                                             qt=self.qtype)
    
        elif self.representation == "polar":
            rep = self.txyz_2_representation("polar")
            string = "(({Ap}, {An}) A, ({thetaXp}, {thetaXn})  𝜈x, ({thetaYp}, {thetaYn}) 𝜈y, ({thetaZp}, {thetaZn}) 𝜈z) {qt}".format(Ap=rep[0], An=rep[1], thetaXp=rep[2], thetaXn=rep[3], thetaYp=rep[4], thetaYn=rep[5], thetaZp=rep[6], thetaZn=rep[7], 
                                                                    qt=self.qtype)
 
        elif self.representation == "spherical":
            rep = self.txyz_2_representation("spherical")
            string = "(({tp}, {tn}) t, ({Rp}, {Rn}) R, ({thetap}, {thetan}) θ , ({phip}, {phin}) φ) {qt}".format(tp=rep[0], tn=rep[1], Rp=rep[2], Rn=rep[3], thetap=rep[4], thetan=rep[5], phip=rep[6], phin=rep[7], 
                                                                       qt=self.qtype)
        return string 
        
        
        return "(({tp}, {tn}), ({xp}, {xn}), ({yp}, {yn}), ({zp}, {zn})) {qt}".format(tp=self.a[0], tn=self.a[1], 
                                                             xp=self.a[2], xn=self.a[3], 
                                                             yp=self.a[4], yn=self.a[5], 
                                                             zp=self.a[6], zn=self.a[7],
                                                             qt=self.qtype)
    
        return string
    
    def is_symbolic(self):
        """Looks to see if a symbol is inside one of the terms."""
        
        symbolic = False
        
        for i in range(8):
            if hasattr(self.a[i], "free_symbols"): 
                symbolic = True
            
        return symbolic
        
    def txyz_2_representation(self, representation):
        """Converts Cartesian txyz into an array of 4 values in a different representation."""

        symbolic = self.is_symbolic()
                
        if representation == "":
            rep = [self.a[0], self.a[1], self.a[2], self.a[3], self.a[4], self.a[5], self.a[6], self.a[7]]
        
        elif representation == "polar":
            
            dtr = self.a[0] - self.a[1]
            dxr = self.a[2] - self.a[3]
            dyr = self.a[4] - self.a[5]
            dzr = self.a[6] - self.a[7]
            
            amplitude = (dtr ** 2 + dxr ** 2 + dyr **2 + dzr **2) ** (1/2)
            
            abs_v = self.abs_of_vector().a[0]
            
            if symbolic:
                theta = sp.atan2(abs_v, dtr)
            else:
                theta = math.atan2(abs_v, dtr)
                
            if abs_v == 0:
                thetaX, thetaY, thetaZ = 0, 0, 0
                
            else:
                thetaX = theta * dxr / abs_v
                thetaY = theta * dyr / abs_v
                thetaZ = theta * dzr / abs_v
                
            damp = Doubleta(amplitude)
            dthetaX = Doubleta(thetaX)
            dthetaY = Doubleta(thetaY)
            dthetaZ = Doubleta(thetaZ)
            
            rep = [damp.d[0], damp.d[1], 
                   dthetaX.d[0], dthetaX.d[1], 
                   dthetaY.d[0], dthetaY.d[1], 
                   dthetaZ.d[0], dthetaZ.d[1]]
        
        elif representation == "spherical":
            
            dtr = self.a[0] - self.a[1]
            dxr = self.a[2] - self.a[3]
            dyr = self.a[4] - self.a[5]
            dzr = self.a[6] - self.a[7]
            
            R = (dxr ** 2 + dyr **2 + dzr**2) ** (1/2)
            
            if symbolic:
                theta = sp.acos(dzr / R)
                phi = sp.atan2(dyr, dxr)
            
            else:
                theta = math.acos(dzr / R)
                phi = math.atan2(dyr, dxr)
                
            dt = Doubleta(dtr)
            dR = Doubleta(R)
            dtheta = Doubleta(theta)
            dphi = Doubleta(phi)
            
            rep = [dt.d[0], dt.d[1], 
                   dR.d[0], dR.d[1], 
                   dtheta.d[0], dtheta.d[1], 
                   dphi.d[0], dphi.d[1]]
        
        else:
            print("Oops, don't know representation: ", representation)
            
        return rep
    
    def representation_2_txyz(self, representation):
        """Convert from a representation to Cartesian txyz."""
        
        symbolic = self.is_symbolic()

        if representation == "":
            rep = [self.a[0], self.a[1], self.a[2], self.a[3], self.a[4], self.a[5], self.a[6], self.a[7]]
        
        elif representation == "polar":
                                
            amplitude1, amplitude2 = self.a[0], self.a[1]
            thetaX1, thetaX2 = self.a[2], self.a[3]
            thetaY1, thetaY2 = self.a[4], self.a[5]
            thetaZ1, thetaZ2 = self.a[6], self.a[7]
    
            amp = amplitude1 - amplitude2
            thetaXr = thetaX1 - thetaX2
            thetaYr = thetaY1 - thetaY2
            thetaZr = thetaZ1 - thetaZ2
                                
            theta = (thetaXr ** 2 + thetaYr ** 2 + thetaZr ** 2) ** (1/2)
                
            if theta == 0:
                t = amp
                x, y, z = 0, 0, 0
            
            else:
                if symbolic:
                    t = amp * sp.cos(theta)
                    x = thetaXr / theta * amp * sp.sin(theta)
                    y = thetaYr / theta * amp * sp.sin(theta)
                    z = thetaZr / theta * amp * sp.sin(theta)
                else:
                    t = amp * math.cos(theta)
                    x = thetaXr / theta * amp * math.sin(theta)
                    y = thetaYr / theta * amp * math.sin(theta)
                    z = thetaZr / theta * amp * math.sin(theta)
                    
            dt = Doubleta(t)
            dx = Doubleta(x)
            dy = Doubleta(y)
            dz = Doubleta(z)
            
            rep = [dt.d[0], dt.d[1],
                   dx.d[0], dx.d[1],
                   dy.d[0], dy.d[1],
                   dz.d[0], dz.d[1]]
            
        elif representation == "spherical":
            t1, t2 = self.a[0], self.a[1]
            R1, R2 = self.a[2], self.a[3]
            theta1, theta2 = self.a[4], self.a[5]
            phi1, phi2 = self.a[6], self.a[7]
    
            t = t1 - t2
            R = R1 - R2
            thetar = theta1 - theta2
            phir = phi1 - phi2
            
            if symbolic:
                x = R * sp.sin(thetar) * sp.cos(phir)
                y = R * sp.sin(thetar) * sp.sin(phir)
                z = R * sp.cos(thetar)
                
            else:
                x = R * math.sin(thetar) * math.cos(phir)
                y = R * math.sin(thetar) * math.sin(phir)
                z = R * math.cos(thetar)
                
            dt = Doubleta(t)
            dx = Doubleta(x)
            dy = Doubleta(y)
            dz = Doubleta(z)

            rep = [dt.d[0], dt.d[1],
                   dx.d[0], dx.d[1],
                   dy.d[0], dy.d[1],
                   dz.d[0], dz.d[1]]

        else:
            print("Oops, don't know representation: ", representation)
        
        return rep
        
    def check_representations(self, q1):
        """If they are the same, report true. If not, kick out an exception. Don't add apples to oranges."""

        if self.representation == q1.representation:
            return True
        
        else:
            raise Exception("Oops, 2 quaternions have different representations: {}, {}".format(self.representation, q1.representation))
            return False
    
    def q4(self):
        """Return a 4 element array."""
        return [self.a[0] - self.a[1], self.a[0] - self.a[1], self.a[4] - self.a[5], self.a[6] - self.a[7]]
            
    def q_0(self, qtype="0"):
        """Return a zero quaternion."""
        
        q0 = Q8a(qtype=qtype, representation=self.representation)
        
        return q0
      
    def q_1(self, qtype="1"):
        """Return a multiplicative identity quaternion."""
        
        q1 = Q8a([1, 0, 0, 0, 0, 0, 0, 0], qtype=qtype, representation=self.representation)
        
        return q1
    
    def q_i(self, qtype="i"):
        """Return i."""
        
        qi = Q8a([0, 0, 1, 0, 0, 0, 0, 0], qtype=qtype, representation=self.representation)
        return qi
    
    def q_j(self, qtype="j"):
        """Return j."""
        
        qj = Q8a([0, 0, 0, 0, 1, 0, 0, 0], qtype=qtype, representation=self.representation)
        
        return qj
    

    def q_k(self, qtype="k"):
        """Return k."""
        
        qk = Q8a([0, 0, 0, 0, 0, 0, 1, 0], qtype=qtype, representation=self.representation)
        
        return qk

    def q_random(self, qtype="?"):
        """Return a random-valued quaternion."""

        qr = Q8a([random.random(), random.random(), random.random(), random.random(), random.random(), random.random(), random.random(), random.random()], qtype=qtype)
        qr.representation = self.representation
        
        return qr
    
    def equals(self, q2):
        """Tests if two quaternions are equal."""
        
        self_red = self.reduce()
        q2_red = q2.reduce()
        result = True
        
        for i in range(8):
            if not math.isclose(self_red.a[i], q2_red.a[i]):
                result = False
        
        return result
    
    def conj(self, conj_type=0, qtype="*"):
        """Three types of conjugates."""
        
        conj_q = Q8a()

        # Flip all but t.                          
        if conj_type == 0:   
            conj_q.a[0] = self.a[0]
            conj_q.a[1] = self.a[1]
            conj_q.a[2] = self.a[3]
            conj_q.a[3] = self.a[2]
            conj_q.a[4] = self.a[5]
            conj_q.a[5] = self.a[4]
            conj_q.a[6] = self.a[7]
            conj_q.a[7] = self.a[6]
        
        # Flip all but x.
        if conj_type == 1:
            conj_q.a[0] = self.a[1]
            conj_q.a[1] = self.a[0]
            conj_q.a[2] = self.a[2]
            conj_q.a[3] = self.a[3]
            conj_q.a[4] = self.a[5]
            conj_q.a[5] = self.a[4]
            conj_q.a[6] = self.a[7]
            conj_q.a[7] = self.a[6]
            qtype += "1"

        # Flip all but y.                                 
        if conj_type == 2:
            conj_q.a[0] = self.a[1]
            conj_q.a[1] = self.a[0]
            conj_q.a[2] = self.a[3]
            conj_q.a[3] = self.a[2]
            conj_q.a[4] = self.a[4]
            conj_q.a[5] = self.a[5]
            conj_q.a[6] = self.a[7]
            conj_q.a[7] = self.a[6]
            qtype += "2"
            
        conj_q.qtype = self.qtype + qtype
        conj_q.representation = self.representation
        
        return conj_q
    
    def vahlen_conj(self, conj_type="-", qtype="vc"):
        """Three types of conjugates -'* done by Vahlen in 1901."""
        
        conj_q = Q8a()

        if conj_type == "-":
            conj_q.a[0] = self.a[0]
            conj_q.a[1] = self.a[1]
            conj_q.a[2] = self.a[3]
            conj_q.a[3] = self.a[2]
            conj_q.a[4] = self.a[5]
            conj_q.a[5] = self.a[4]
            conj_q.a[6] = self.a[7]
            conj_q.a[7] = self.a[6]
            qtype += "-"

        # Flip the sign of x and y.
        if conj_type == "'":
            conj_q.a[0] = self.a[0]
            conj_q.a[1] = self.a[1]
            conj_q.a[2] = self.a[3]
            conj_q.a[3] = self.a[2]
            conj_q.a[4] = self.a[5]
            conj_q.a[5] = self.a[4]
            conj_q.a[6] = self.a[6]
            conj_q.a[7] = self.a[7]
            qtype += "'"
            
        # Flip the sign of only z.
        if conj_type == "*":
            conj_q.a[0] = self.a[0]
            conj_q.a[1] = self.a[1]
            conj_q.a[2] = self.a[2]
            conj_q.a[3] = self.a[3]
            conj_q.a[4] = self.a[4]
            conj_q.a[5] = self.a[5]
            conj_q.a[6] = self.a[7]
            conj_q.a[7] = self.a[6]
            qtype += "*"
            
        conj_q.qtype = self.qtype + qtype
        conj_q.representation = self.representation
        
        return conj_q

    def flip_signs(self, conj_type=0, qtype="-"):
        """Flip all the signs, just like multipying by -1."""

        end_qtype = "-{}".format(self.qtype)
        
        t1, t2 = self.a[0], self.a[1]
        x1, x2 = self.a[2], self.a[3]
        y1, y2 = self.a[4], self.a[5]
        z1, z2 = self.a[6], self.a[7]
        
        flip_q = Q8a(qtype=end_qtype)

        flip_q.a[0] = t2
        flip_q.a[1] = t1
        flip_q.a[2] = x2
        flip_q.a[3] = x1
        flip_q.a[4] = y2
        flip_q.a[5] = y1
        flip_q.a[6] = z2
        flip_q.a[7] = z1

        flip_q.qtype = end_qtype
        flip_q.representation = self.representation
        
        return flip_q
    
    def _commuting_products(self, q1):
        """Returns a dictionary with the commuting products."""

        products = {'tt0': self.a[0] * q1.a[0] + self.a[1] * q1.a[1],
                    'tt1': self.a[0] * q1.a[1] + self.a[1] * q1.a[0],
                    
                    'xx+yy+zz0': self.a[2] * q1.a[2] + self.a[3] * q1.a[3] + self.a[4] * q1.a[4] + self.a[5] * q1.a[5] + self.a[6] * q1.a[6] + self.a[7] * q1.a[7], 
                    'xx+yy+zz1': self.a[2] * q1.a[3] + self.a[3] * q1.a[2] + self.a[4] * q1.a[5] + self.a[5] * q1.a[4] + self.a[6] * q1.a[7] + self.a[7] * q1.a[6], 
                    
                    'tx+xt0': self.a[0] * q1.a[2] + self.a[1] * q1.a[3] + self.a[2] * q1.a[0] + self.a[3] * q1.a[1],
                    'tx+xt1': self.a[0] * q1.a[3] + self.a[1] * q1.a[2] + self.a[3] * q1.a[0] + self.a[2] * q1.a[1],
                    
                    'ty+yt0': self.a[0] * q1.a[4] + self.a[1] * q1.a[5] + self.a[4] * q1.a[0] + self.a[5] * q1.a[1],
                    'ty+yt1': self.a[0] * q1.a[5] + self.a[1] * q1.a[4] + self.a[5] * q1.a[0] + self.a[4] * q1.a[1],
                    
                    'tz+zt0': self.a[0] * q1.a[6] + self.a[1] * q1.a[7] + self.a[6] * q1.a[0] + self.a[7] * q1.a[1],
                    'tz+zt1': self.a[0] * q1.a[7] + self.a[1] * q1.a[6] + self.a[7] * q1.a[0] + self.a[6] * q1.a[1]
                    }
        
        return products
    
    def _anti_commuting_products(self, q1):
        """Returns a dictionary with the three anti-commuting products."""

        yz0 = self.a[4] * q1.a[6] + self.a[5] * q1.a[7]
        yz1 = self.a[4] * q1.a[7] + self.a[5] * q1.a[6]
        zy0 = self.a[6] * q1.a[4] + self.a[7] * q1.a[5]
        zy1 = self.a[6] * q1.a[5] + self.a[7] * q1.a[4]

        zx0 = self.a[6] * q1.a[2] + self.a[7] * q1.a[3]
        zx1 = self.a[6] * q1.a[3] + self.a[7] * q1.a[2]
        xz0 = self.a[2] * q1.a[6] + self.a[3] * q1.a[7]
        xz1 = self.a[2] * q1.a[7] + self.a[3] * q1.a[6]

        xy0 = self.a[2] * q1.a[4] + self.a[3] * q1.a[5]
        xy1 = self.a[2] * q1.a[5] + self.a[3] * q1.a[4]
        yx0 = self.a[4] * q1.a[2] + self.a[5] * q1.a[3]
        yx1 = self.a[4] * q1.a[3] + self.a[5] * q1.a[2]
                                   
        products = {'yz-zy0': yz0 + zy1,
                    'yz-zy1': yz1 + zy0,
                    
                    'zx-xz0': zx0 + xz1,
                    'zx-xz1': zx1 + xz0,
                    
                    'xy-yx0': xy0 + yx1,
                    'xy-yx1': xy1 + yx0,
                   
                    'zy-yz0': yz1 + zy0,
                    'zy-yz1': yz0 + zy1,
                    
                    'xz-zx0': zx1 + xz0,
                    'xz-zx1': zx0 + xz1,
                    
                    'yx-xy0': xy1 + yx0,
                    'yx-xy1': xy0 + yx1
                   }
        
        return products
    
    def _all_products(self, q1):
        """Returns a dictionary with all possible products."""

        products = self._commuting_products(q1)
        products.update(self._anti_commuting_products(q1))
        
        return products
    
    def square(self, qtype="^2"):
        """Square a quaternion."""
        
        end_qtype = "{}{}".format(self.qtype, qtype)
        
        qxq = self._commuting_products(self)
        
        sq_q = Q8a(qtype=self.qtype)        
        sq_q.a[0] = qxq['tt0'] + (qxq['xx+yy+zz1'])
        sq_q.a[1] = qxq['tt1'] + (qxq['xx+yy+zz0'])
        sq_q.a[2] = qxq['tx+xt0']
        sq_q.a[3] = qxq['tx+xt1']
        sq_q.a[4] = qxq['ty+yt0']
        sq_q.a[5] = qxq['ty+yt1']
        sq_q.a[6] = qxq['tz+zt0']
        sq_q.a[7] = qxq['tz+zt1']
        
        sq_q.qtype = end_qtype
        sq_q.representation = self.representation
        
        return sq_q
    
    def reduce(self, qtype="-reduce"):
        """Put all Doubletas into the reduced form so one of each pair is zero."""

        end_qtype = "{}{}".format(self.qtype, qtype)
        
        red_t = Doubleta([self.a[0], self.a[1]]).d_reduce()
        red_x = Doubleta([self.a[2], self.a[3]]).d_reduce()
        red_y = Doubleta([self.a[4], self.a[5]]).d_reduce()
        red_z = Doubleta([self.a[6], self.a[7]]).d_reduce()
            
        q_red = Q8a(qtype=self.qtype)
        q_red.a[0] = red_t.d[0]
        q_red.a[1] = red_t.d[1]
        q_red.a[2] = red_x.d[0]
        q_red.a[3] = red_x.d[1]
        q_red.a[4] = red_y.d[0]
        q_red.a[5] = red_y.d[1]
        q_red.a[6] = red_z.d[0]
        q_red.a[7] = red_z.d[1]

        q_red.qtype = end_qtype
        q_red.representation = self.representation
        
        return q_red
    
    def norm_squared(self, qtype="|| ||^2"):
        """The norm_squared of a quaternion."""
        
        end_qtype = "||{}||^2".format(self.qtype)
        
        qxq = self._commuting_products(self)
        
        n_q = Q8a()        
        n_q.a[0] = qxq['tt0'] + qxq['xx+yy+zz0']
        n_q.a[1] = qxq['tt1'] + qxq['xx+yy+zz1']
        result = n_q.reduce()
        
        result.qtype = end_qtype
        result.representation = self.representation
        
        return result
    
    def norm_squared_of_vector(self, qtype="V(|| ||)^2"):
        """The norm_squared of the vector of a quaternion."""
        
        end_qtype = "V||({})||^2".format(self.qtype)
        
        qxq = self._commuting_products(self)
        
        nv_q = Q8a()
        nv_q.a[0] = qxq['xx+yy+zz0']
        nv_q.a[1] = qxq['xx+yy+zz1']
        result = nv_q.reduce()
        result.qtype = end_qtype
        result.representation = self.representation

        return result
        
    def abs_of_q(self, qtype="| |"):
        """The absolute value, the square root of the norm_squared."""

        end_qtype = "|{}|".format(self.qtype)
        
        abq = self.norm_squared()
        sqrt_t0 = abq.a[0] ** (1/2)
        abq.a[0] = sqrt_t0
        abq.qtype = end_qtype
        abq.representation = self.representation
        
        return abq

    def abs_of_vector(self, qtype="|V()|)"):
        """The absolute value of the vector, the square root of the norm_squared of the vector."""

        end_qtype = "|V({})|".format(self.qtype, qtype)
        
        av = self.norm_squared_of_vector()
        sqrt_t = av.a[0] ** (1/2)
        av.a[0] = sqrt_t
        av.qtype = end_qtype
        av.representation = self.representation
        
        return av
    
    def normalize(self, qtype="U"):
        """Normalize a quaternion"""
        
        end_qtype = "{}U".format(self.qtype)
        
        abs_q_inv = self.abs_of_q().invert()
        n_q = self.product(abs_q_inv)
        n_q.qtype = end_qtype
        n_q.representation=self.representation
        
        return n_q
    
    def add(self, q1, qtype="+"):
        """Form a add given 2 quaternions."""

        self.check_representations(q1)
        
        add_q = Q8a()
        for i in range(0, 8):
            add_q.a[i] = self.a[i] + q1.a[i]
                    
        add_q.qtype = "{f}+{s}".format(f=self.qtype, s=q1.qtype)
        add_q.representation = self.representation    
        
        return add_q    

    def dif(self, q1, qtype="-"):
        """Form a add given 2 quaternions."""

        self.check_representations(q1)
        
        dif_q = Q8a()

        dif_q.a[0] = self.a[0] + q1.a[1]
        dif_q.a[1] = self.a[1] + q1.a[0]
        dif_q.a[2] = self.a[2] + q1.a[3]
        dif_q.a[3] = self.a[3] + q1.a[2]
        dif_q.a[4] = self.a[4] + q1.a[5]
        dif_q.a[5] = self.a[5] + q1.a[4]
        dif_q.a[6] = self.a[6] + q1.a[7]
        dif_q.a[7] = self.a[7] + q1.a[6]
     
        dif_q.qtype = "{f}-{s}".format(f=self.qtype, s=q1.qtype)
        dif_q.representation = self.representation
        
        return dif_q
    
    def product(self, q1, kind="", reverse=False, qtype=""):
        """Form a product given 2 quaternions."""

        self.check_representations(q1)
        
        commuting = self._commuting_products(q1)
        q_even = Q8a()
        q_even.a[0] = commuting['tt0'] + commuting['xx+yy+zz1']
        q_even.a[1] = commuting['tt1'] + commuting['xx+yy+zz0']
        q_even.a[2] = commuting['tx+xt0']
        q_even.a[3] = commuting['tx+xt1']
        q_even.a[4] = commuting['ty+yt0']
        q_even.a[5] = commuting['ty+yt1']
        q_even.a[6] = commuting['tz+zt0']
        q_even.a[7] = commuting['tz+zt1']
        
        anti_commuting = self._anti_commuting_products(q1)
        q_odd = Q8a()
        
        if reverse:
            q_odd.a[2] = anti_commuting['zy-yz0']
            q_odd.a[3] = anti_commuting['zy-yz1']
            q_odd.a[4] = anti_commuting['xz-zx0']
            q_odd.a[5] = anti_commuting['xz-zx1']
            q_odd.a[6] = anti_commuting['yx-xy0']
            q_odd.a[7] = anti_commuting['yx-xy1']
            
        else:
            q_odd.a[2] = anti_commuting['yz-zy0']
            q_odd.a[3] = anti_commuting['yz-zy1']
            q_odd.a[4] = anti_commuting['zx-xz0']
            q_odd.a[5] = anti_commuting['zx-xz1']
            q_odd.a[6] = anti_commuting['xy-yx0']
            q_odd.a[7] = anti_commuting['xy-yx1']
        
        if kind == "":
            result = q_even.add(q_odd)
            times_symbol = "x"
        elif kind.lower() == "even":
            result = q_even
            times_symbol = "xE"
        elif kind.lower() == "odd":
            result = q_odd
            times_symbol = "xO"
        else:
            raise Exception("Three 'kind' values are known: '', 'even', and 'odd'")
            
        if reverse:
            times_symbol = times_symbol.replace('x', 'xR')    
            
        result.qtype = "{f}{ts}{s}".format(f=self.qtype, ts=times_symbol, s=q1.qtype)
        result.representation = self.representation
        
        return result

    def Euclidean_product(self, q1, kind="", reverse=False, qtype=""):
        """Form a product p* q given 2 quaternions, not associative."""

        self.check_representations(q1)
        
        pq = Q8a()
        pq = self.conj().product(q1, kind, reverse, qtype)
        pq.representation = self.representation
        
        return pq

    def invert(self, qtype="^-1"):
        """Invert a quaternion."""
        
        end_qtype = "{}{}".format(self.qtype, qtype)
        
        q_conj = self.conj()
        q_norm_squared = self.norm_squared().reduce()
        
        if q_norm_squared.a[0] == 0:
            return self.q0()
        
        q_norm_squared_inv = Q8a([1.0 / q_norm_squared.a[0], 0, 0, 0, 0, 0, 0, 0])

        q_inv = q_conj.product(q_norm_squared_inv)
        q_inv.qtype = end_qtype
        q_inv.representation = self.representation
        
        return q_inv

    def divide_by(self, q1, qtype=""):
        """Divide one quaternion by another. The order matters unless one is using a norm_squared (real number)."""

        self.check_representations(q1)
        
        q_inv = q1.invert()
        q_div = self.product(q_inv) 
        q_div.qtype = "{f}/{s}".format(f=self.qtype, s=q1.qtype)
        q_div.representation = self.representation    
        
        return q_div
    
    def triple_product(self, q1, q2):
        """Form a triple product given 3 quaternions."""
        
        self.check_representations(q1)
        self.check_representations(q2)
        
        triple = self.product(q1).product(q2)
        
        return triple
    
    # Quaternion rotation involves a triple product:  UQU∗
    # where the U is a unitary quaternion (having a norm_squared of one).
    def rotate(self, a_1p=0, a_1n=0, a_2p=0, a_2n=0, a_3p=0, a_3n=0):
        """Do a rotation given up to three angles."""
    
        u = Q8a([0, 0, a_1p, a_1n, a_2p, a_2n, a_3p, a_3n])
        u_abs = u.abs_of_q()
        u_norm_squaredalized = u.divide_by(u_abs)
        q_rot = u_norm_squaredalized.triple_product(self, u_norm_squaredalized.conj())
        q_rot.representation = self.representation
        
        return q_rot
    
    # A boost also uses triple products like a rotation, but more of them.
    # This is not a well-known result, but does work.
    def boost(self, beta_x=0, beta_y=0, beta_z=0, qtype="boost"):
        """A boost along the x, y, and/or z axis."""
        
        end_qtype = "{}{}".format(self.qtype, qtype)
        
        boost = Q8a(sr_gamma_betas(beta_x, beta_y, beta_z))
        b_conj = boost.conj()
        
        triple_1 = boost.triple_product(self, b_conj)
        triple_2 = boost.triple_product(boost, self).conj()
        triple_3 = b_conj.triple_product(b_conj, self).conj()
              
        triple_23 = triple_2.dif(triple_3)
        half_23 = triple_23.product(Q8a([0.5, 0, 0, 0, 0, 0, 0, 0]))
        triple_123 = triple_1.add(half_23)
        
        triple_123.qtype = end_qtype
        triple_123.representation = self.representation
        
        return triple_123
    
    # g_shift is a function based on the space-times-time invariance proposal for gravity,
    # which proposes that if one changes the distance from a gravitational source, then
    # squares a measurement, the observers at two different hieghts agree to their
    # space-times-time values, but not the intervals.
    def g_shift(self, dimensionless_g, g_form="exp", qtype="g_shift"):
        """Shift an observation based on a dimensionless GM/c^2 dR."""
        
        end_qtype = "{}{}".format(self.qtype, qtype)
        
        if g_form == "exp":
            g_factor = sp.exp(dimensionless_g)
            if qtype == "g_shift":
                qtype = "g_exp"
        elif g_form == "minimal":
            g_factor = 1 + 2 * dimensionless_g + 2 * dimensionless_g ** 2
            if qtype == "g_shift":
                qtype = "g_minimal"
        else:
            print("g_form not defined, should be 'exp' or 'minimal': {}".format(g_form))
            return self
        exp_g = sp.exp(dimensionless_g)
        
        dt = Doubleta([self.a[0] / exp_g, self.a[1] / exp_g])
        dx = Doubleta([self.a[2] * exp_g, self.a[3] * exp_g])
        dy = Doubleta([self.a[4] * exp_g, self.a[5] * exp_g])
        dz = Doubleta([self.a[6] * exp_g, self.a[7] * exp_g])
        
        g_q = Q8a(qtype=self.qtype)
        g_q.a[0] = dt.d[0]
        g_q.a[1] = dt.d[1]
        g_q.a[2] = dx.d[0]
        g_q.a[3] = dx.d[1]
        g_q.a[4] = dy.d[0]
        g_q.a[5] = dy.d[1]
        g_q.a[6] = dz.d[0]
        g_q.a[7] = dz.d[1]
        
        g_q.qtype = end_qtype
        g_q.representation = self.representation
        
        return g_q
    
    def sin(self, qtype="sin"):
        """Take the sine of a quaternion, (sin(t) cosh(|R|), cos(t) sinh(|R|) R/|R|)"""
        
        end_qtype = "sin({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()
        red_t = Doubleta([self.a[0], self.a[1]]).d_reduce()

        if red_t.d[0] == 0 and red_t.d[1] != 0:
            if abs_v.a[0] == 0:    
                return Q8a([-1 * math.sin(red_t.d[1]), 0, 0, 0], qtype=end_qtype, representation=self.representation)
        
            sint = math.sin(-1 * red_t.d[1])
            cost = math.cos(-1 *red_t.d[1])    
        else:
            if abs_v.a[0] == 0:    
                return Q8a([math.sin(red_t.d[0]), 0, 0, 0], qtype=end_qtype, representation=self.representation)

            sint = math.sin(red_t.d[0])
            cost = math.cos(red_t.d[0])    
            
        sinhR = math.sinh(abs_v.a[0])
        coshR = math.cosh(abs_v.a[0])
        
        k = cost * sinhR / abs_v.a[0]
            
        q_out_dt = Doubleta(sint * coshR)
        q_out_dx = Doubleta(k * (self.a[2] - self.a[3]))
        q_out_dy = Doubleta(k * (self.a[4] - self.a[5]))
        q_out_dz = Doubleta(k * (self.a[6] - self.a[7]))
        q_out = Q8a([q_out_t, q_out_x, q_out_y, q_out_z], qtype=end_qtype, representation=self.representation)
        
        return q_out
     
    def cos(self, qtype="cos"):
        """Take the cosine of a quaternion, (cos(t) cosh(|R|), sin(t) sinh(|R|) R/|R|)"""

        end_qtype = "cos({sq})".format(sq=self.qtype)
            
        abs_v = self.abs_of_vector()
        red_t = Doubleta([self.a[0], self.a[1]]).d_reduce()
       
        if red_t.d[0] == 0 and red_t.d[1] != 0:
            if abs_v.a[0] == 0:    
                return Q8a([math.cos(-1 * red_t.d[1]), 0, 0, 0], qtype=end_qtype)
        
            sint = math.sin(-1 * red_t.d[1])
            cost = math.cos(-1 * red_t.d[1]) 
            
        else:
            if abs_v.a[0] == 0:    
                return Q8a([math.cos(red_t.d[0]), 0, 0, 0], qtype=end_qtype)
        
            sint = math.sin(red_t.d[0])
            cost = math.cos(red_t.d[0])
            
        sinhR = math.sinh(abs_v.a[0])
        coshR = math.cosh(abs_v.a[0])
        
        k = -1 * sint * sinhR / abs_v.a[0]
            
        q_out_dt = Doubleta(cost * coshR)
        q_out_dx = Doubleta(k * (self.a[2] - self.a[3]))
        q_out_dy = Doubleta(k * (self.a[4] - self.a[5]))
        q_out_dz = Doubleta(k * (self.a[6] - self.a[7]))
        q_out = Q8a([q_out_t, q_out_x, q_out_y, q_out_z], qtype=end_qtype, representation=