from typing import Optional, Sequence
import numpy
from .backend import is_symbolic
[docs]
class Quaternion:
"""
Quaternion representing a 4-dimensional number ``[w, x, y, z]``.
Used as the building block of `DualQuaternion`, where the primal and
dual parts are each a ``Quaternion``.
By default, all computation is performed with NumPy (``float64``). When
the global backend is set to ``"sympy"`` via
:func:`.set_backend`, construction transparently
returns a :class:`.QuaternionSymbolic` instance
instead, with no change to the calling code required.
Parameters
----------
coeffs :
Coefficients ``[w, x, y, z]``. If ``None``, the identity
quaternion ``[1, 0, 0, 0]`` is constructed.
Attributes
----------
q : numpy.ndarray
4-vector of quaternion coefficients ``[w, x, y, z]``.
Raises
------
ValueError
If ``coeffs`` is not a 4-vector.
Examples
--------
.. code-block:: python
from rational_linkages import Quaternion
identity = Quaternion()
q = Quaternion([1.0, 2.0, 3.0, 4.0])
print(identity)
print(q)
.. clear-namespace::
.. code-block:: python
# Fully symbolic backend — set once at the top of your script
import rational_linkages
rational_linkages.set_backend("sympy")
from rational_linkages import Quaternion
from sympy import symbols
a, b = symbols("a b", real=True)
q = Quaternion([a, b, 0, 0]) # transparently returns QuaternionSymbolic
.. clear-namespace::
"""
__hash__ = None # mutable, not hashable by default; can be overridden in subclasses
__array_priority__ = 20.0 # prioritizes Quaternion over numpy array __rmul__
# ------------------------------------------------------------------
# Factory
# ------------------------------------------------------------------
def __new__(cls, coeffs=None):
"""
Intercept construction and return a
:class:`.QuaternionSymbolic` when the global
backend is ``"sympy"``.
Only applied when ``cls`` is exactly ``Quaternion``; subclass
constructors are never redirected, preventing infinite recursion.
Parameters
----------
coeffs :
Forwarded unchanged to ``__init__``.
Returns
-------
Quaternion or QuaternionSymbolic
A numeric or symbolic instance depending on the active backend.
"""
if cls is Quaternion:
symbolic = is_symbolic() or (
coeffs is not None
and any(hasattr(c, 'free_symbols') for c in coeffs)
)
if symbolic:
from .QuaternionSymbolic import QuaternionSymbolic
return object.__new__(QuaternionSymbolic)
return object.__new__(cls)
# ------------------------------------------------------------------
# Construction
# ------------------------------------------------------------------
def __init__(self, coeffs: Optional[Sequence[float]] = None):
if coeffs is not None:
if len(coeffs) != 4:
raise ValueError("Quaternion: coeffs has to be 4-vector")
try:
self.q = numpy.asarray(coeffs, dtype=numpy.float64)
except (TypeError, ValueError):
self.q = numpy.asarray(coeffs, dtype=object)
else:
self.q = numpy.array([1.0, 0.0, 0.0, 0.0], dtype=numpy.float64)
@property
def real(self):
return self.q[0]
@property
def imag(self):
return self.q[1:]
# ------------------------------------------------------------------
# Indexing
# ------------------------------------------------------------------
def __getitem__(self, idx):
"""
Get a coefficient by index.
Parameters
----------
idx :
Index in range ``0..3``.
Returns
-------
float
Coefficient at ``idx``.
"""
return self.q[idx]
def __setitem__(self, idx, value):
"""
Set a coefficient by index.
Parameters
----------
idx :
Index in range ``0..3``.
value :
New coefficient value.
"""
self.q[idx] = value
def __len__(self):
"""Length of the quaternion, always 4."""
return 4
def __iter__(self):
"""Iterate over quaternions."""
return iter(self.q)
# ------------------------------------------------------------------
# Representation
# ------------------------------------------------------------------
def __repr__(self):
q = numpy.array2string(self.array(),
precision=16,
suppress_small=True,
separator=', ',
max_line_width=100000)
return f"{self.__class__.__qualname__}({q})"
# ------------------------------------------------------------------
# Properties
# ------------------------------------------------------------------
@property
def coordinates(self):
"""
Return the coordinates of the quaternion.
Returns
-------
numpy.ndarray
4-vector of quaternion coordinates.
"""
return self.q.copy()
# ------------------------------------------------------------------
# Arithmetic operators
# ------------------------------------------------------------------
def __add__(self, other: "Quaternion") -> "Quaternion":
"""
Add two quaternions.
Parameters
----------
other :
Quaternion to add.
Returns
-------
Quaternion
Element-wise sum.
"""
return self.__class__(self.q + other.q)
def __sub__(self, other: "Quaternion") -> "Quaternion":
"""
Subtract two quaternions.
Parameters
----------
other :
Quaternion to subtract.
Returns
-------
Quaternion
Element-wise difference.
"""
return self.__class__(self.q - other.q)
def __mul__(self, other: "Quaternion | int | float") -> "Quaternion":
"""
Multiply two quaternions, or scale by a scalar.
Parameters
----------
other :
Quaternion, or a scalar ``int`` / ``float``.
Returns
-------
Quaternion
Hamilton product, or scalar-scaled quaternion.
"""
if isinstance(other, Quaternion):
w, x, y, z = self.q
ow, ox, oy, oz = other.q
return self.__class__(numpy.array([
w * ow - x * ox - y * oy - z * oz,
w * ox + x * ow + y * oz - z * oy,
w * oy - x * oz + y * ow + z * ox,
w * oz + x * oy - y * ox + z * ow,
]))
else:
return self.__class__(self.q * other)
def __rmul__(self, other: "Quaternion | int | float") -> "Quaternion":
"""Scalar-on-left multiplication, delegates to ``__mul__``."""
from numbers import Number # lazy import
if isinstance(other, Number):
return self.__mul__(other)
return NotImplemented
def __truediv__(self, other: "Quaternion | int | float") -> "Quaternion":
"""
Divide by a quaternion or scalar.
Parameters
----------
other :
Quaternion, or a scalar ``int`` / ``float``.
Returns
-------
Quaternion
``self * other.inv()`` for quaternions, or element-wise
division for scalars.
"""
if isinstance(other, Quaternion):
return self * other.inv()
return self.__class__(self.q / other)
def __neg__(self) -> "Quaternion":
"""
Negate the quaternion.
Returns
-------
Quaternion
Quaternion with all coefficients negated.
"""
return self.__class__(-self.q)
def __eq__(self, other: "Quaternion") -> bool:
"""
Test coefficient-wise equality.
Parameters
----------
other :
Quaternion to compare against.
Returns
-------
bool
``True`` if all coefficients are equal.
"""
return numpy.array_equal(self.array(), other.array())
# ------------------------------------------------------------------
# Core operations
# ------------------------------------------------------------------
[docs]
def array(self) -> numpy.ndarray:
"""
Return coefficients as a numpy array.
Returns
-------
numpy.ndarray
4-vector ``[w, x, y, z]``.
Examples
--------
.. code-block:: python
from rational_linkages import Quaternion
q = Quaternion([1, 2, 3, 4])
q_array = q.array()
print(q_array) # numpy.array([1., 2., 3., 4.])
.. clear-namespace::
"""
return self.q.copy()
[docs]
def conjugate(self) -> "Quaternion":
"""
Quaternion conjugate.
Returns
-------
Quaternion
``[w, -x, -y, -z]``.
Examples
--------
.. code-block:: python
from rational_linkages import Quaternion
q = Quaternion([1, 2, 3, 4])
q_conj = q.conjugate()
print(q_conj) # [1., -2., -3., -4.]
.. clear-namespace::
"""
return self.__class__([self.q[0], -self.q[1], -self.q[2], -self.q[3]])
[docs]
def norm(self) -> float:
"""
Quaternion norm (also called the Quadrance).
Returns the squared length ``w² + x² + y² + z²``, not the
Euclidean length. See `length` for the latter.
Returns
-------
float
Squared norm.
Examples
--------
.. code-block:: python
from rational_linkages import Quaternion
q = Quaternion([1, 2, 3, 4])
q_quadrance = q.norm()
print(q_quadrance)
.. clear-namespace::
"""
return float(numpy.dot(self.q, self.q))
[docs]
def length(self) -> float:
"""
Euclidean length of the quaternion.
Returns
-------
float
``sqrt(norm())``.
Examples
--------
.. code-block:: python
from rational_linkages import Quaternion
q = Quaternion([1, 2, 3, 4])
q_len = q.length()
print(q_len)
.. clear-namespace::
"""
return float(numpy.sqrt(self.norm()))
[docs]
def inv(self) -> "Quaternion":
"""
Quaternion inverse.
Returns
-------
Quaternion
``conjugate() / norm()``.
Examples
--------
.. code-block:: python
from rational_linkages import Quaternion
q = Quaternion([1, 2, 3, 4])
q_inv = q.inv()
print(q_inv)
.. clear-namespace::
"""
return self.__class__(self.conjugate().q / self.norm())
[docs]
def normalize(self) -> "Quaternion":
"""
Normalize the quaternion to unit length.
Returns
-------
Quaternion
Normalized quaternion with the same direction but length 1.
Examples
--------
.. code-block:: python
from rational_linkages import Quaternion
q = Quaternion([2, 1, -2, 0])
q_normalized = q.normalize()
print(q_normalized)
.. clear-namespace::
"""
return self / self.length()
[docs]
def eval(self, subs: dict) -> "Quaternion":
"""
Evaluate the quaternion by substituting symbols with values.
Parameters
----------
subs : dict
Dictionary mapping SymPy symbols to numeric values.
Returns
-------
Quaternion
New numeric quaternion with substitutions applied, backed by
``float64``.
Examples
--------
.. code-block:: python
from rational_linkages import Quaternion
from sympy import symbols
t = symbols("t")
q1 = Quaternion([5*t, 3, 0, -t])
q_numeric = q1.eval({t : 1})
print(type(q_numeric)) # Quaternion (numeric)
print(q_numeric)
.. clear-namespace::
"""
evaluated = [float(v.subs(subs)) if hasattr(v, "subs") else float(v)
for v in self.q]
return Quaternion(evaluated)
[docs]
def evalf(self) -> "Quaternion":
"""
Placeholder for QuaternionSymbolic.evalf() method.
Returns
-------
Quaternion
Evaluated quaternion.
"""
return self