Source code for rational_linkages.QuaternionSymbolic

from typing import Optional, Sequence

import numpy
import sympy

from .Quaternion import Quaternion


[docs] class QuaternionSymbolic(Quaternion): """ Symbolic quaternion backed by SymPy expressions. Subclass of :class:`.Quaternion` for algebraic computation. Typically, not instantiated directly — when the global backend is set to ``"sympy"`` via :func:`.set_backend`, :class:`.Quaternion` transparently returns instances of this class via its ``__new__`` factory. All arithmetic operators return :class:`QuaternionSymbolic` instances. Products are simplified via :func:`sympy.expand` and inverses via :func:`sympy.simplify` for clean algebraic output. Parameters ---------- coeffs : Coefficients ``[w, x, y, z]`` as SymPy expressions or plain numbers. If ``None``, the identity quaternion ``[1, 0, 0, 0]`` is constructed. Attributes ---------- q : numpy.ndarray Object-dtype array of SymPy expressions ``[w, x, y, z]``. Raises ------ ValueError If ``coeffs`` is not a 4-vector. Examples -------- .. code-block:: python import rational_linkages rational_linkages.set_backend("sympy") from rational_linkages import Quaternion from sympy import symbols a, b, c, d = symbols("a b c d", real=True) q1 = Quaternion([a, b, c, d]) q2 = Quaternion([1, 0, 0, 0]) # identity print(q1 * q2) # QuaternionSymbolic([a, b, c, d]) print(q1.norm()) # a**2 + b**2 + c**2 + d**2 rational_linkages.set_backend("numpy") # restore default .. clear-namespace:: """ # ------------------------------------------------------------------ # Construction # ------------------------------------------------------------------ def __init__(self, coeffs: Optional[Sequence] = None): if coeffs is not None: if len(coeffs) != 4: raise ValueError("QuaternionSymbolic: coeffs has to be 4-vector") self.q = numpy.array( [sympy.sympify(v) for v in coeffs], dtype=object ) else: self.q = numpy.array( [sympy.Integer(1), sympy.Integer(0), sympy.Integer(0), sympy.Integer(0)], dtype=object, ) # ------------------------------------------------------------------ # Representation # ------------------------------------------------------------------ def __repr__(self): entries = ", ".join(str(v) for v in self.q) return f"Qt([{entries}])" # ------------------------------------------------------------------ # Arithmetic operators # ------------------------------------------------------------------ def __mul__(self, other: "Quaternion | int | float") -> "QuaternionSymbolic": """ Multiply two quaternions, or scale by a scalar. Results are simplified via :func:`sympy.expand`. Parameters ---------- other : Quaternion, or a scalar ``int`` / ``float``. Returns ------- QuaternionSymbolic 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__([ sympy.expand(w * ow - x * ox - y * oy - z * oz), sympy.expand(w * ox + x * ow + y * oz - z * oy), sympy.expand(w * oy - x * oz + y * ow + z * ox), sympy.expand(w * oz + x * oy - y * ox + z * ow), ]) else: return self.__class__(self.q * sympy.sympify(other)) def __rmul__(self, other: "int | float | sympy.Expr") -> "QuaternionSymbolic": """Scalar-on-left multiplication, delegates to ``__mul__``.""" from numbers import Number if isinstance(other, (Number, sympy.Basic)): return self.__mul__(other) return NotImplemented def __eq__(self, other: "Quaternion") -> bool: """ Test coefficient-wise equality via symbolic simplification. Parameters ---------- other : Quaternion to compare against. Returns ------- bool ``True`` if all coefficient differences simplify to zero. """ return all(sympy.simplify(a - b) == 0 for a, b in zip(self.q, other.q)) # ------------------------------------------------------------------ # Core operations # ------------------------------------------------------------------
[docs] def array(self) -> numpy.ndarray: """ Return coefficients as an object-dtype numpy array. Returns ------- numpy.ndarray Object-dtype 4-vector ``[w, x, y, z]`` of SymPy expressions. """ return numpy.array(self.q, dtype=object)
[docs] def norm(self) -> sympy.Expr: """ Quaternion norm (also called the Quadrance). Returns the squared length ``w² + x² + y² + z²``, not the Euclidean length. See `length` for the latter. Returns ------- sympy.Expr Squared norm as a SymPy expression. """ return sympy.expand(sum(v ** 2 for v in self.q))
[docs] def length(self) -> sympy.Expr: """ Euclidean length of the quaternion. Returns ------- sympy.Expr ``sqrt(norm())`` as a SymPy expression. """ return sympy.sqrt(self.norm())
[docs] def inv(self) -> "QuaternionSymbolic": """ Quaternion inverse. Returns ------- QuaternionSymbolic ``conjugate() / norm()``, with each component simplified. """ n = self.norm() return self.__class__([sympy.simplify(v / n) for v in self.conjugate().q])
[docs] def eval(self, subs: dict) -> "QuaternionSymbolic": """ Evaluate the quaternion by substituting symbols with values. Parameters ---------- subs : dict Dictionary mapping SymPy symbols to values. Returns ------- QuaternionSymbolic New quaternion with substitutions applied. Examples -------- .. code-block:: python import rational_linkages rational_linkages.set_backend("sympy") from rational_linkages import Quaternion from sympy import symbols a, b, c, d = symbols("a b c d", real=True) q1 = Quaternion([a, b, c, d]) subs = {a: 1, b: -2, c: 0, d: 0} q1_evaluated = q1.eval(subs) print(q1_evaluated) .. clear-namespace:: """ return self.__class__([v.subs(subs) for v in self.q])
[docs] def evalf(self): """ Replace rational numbers by numerical ones. Returns ------- Quaternion Evaluated numerically. """ return Quaternion( numpy.array([val.evalf() for val in self.coordinates], dtype=numpy.float64))