Source code for rational_linkages.DualQuaternion

from typing import Optional, Sequence
from warnings import warn

import numpy

from .Quaternion import Quaternion
from .backend import is_symbolic


[docs] class DualQuaternion: """ Dual quaternion representing a rigid body displacement in 3D space. A dual quaternion consists of a primal part ``p`` (rotation) and a dual part ``d`` (translation), stored as two :class:`.Quaternion` instances. The 8 Study parameters ``[p0, p1, p2, p3, d0, d1, d2, d3]`` are the concatenation of the two quaternion coefficient vectors. 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:`.DualQuaternionSymbolic` instance instead. Parameters ---------- coeffs : 8-vector of Study parameters ``[p0, p1, p2, p3, d0, d1, d2, d3]``. If ``None``, the identity dual quaternion ``[1, 0, 0, 0, 0, 0, 0, 0]`` is constructed. Attributes ---------- p : Quaternion Primal part ``[p0, p1, p2, p3]``, representing rotation. d : Quaternion Dual part ``[d0, d1, d2, d3]``, representing translation. Raises ------ ValueError If ``coeffs`` is not an 8-vector. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([1, 2, 3, 4, 0.1, 0.2, 0.3, 0.4]) identity = DualQuaternion() print(dq) print(identity) .. clear-namespace:: .. code-block:: python from rational_linkages import DualQuaternion, Quaternion q1 = Quaternion([0.5, 0.5, 0.5, 0.5]) q2 = Quaternion([1, 2, 3, 4]) dq = DualQuaternion.from_two_quaternions(q1, q2) print(dq) .. clear-namespace:: .. code-block:: python # Symbolic backend 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) dq = DualQuaternion([p0, p1, p2, p3, d0, d1, d2, d3]) print(dq) rational_linkages.set_backend("numpy") .. clear-namespace:: """ __hash__ = None __array_priority__ = 20.0 # ------------------------------------------------------------------ # Factory # ------------------------------------------------------------------ def __new__(cls, coeffs=None, rational: bool = False): """ Intercept construction and return a :class:`.DualQuaternionSymbolic` when the global backend is ``"sympy"``. Only applied when ``cls`` is exactly ``DualQuaternion``; subclass constructors are never redirected. Parameters ---------- coeffs : Forwarded unchanged to ``__init__``. rational : Keeps or forces rational values by using symbolic backend Returns ------- DualQuaternion or DualQuaternionSymbolic """ if cls is DualQuaternion: symbolic = is_symbolic() or rational or ( coeffs is not None and any(hasattr(c, 'free_symbols') for c in coeffs) ) if symbolic: from .DualQuaternionSymbolic import DualQuaternionSymbolic return object.__new__(DualQuaternionSymbolic) return object.__new__(cls) # ------------------------------------------------------------------ # Construction # ------------------------------------------------------------------ def __init__(self, coeffs: Optional[Sequence[float]] = None, rational: bool = False): self.is_rational = rational if coeffs is not None: if len(coeffs) != 8: raise ValueError("DualQuaternion: input has to be 8-vector") coeffs = numpy.asarray(coeffs) import sympy as _sympy if any(isinstance(v, _sympy.Basic) for v in coeffs): from .QuaternionSymbolic import QuaternionSymbolic self.p = QuaternionSymbolic(coeffs[:4]) self.d = QuaternionSymbolic(coeffs[4:]) else: try: coeffs = coeffs.astype(numpy.float64) except (TypeError, ValueError): coeffs = coeffs.astype(object) self.p = Quaternion(coeffs[:4]) self.d = Quaternion(coeffs[4:]) else: self.p = Quaternion([1.0, 0.0, 0.0, 0.0]) self.d = Quaternion([0.0, 0.0, 0.0, 0.0]) # ------------------------------------------------------------------ # Class methods # ------------------------------------------------------------------
[docs] @classmethod def from_two_quaternions( cls, primal: Quaternion, dual: Quaternion ) -> "DualQuaternion": """ Construct a DualQuaternion from primal and dual Quaternions. Parameters ---------- primal : Primal (rotation) quaternion. dual : Dual (translation) quaternion. Returns ------- DualQuaternion Examples -------- .. code-block:: python from rational_linkages import DualQuaternion, Quaternion p = Quaternion([1, 0, 0, 0]) d = Quaternion([0, 1, 0, 0]) dq = DualQuaternion.from_two_quaternions(p, d) print(dq) .. clear-namespace:: """ return cls(numpy.concatenate((primal.array(), dual.array())))
[docs] @classmethod def random(cls, interval: float = 1.0) -> "DualQuaternion": """ Construct a random DualQuaternion with coefficients in ``(-interval, interval)``. Parameters ---------- interval : Half-width of the uniform sampling range. Default ``1.0``. Returns ------- DualQuaternion Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion.random() print(dq) .. clear-namespace:: """ if cls is DualQuaternion and is_symbolic(): from .DualQuaternionSymbolic import DualQuaternionSymbolic return DualQuaternionSymbolic.random(interval) return cls(numpy.random.uniform(-interval, interval, 8))
[docs] @classmethod def random_on_study_quadric(cls, interval: float = 1.0) -> "DualQuaternion": """ Construct a random DualQuaternion projected onto the Study quadric. Parameters ---------- interval : Half-width of the uniform sampling range. Default ``1.0``. Returns ------- DualQuaternion """ return cls.random(interval).back_projection()
[docs] @classmethod def random_integers( cls, low: int = -1, high: int = 2, study_condition: bool = True, ) -> "DualQuaternion": """ Construct a random DualQuaternion with integer elements. Parameters ---------- low : Lower bound (inclusive) for random integers. Default ``-1``. high : Upper bound (exclusive) for random integers. Default ``2``. study_condition : If ``True``, project the result onto the Study quadric. Default ``True``. Returns ------- DualQuaternion """ params = numpy.random.randint(low, high, 8).astype(float) dq = cls(params) if study_condition: dq = dq.back_projection() return dq
[docs] @classmethod def as_rational( cls, coeffs: Optional[Sequence] = None ) -> "DualQuaternion": """ Construct a DualQuaternion from SymPy ``Rational`` numbers. .. deprecated:: ``as_rational`` is deprecated and will be removed in a future version. Pass SymPy ``Rational`` values directly to the ``DualQuaternion`` constructor instead:: from sympy import Rational dq = DualQuaternion([Rational(1, 2), 0, 0, 0, 0, 0, 0, 0]) Each element of ``coeffs`` is converted to a :class:`sympy.Rational`. Elements may be plain numbers (converted via ``Rational(x)``), or 2-tuples ``(p, q)`` (converted via ``Rational(p, q)``). If ``None``, the rational identity dual quaternion is returned. Parameters ---------- coeffs : List or array of 8 numbers or ``(numerator, denominator)`` tuples. If ``None``, the identity is constructed. Returns ------- DualQuaternion Dual quaternion whose coefficients are SymPy ``Rational`` objects. """ warn( "DualQuaternion.as_rational() is deprecated and will be removed in a " "future version. Pass sympy.Rational values directly to the " "DualQuaternion constructor instead.", DeprecationWarning, stacklevel=2, ) from sympy import Rational, Expr if coeffs is not None: rational_params = [ x if isinstance(x, Expr) else Rational(*x) if isinstance(x, tuple) else Rational(x) for x in coeffs ] else: rational_params = [ Rational(1), Rational(0), Rational(0), Rational(0), Rational(0), Rational(0), Rational(0), Rational(0), ] return cls(rational_params)
[docs] @classmethod def from_bq_biquaternion(cls, biquaternion) -> "DualQuaternion": """ Construct a DualQuaternion from a ``biquaternion_py`` BiQuaternion. Parameters ---------- biquaternion : A ``biquaternion_py.biquaternion.BiQuaternion`` instance. Returns ------- DualQuaternion Raises ------ ValueError If ``biquaternion`` is not a ``BiQuaternion`` instance. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion from biquaternion_py import BiQuaternion bq = BiQuaternion(1, 0, 0, 0, 0, 2, 3, 4) dq = DualQuaternion.from_bq_biquaternion(bq) print(dq) .. clear-namespace:: """ from biquaternion_py import BiQuaternion # lazy import if not isinstance(biquaternion, BiQuaternion): raise ValueError( "Input must be a biquaternion_py.biquaternion.BiQuaternion object." ) return cls(numpy.array(biquaternion.coeffs, dtype="float64"))
[docs] @classmethod def from_bq_poly(cls, poly, indet) -> "DualQuaternion": """ Construct a DualQuaternion from a degree-1 ``biquaternion_py`` polynomial. The polynomial is expected to be of the form ``(t - h)``, where ``t`` is the indeterminate and ``h`` is a BiQuaternion. The DualQuaternion is assembled from the negated constant coefficients of the polynomial. Parameters ---------- poly : A ``biquaternion_py.polynomials.Poly`` instance of degree 1. indet : The SymPy symbol used as indeterminate of the polynomial. Returns ------- DualQuaternion Raises ------ ValueError If ``poly`` is not a ``Poly`` instance or is not of degree 1. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion from biquaternion_py import Poly, KK, EE, II from sympy import Symbol t = Symbol("t") h = 2 * KK + EE * II dq = DualQuaternion.from_bq_poly(Poly(t - h, t), indet=t) print(dq) .. clear-namespace:: """ from biquaternion_py import Poly # lazy import if not isinstance(poly, Poly): raise ValueError( "Input must be a biquaternion_py.polynomials.Poly object." ) if poly.deg(indet) != 1: raise ValueError("Polynomial must be of degree 1.") poly_coeffs = [-x for x in poly.coeff(indet, 0).coeffs] return cls(numpy.array(poly_coeffs, dtype="float64"))
# ------------------------------------------------------------------ # Representation # ------------------------------------------------------------------ def __repr__(self) -> str: arr = numpy.array2string( self.array(), precision=16, suppress_small=True, separator=", ", max_line_width=100000, ) return f"{self.__class__.__qualname__}({arr})" # ------------------------------------------------------------------ # Indexing # ------------------------------------------------------------------ def __getitem__(self, idx): """ Get a Study parameter by index (0..7). Parameters ---------- idx : Index in range ``0..7``. Returns ------- float """ return self.array()[idx] def __setitem__(self, idx, value): """ Set a Study parameter by index (0..7). Parameters ---------- idx : Index in range ``0..7``. value : New value. """ if idx < 4: self.p[idx] = value else: self.d[idx - 4] = value def __len__(self) -> int: """Length of the dual quaternion, always 8.""" return 8 def __iter__(self): """Iterate over Study parameters.""" return iter(self.array()) # ------------------------------------------------------------------ # Arithmetic operators # ------------------------------------------------------------------ def __eq__(self, other: "DualQuaternion") -> bool: """ Test coefficient-wise equality. Parameters ---------- other : DualQuaternion to compare against. Returns ------- bool """ return numpy.array_equal(self.array(), other.array()) def __add__(self, other: "DualQuaternion") -> "DualQuaternion": """ Add two dual quaternions. Parameters ---------- other : DualQuaternion to add. Returns ------- DualQuaternion """ return self.__class__.from_two_quaternions(self.p + other.p, self.d + other.d) def __sub__(self, other: "DualQuaternion") -> "DualQuaternion": """ Subtract two dual quaternions. Parameters ---------- other : DualQuaternion to subtract. Returns ------- DualQuaternion """ return self.__class__.from_two_quaternions(self.p - other.p, self.d - other.d) def __mul__( self, other: "DualQuaternion | int | float" ) -> "DualQuaternion": """ Multiply two dual quaternions, or scale by a scalar. For two dual quaternions the product is defined as: - primal: ``self.p * other.p`` - dual: ``self.d * other.p + self.p * other.d`` Parameters ---------- other : DualQuaternion, or a scalar ``int`` / ``float``. Returns ------- DualQuaternion """ if isinstance(other, DualQuaternion): p = self.p * other.p d = self.d * other.p + self.p * other.d return self.__class__.from_two_quaternions(p, d) else: return self.__class__(self.array() * other) def __rmul__(self, other: "DualQuaternion | int | float") -> "DualQuaternion": """Scalar-on-left multiplication, delegates to ``__mul__``.""" from numbers import Number if isinstance(other, Number): return self.__mul__(other) return NotImplemented def __truediv__( self, other: "DualQuaternion | int | float" ) -> "DualQuaternion": """ Divide by a dual quaternion or scalar. Dividing by a dual quaternion is equivalent to multiplying by its inverse and emits a warning. Parameters ---------- other : DualQuaternion, or a scalar ``int`` / ``float``. Returns ------- DualQuaternion """ if isinstance(other, DualQuaternion): warn( "DualQuaternion divided by DualQuaternion: " "computing self * other.inv()." ) return self * other.inv() return self.__class__(self.array() / other) def __neg__(self) -> "DualQuaternion": """ Negate the dual quaternion. Returns ------- DualQuaternion """ return self.__class__(-self.array()) # ------------------------------------------------------------------ # Properties # ------------------------------------------------------------------ @property def real(self) -> numpy.ndarray: """ Real scalars of the dual quaternion ``[p0, d0]``. Returns ------- numpy.ndarray 2-vector. """ arr = self.array() return numpy.array([arr[0], arr[4]]) @property def imag(self) -> numpy.ndarray: """ Imaginary parts of the dual quaternion ``[p1, p2, p3, d1, d2, d3]``. Returns ------- numpy.ndarray 6-vector (e.g. Plücker coordinates when the DQ represents a line). """ arr = self.array() return numpy.array([arr[1], arr[2], arr[3], arr[5], arr[6], arr[7]]) @property def coordinates(self): """ Return the coordinates of the dual quaternion. Returns ------- numpy.ndarray 8-vector of dual quaternion coordinates. """ return self.array() # ------------------------------------------------------------------ # Core operations # ------------------------------------------------------------------
[docs] def array(self) -> numpy.ndarray: """ Return the 8 Study parameters as a numpy array. Returns ------- numpy.ndarray 8-vector ``[p0, p1, p2, p3, d0, d1, d2, d3]``. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([1, 2, 3, 4, 5, 6, 7, 8]) print(dq.array()) .. clear-namespace:: """ return numpy.concatenate((self.p.array(), self.d.array()))
[docs] def conjugate(self) -> "DualQuaternion": """ Dual quaternion conjugate, both primal and dual parts. Returns ------- DualQuaternion ``[p.conjugate(), d.conjugate()]``. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([1, 2, 3, 4, 5, 6, 7, 8]) print(dq.conjugate()) .. clear-namespace:: """ return self.__class__.from_two_quaternions( self.p.conjugate(), self.d.conjugate() )
[docs] def eps_conjugate(self) -> "DualQuaternion": """ Epsilon (dual) conjugate, negate the dual part only. Returns ------- DualQuaternion ``[p, -d]``. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([1, 2, 3, 4, 5, 6, 7, 8]) print(dq.eps_conjugate()) .. clear-namespace:: """ return self.__class__.from_two_quaternions(self.p, -self.d)
[docs] def norm(self) -> "DualQuaternion": """ Dual quaternion norm as a dual number, returned as a DualQuaternion. The primal norm ``p·p`` occupies index 0, the dual norm ``2(p·d)`` occupies index 4; all other entries are zero. Returns ------- DualQuaternion Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([1, 0, 0, 0, 0, 1, 2, 3]) print(dq.norm()) .. clear-namespace:: """ p_arr = self.p.array() d_arr = self.d.array() primal_norm = numpy.dot(p_arr, p_arr) dual_norm = 2.0 * numpy.dot(p_arr, d_arr) return self.__class__( numpy.array([primal_norm, 0.0, 0.0, 0.0, dual_norm, 0.0, 0.0, 0.0]) )
[docs] def normalize(self) -> "DualQuaternion": """ Normalize the dual quaternion so that ``p0 == 1``. Returns ------- DualQuaternion Raises ------ ValueError If the first Study parameter is zero. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([2, 0, 0, 0, 0, 2, 4, 6]) print(dq.normalize()) .. clear-namespace:: """ p0 = self.array()[0] if numpy.isclose(p0, 0.0): raise ValueError( "DualQuaternion: the first Study parameter is zero; " "cannot normalize." ) return self.__class__(self.array() / p0)
[docs] def inv(self) -> "DualQuaternion": """ Inverse of the dual quaternion. Computed as ``p_inv = p.inv()``; ``d_inv = -p_inv * d * p_inv``. Returns ------- DualQuaternion Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([1, 0, 0, 0, 0, 1, 0, 0]) print(dq * dq.inv()) .. clear-namespace:: """ p_inv = self.p.inv() d_inv = -1 * p_inv * self.d * p_inv return self.__class__.from_two_quaternions(p_inv, d_inv)
[docs] def back_projection(self) -> "DualQuaternion": """ Project the dual quaternion onto the Study quadric (fiber projection). If the dual quaternion already lies on the Study quadric the instance is returned unchanged. Returns ------- DualQuaternion Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([1, 2, 3, 4, 5, 6, 7, 8]) dq_on_quadric = dq.back_projection() print(dq_on_quadric.is_on_study_quadric()) .. clear-namespace:: """ if self.is_on_study_quadric(): return self primal = self.p dual = self.d primal_2norm = 2.0 * primal.norm() new_primal = Quaternion([primal_2norm, 0.0, 0.0, 0.0]) new_dual = -1 * (primal * dual.conjugate() - dual * primal.conjugate()) zero_q = Quaternion([0.0, 0.0, 0.0, 0.0]) dq = ( self.__class__.from_two_quaternions(new_primal, new_dual) * self.__class__.from_two_quaternions(primal, zero_q) ) / 2.0 return dq
[docs] def extended_dot(self, other: "DualQuaternion") -> float: """ Extended scalar (dot) product of two dual quaternions. Defined as ``self.p · other.d + self.d · other.p``. Parameters ---------- other : Second DualQuaternion. Returns ------- float Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq1 = DualQuaternion([1, 0, 0, 0, 0, 1, 0, 0]) dq2 = DualQuaternion([1, 0, 0, 0, 0, 0, 1, 0]) print(dq1.extended_dot(dq2)) .. clear-namespace:: """ return float( numpy.dot(self.p.array(), other.d.array()) + numpy.dot(self.d.array(), other.p.array()) )
[docs] def is_on_study_quadric(self, approximate: bool = False) -> bool: """ Check whether the dual quaternion lies on the Study quadric. The Study condition is ``p · d == 0`` (dot product of primal and dual coefficient vectors). Parameters ---------- approximate : If ``True``, use a looser threshold of ``1e-10`` instead of the default strict ``1e-20``. Returns ------- bool Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([1, 0, 0, 0, 0, 0, 0, 0]) print(dq.is_on_study_quadric()) .. clear-namespace:: """ threshold = 1e-10 if approximate else 1e-20 condition = self.study_condition() return numpy.isclose(condition, 0.0, atol=threshold)
[docs] def study_condition(self) -> float: """ Compute the Study condition value ``p · d``. Returns ------- float The value of the Study condition, which should be zero for valid rigid body displacements. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([1, 0, 0, 0, 0, 0, 0, 0]) print(dq.study_condition()) """ return float(numpy.dot(self.p.array(), self.d.array()))
[docs] def dq2matrix(self, normalize: bool = True) -> numpy.ndarray: """ Convert to a 4×4 SE(3) homogeneous transformation matrix. Parameters ---------- normalize : If ``True`` (default), divide by the top-left entry so that the rotation block is properly scaled. Returns ------- numpy.ndarray 4×4 transformation matrix. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([1, 0, 0, 0, 0, 1, 2, 3]) print(dq.dq2matrix()) .. clear-namespace:: """ p0, p1, p2, p3 = self.p.array() d0, d1, d2, d3 = self.d.array() r11 = p0**2 + p1**2 - p2**2 - p3**2 r22 = p0**2 - p1**2 + p2**2 - p3**2 r33 = p0**2 - p1**2 - p2**2 + p3**2 r44 = p0**2 + p1**2 + p2**2 + p3**2 r12 = 2 * (p1 * p2 - p0 * p3) r13 = 2 * (p1 * p3 + p0 * p2) r21 = 2 * (p1 * p2 + p0 * p3) r23 = 2 * (p2 * p3 - p0 * p1) r31 = 2 * (p1 * p3 - p0 * p2) r32 = 2 * (p2 * p3 + p0 * p1) r14 = 2 * (-p0 * d1 + p1 * d0 - p2 * d3 + p3 * d2) r24 = 2 * (-p0 * d2 + p1 * d3 + p2 * d0 - p3 * d1) r34 = 2 * (-p0 * d3 - p1 * d2 + p2 * d1 + p3 * d0) mat = numpy.array([ [r44, 0, 0, 0 ], [r14, r11, r12, r13], [r24, r21, r22, r23], [r34, r31, r32, r33], ]) return mat / mat[0, 0] if normalize else mat
[docs] def dq2point(self) -> numpy.ndarray: """ Extract the translation 3-vector directly from Study parameters. Normalizes by ``p0`` first, then reads indices 5-7. Returns ------- numpy.ndarray 3-vector ``[tx, ty, tz]``. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([2, 0, 0, 0, 0, 2, 4, 6]) print(dq.dq2point()) .. clear-namespace:: """ arr = self.array() / self.array()[0] return arr[5:8]
[docs] def dq2point_homogeneous(self) -> numpy.ndarray: """ Extract the homogeneous point ``[p0, d1, d2, d3]``. Returns ------- numpy.ndarray 4-vector. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([1, 0, 0, 0, 0, 1, 2, 3]) print(dq.dq2point_homogeneous()) .. clear-namespace:: """ arr = self.array() return numpy.array([arr[0], arr[5], arr[6], arr[7]])
[docs] def dq2point_via_matrix(self) -> numpy.ndarray: """ Extract the translation 3-vector via the SE(3) matrix. Returns ------- numpy.ndarray 3-vector ``[tx, ty, tz]`` (column 0, rows 1-3 of ``dq2matrix()``). Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([1, 0, 0, 0, 0, 1, 2, 3]) print(dq.dq2point_via_matrix()) .. clear-namespace:: """ return self.dq2matrix()[1:4, 0]
[docs] def dq2line_vectors(self) -> tuple: """ Convert the dual quaternion to Plücker line coordinates. Returns the direction vector and moment vector of the represented line. Both vectors are normalized. Returns ------- tuple[numpy.ndarray, numpy.ndarray] ``(direction, moment)`` each a 3-vector. Raises ------ ValueError If the dual quaternion contains more than one free symbol. Warns ----- UserWarning If the dual quaternion does not appear to represent a line (non-zero first or fifth element). Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([0, 0, 0, 1, 0, 0, -2, 0]) direction, moment = dq.dq2line_vectors() print(direction, moment) .. clear-namespace:: """ arr = self.array() # try to cast symbolic arrays to numeric try: arr = arr.astype(numpy.float64) except (TypeError, ValueError): pass if arr.dtype == object: # symbolic path import sympy dq0 = sympy.simplify(arr[0]) dq4 = sympy.simplify(arr[4]) free = (dq0 + dq4).free_symbols if len(free) > 1: raise ValueError( "dq2line_vectors: dual quaternion has more than one free symbol." ) if dq0 != 0 or dq4 != 0: warn( "dq2line_vectors: dual quaternion has non-zero first or fifth " "element; it may not represent a line." ) return arr[1:4], -arr[5:8] # numeric path k = arr[0]**2 - arr[1]**2 - arr[2]**2 - arr[3]**2 f = k - arr[0]**2 g = arr[0] * arr[4] dir_vec = f * arr[1:4] mom_vec = numpy.array([ g * arr[1] - f * arr[5], g * arr[2] - f * arr[6], g * arr[3] - f * arr[7], ]) norm_dir = numpy.linalg.norm(dir_vec) direction = -dir_vec / norm_dir moment = -mom_vec / norm_dir return direction, moment
[docs] def dq2screw(self) -> numpy.ndarray: """ Convert to 6D screw coordinates ``[direction | moment]``. Returns ------- numpy.ndarray 6-vector. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([0, 0, 0, 1, 0, 0, -2, 0]) print(dq.dq2screw()) .. clear-namespace:: """ direction, moment = self.dq2line_vectors() return numpy.concatenate((direction, moment))
[docs] def dq2point_via_line(self) -> numpy.ndarray: """ Recover a point on the line via ``direction × moment``. Returns ------- numpy.ndarray 3-vector. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([0, 0, 0, 1, 0, 0, -2, 0]) print(dq.dq2point_via_line()) .. clear-namespace:: """ direction, moment = self.dq2line_vectors() return numpy.cross(direction, moment)
[docs] def as_12d_vector(self) -> numpy.ndarray: """ Return the dual quaternion as a 12D vector from the SE(3) matrix. Columns 0-3 of rows 1-3 of ``dq2matrix()`` are stacked horizontally. Returns ------- numpy.ndarray 12-vector. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion dq = DualQuaternion([1, 0, 0, 0, 0, 1, 2, 3]) print(dq.as_12d_vector()) .. clear-namespace:: """ mat = self.dq2matrix() return numpy.hstack((mat[1:4, 0], mat[1:4, 1], mat[1:4, 2], mat[1:4, 3]))
[docs] def eval(self, subs: dict) -> "DualQuaternion": """ Evaluate the dual quaternion by substituting symbols with values. Parameters ---------- subs : dict Mapping of SymPy symbols to numeric values. Returns ------- DualQuaternion New numeric 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") .. clear-namespace:: """ evaluated = [ float(v.subs(subs)) if hasattr(v, "subs") else float(v) for v in self.array() ] return DualQuaternion(evaluated)
[docs] def evalf(self) -> "DualQuaternion": """ Placeholder for DualQuaternionSymbolic.evalf() method. Returns ------- DualQuaternion Evaluated quaternion. """ return self
[docs] def act(self, affected_object) -> object: """ Act on a geometric object (line or point) with this dual quaternion. The action is a half-turn about the dual quaternion's axis. If ``affected_object`` is itself a ``DualQuaternion`` (treated as a rotation-axis dual quaternion), it is first converted to a ``NormalizedLine`` and the action is performed on that. Parameters ---------- affected_object : A ``NormalizedLine``, ``PointHomogeneous``, or ``DualQuaternion`` to act upon. Returns ------- NormalizedLine or PointHomogeneous The transformed geometric object. Examples -------- .. code-block:: python from rational_linkages import DualQuaternion, NormalizedLine from rational_linkages.dualQuaternionAction import act dq = DualQuaternion([1, 0, 0, 1, 0, 3, 2, -1]) line = NormalizedLine.from_direction_and_point([0, 0, 1], [0, -2, 0]) line_transformed = dq.act(line) .. clear-namespace:: """ from .dualQuaternionAction import act as _act return _act(self, affected_object)