Source code for rational_linkages.DualQuaternionSymbolic

from typing import Optional, Sequence
from numbers import Number

import numpy
import sympy

from .DualQuaternion import DualQuaternion


[docs] class DualQuaternionSymbolic(DualQuaternion): """ Symbolic dual quaternion backed by SymPy expressions. Subclass of :class:`.DualQuaternion` for algebraic computation. Typically, not instantiated directly — when the global backend is set to ``"sympy"`` via :func:`.set_backend`, :class:`.DualQuaternion` transparently returns instances of this class via its ``__new__`` factory. All arithmetic operators return :class:`DualQuaternionSymbolic` instances. Products are simplified via :func:`sympy.expand` and inverses via :func:`sympy.simplify` for clean algebraic output. Parameters ---------- coeffs : 8-vector of Study parameters ``[p0, p1, p2, p3, d0, d1, d2, d3]`` as SymPy expressions or plain numbers. If ``None``, the identity dual quaternion ``[1, 0, 0, 0, 0, 0, 0, 0]`` is constructed. Attributes ---------- p : QuaternionSymbolic Primal part. d : QuaternionSymbolic Dual part. Raises ------ ValueError If ``coeffs`` is not an 8-vector. Examples -------- .. code-block:: python import rational_linkages rational_linkages.set_backend("sympy") from rational_linkages import DualQuaternion from sympy import symbols p0, p1, p2, p3, d0, d1, d2, d3 = symbols( "p0 p1 p2 p3 d0 d1 d2 d3", real=True ) dq1 = DualQuaternion([p0, p1, p2, p3, d0, d1, d2, d3]) dq2 = DualQuaternion() # identity print(dq1 * dq2) print(dq1.norm()) rational_linkages.set_backend("numpy") """ # ------------------------------------------------------------------ # Construction # ------------------------------------------------------------------ def __init__(self, coeffs: Optional[Sequence] = None, rational: bool = False): from .QuaternionSymbolic import QuaternionSymbolic # rationalize the input if rational: # check if any element is not already a sympy object if not all(isinstance(c, sympy.Basic) for c in coeffs): coeffs = [sympy.Rational(v) for v in coeffs] self.is_rational = True if coeffs is not None: if len(coeffs) != 8: raise ValueError( "DualQuaternionSymbolic: input has to be 8-vector" ) params = [sympy.sympify(v) for v in coeffs] self.p = QuaternionSymbolic(params[:4]) self.d = QuaternionSymbolic(params[4:]) else: self.p = QuaternionSymbolic() # [1, 0, 0, 0] self.d = QuaternionSymbolic([ sympy.Integer(0), sympy.Integer(0), sympy.Integer(0), sympy.Integer(0), ])
[docs] @classmethod def random(cls, interval: int = 1, max_denominator: int = 4,) -> "DualQuaternionSymbolic": """ Create a random dual quaternion with rational coefficients. Parameters ---------- interval : SymPy number specifying the range of random coefficients. Each coefficient is sampled uniformly from the interval ``[-interval, interval]``. Default is 1. max_denominator : Maximum denominator for the random rational coefficients. Default is 4. Returns ------- DualQuaternionSymbolic """ from random import randint # inner import def random_rational(_interval=1, _max_den=4): """ Random SymPy Rational in the open interval (-interval, interval). Example for interval=1: 1/3, -8/9, 0, ... """ while True: den = randint(1, _max_den) num = randint(-_interval * den + 1, _interval * den - 1) # (-interval, interval) if num != 0: # exclude zero return sympy.Rational(num, den) return cls([random_rational(interval, max_denominator) for _ in range(8)])
# ------------------------------------------------------------------ # Representation # ------------------------------------------------------------------ def __repr__(self) -> str: entries = ", ".join(str(v) for v in self.array()) return f"DQ([{entries}])" # ------------------------------------------------------------------ # Arithmetic operators # ------------------------------------------------------------------ def __eq__(self, other: "DualQuaternion") -> bool: """ Coefficient-wise equality via symbolic simplification. Parameters ---------- other : DualQuaternion 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.array(), other.array()) ) def __mul__( self, other: "DualQuaternion | int | float" ) -> "DualQuaternionSymbolic": """ Multiply two dual quaternions, or scale by a scalar. Results are expanded via :func:`sympy.expand`. Parameters ---------- other : DualQuaternion, or a scalar ``int`` / ``float``. Returns ------- DualQuaternionSymbolic """ if isinstance(other, Number): s = sympy.sympify(other) return self.__class__([sympy.expand(v * s) for v in self.array()]) p = self.p * other.p d_new = self.d * other.p + self.p * other.d return self.__class__.from_two_quaternions(p, d_new) # ------------------------------------------------------------------ # Core operations # ------------------------------------------------------------------
[docs] def array(self) -> numpy.ndarray: """ Return the 8 Study parameters as an object-dtype numpy array. Returns ------- numpy.ndarray Object-dtype 8-vector of SymPy expressions. """ return numpy.array( list(self.p.array()) + list(self.d.array()), dtype=object )
[docs] def norm(self) -> "DualQuaternionSymbolic": """ Dual quaternion norm as a dual number, returned as a :class:`DualQuaternionSymbolic`. The primal norm ``p·p`` occupies index 0, the dual norm ``2(p·d)`` occupies index 4; all other entries are zero. Returns ------- DualQuaternionSymbolic """ primal_norm = sympy.expand(sum(v**2 for v in self.p.array())) dual_norm = sympy.expand( 2 * sum(a * b for a, b in zip(self.p.array(), self.d.array())) ) return self.__class__([ primal_norm, sympy.Integer(0), sympy.Integer(0), sympy.Integer(0), dual_norm, sympy.Integer(0), sympy.Integer(0), sympy.Integer(0), ])
[docs] def normalize(self) -> "DualQuaternionSymbolic": """ Normalize the dual quaternion so that the first Study parameter equals 1. Returns ------- DualQuaternionSymbolic Raises ------ ValueError If the first Study parameter simplifies to zero. """ p0 = sympy.simplify(self.array()[0]) if p0 == 0: raise ValueError( "DualQuaternionSymbolic: the first Study parameter is zero; " "cannot normalize." ) return self.__class__([sympy.simplify(v / p0) for v in self.array()])
[docs] def inv(self) -> "DualQuaternionSymbolic": """ Inverse of the symbolic dual quaternion. Computed as ``p_inv = p.inv()``; ``d_inv = -p_inv * d * p_inv``, with each component simplified via :func:`sympy.simplify`. Returns ------- DualQuaternionSymbolic """ p_inv = self.p.inv() d_inv = -1 * p_inv * self.d * p_inv # simplify all components from .QuaternionSymbolic import QuaternionSymbolic p_inv_s = QuaternionSymbolic([sympy.simplify(v) for v in p_inv.array()]) d_inv_s = QuaternionSymbolic([sympy.simplify(v) for v in d_inv.array()]) return self.__class__.from_two_quaternions(p_inv_s, d_inv_s)
[docs] def conjugate(self) -> "DualQuaternionSymbolic": """ Dual quaternion conjugate: conjugate both primal and dual parts. Returns ------- DualQuaternionSymbolic """ return self.__class__.from_two_quaternions( self.p.conjugate(), self.d.conjugate() )
[docs] def eps_conjugate(self) -> "DualQuaternionSymbolic": """ Epsilon (dual) conjugate: negate the dual part only. Returns ------- DualQuaternionSymbolic """ return self.__class__.from_two_quaternions(self.p, -self.d)
[docs] def extended_dot(self, other: "DualQuaternion") -> sympy.Expr: """ Extended scalar (dot) product of two dual quaternions. Defined as ``self.p · other.d + self.d · other.p``. Parameters ---------- other : Second DualQuaternion. Returns ------- sympy.Expr """ return sympy.expand( sum(a * b for a, b in zip(self.p.array(), other.d.array())) + sum(a * b for a, b in zip(self.d.array(), other.p.array())) )
[docs] def is_on_study_quadric(self, approximate: bool = False) -> bool: """ Check whether the symbolic dual quaternion satisfies the Study condition ``p · d == 0`` after simplification. Parameters ---------- approximate : Ignored for symbolic instances (simplification is always exact). Returns ------- bool """ condition = self.study_condition() return sympy.simplify(condition) == 0
[docs] def study_condition(self): """ Return the Study condition ``p · d`` as a SymPy expression. Returns ------- sympy.Expr """ condition = sympy.expand( sum(a * b for a, b in zip(self.p.array(), self.d.array())) ) return sympy.simplify(condition)
[docs] def back_projection(self) -> "DualQuaternionSymbolic": """ Project the symbolic dual quaternion onto the Study quadric. Returns ------- DualQuaternionSymbolic """ if self.is_on_study_quadric(): return self from .QuaternionSymbolic import QuaternionSymbolic primal = self.p dual = self.d primal_2norm = sympy.expand(2 * sum(v**2 for v in primal.array())) new_primal = QuaternionSymbolic([ primal_2norm, sympy.Integer(0), sympy.Integer(0), sympy.Integer(0) ]) new_dual = -1 * (primal * dual.conjugate() - dual * primal.conjugate()) zero_q = QuaternionSymbolic([ sympy.Integer(0), sympy.Integer(0), sympy.Integer(0), sympy.Integer(0) ]) dq = ( self.__class__.from_two_quaternions(new_primal, new_dual) * self.__class__.from_two_quaternions(primal, zero_q) ) # divide by 2 return self.__class__([sympy.expand(v / 2) for v in dq.array()])
[docs] def dq2matrix(self, normalize: bool = True) -> numpy.ndarray: """ Convert to a 4×4 SE(3) transformation matrix with SymPy entries. Parameters ---------- normalize : If ``True`` (default), divide each entry by the top-left element. Returns ------- numpy.ndarray Object-dtype 4×4 array of SymPy expressions. """ p0, p1, p2, p3 = self.p.array() d0, d1, d2, d3 = self.d.array() def ex(expr): return sympy.expand(expr) r11 = ex(p0**2 + p1**2 - p2**2 - p3**2) r22 = ex(p0**2 - p1**2 + p2**2 - p3**2) r33 = ex(p0**2 - p1**2 - p2**2 + p3**2) r44 = ex(p0**2 + p1**2 + p2**2 + p3**2) r12 = ex(2 * (p1 * p2 - p0 * p3)) r13 = ex(2 * (p1 * p3 + p0 * p2)) r21 = ex(2 * (p1 * p2 + p0 * p3)) r23 = ex(2 * (p2 * p3 - p0 * p1)) r31 = ex(2 * (p1 * p3 - p0 * p2)) r32 = ex(2 * (p2 * p3 + p0 * p1)) r14 = ex(2 * (-p0 * d1 + p1 * d0 - p2 * d3 + p3 * d2)) r24 = ex(2 * (-p0 * d2 + p1 * d3 + p2 * d0 - p3 * d1)) r34 = ex(2 * (-p0 * d3 - p1 * d2 + p2 * d1 + p3 * d0)) mat = numpy.array([ [r44, sympy.Integer(0), sympy.Integer(0), sympy.Integer(0)], [r14, r11, r12, r13], [r24, r21, r22, r23], [r34, r31, r32, r33], ], dtype=object) if normalize: mat = numpy.array( [[sympy.simplify(v / r44) for v in row] for row in mat], dtype=object, ) return mat
[docs] def eval(self, subs: dict) -> "DualQuaternionSymbolic": """ Evaluate the symbolic dual quaternion by substituting symbols. Parameters ---------- subs : dict Mapping of SymPy symbols to values. Returns ------- DualQuaternionSymbolic New symbolic dual quaternion with substitutions applied. Examples -------- .. code-block:: python import rational_linkages rational_linkages.set_backend("sympy") from rational_linkages import DualQuaternion from sympy import symbols t = symbols("t") dq = DualQuaternion([1, t, 0, 0, 0, t, 0, 0]) result = dq.eval({t: 2}) print(result) rational_linkages.set_backend("numpy") """ return self.__class__([v.subs(subs) for v in self.array()])
[docs] def evalf(self): """ Replace rational numbers by numerical ones. Returns ------- DualQuaternion Evaluated numerically. """ return DualQuaternion( numpy.array([val.evalf() for val in self.coordinates], dtype=numpy.float64))