Symbolic Backend

The package supports two computation backends:

  • NumPy (default) — fast floating-point arithmetic based on float64 arrays.

  • SymPy — exact algebraic computation with symbolic expressions, rational numbers, and parametric quantities, which is more suitable for scientific exploration and symbolic manipulation.

Switching the backend to "sympy" is done once, before constructing any objects, by calling set_backend().

After this call every factory class (e.g. DualQuaternion, Quaternion, NormalizedLine, …) transparently returns its symbolic counterpart.

If you have a numerical value that is close to a rational number, use sympy’s sympy.nsimplify() to convert it to an exact rational. Otherwise, use sympy’s sympy.Rational() to construct rational.

Example - DualQuaternionSymbolic

When the "sympy" backend is active, DualQuaternion (via its __new__ factory) returns a DualQuaternionSymbolic instance. The two classes share the same public API, so all code that works numerically also works symbolically. Symbolic classes have some additional methods such as substitution of symbolic variables.

Basic construction

import rational_linkages
rational_linkages.set_backend("sympy")

from rational_linkages import DualQuaternion
from sympy import symbols, Rational

# Declare eight real-valued symbols for the eight Study parameters.
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)
# DQ([p0, p1, p2, p3, d0, d1, d2, d3])

# substitute inner parameters
dq_eval = dq.eval({p0: 1, p1: 0, p2: Rational(1,3), p3: 0, d0: 0, d1: 0, d2: 0, d3: 0})
print(dq_eval)

# evaluate numerically
dq_eval_num = dq_eval.evalf()
print(dq_eval_num)

Automatic promotion when SymPy values are passed

You do not have to call set_backend() explicitly if you already have SymPy objects. Passing any coefficient that carries free_symbols (i.e. a SymPy expression or Rational) is enough — the constructor detects this and promotes the result to DualQuaternionSymbolic automatically:

from rational_linkages import DualQuaternion
from sympy import Rational, symbols

dq_numeric = DualQuaternion()
print(type(dq_numeric).__name__)  # DualQuaternion (numeric)

# Rational coefficients: no set_backend() call needed.
dq_rational = DualQuaternion([Rational(1, 2), 0, 0, 0, 0, 0, 0, 0])
print(type(dq_rational).__name__)  # DualQuaternionSymbolic

# Symbolic coefficients: again promoted automatically.
t = symbols("t")
dq_sym = DualQuaternion([1, t, 0, 0, 0, t**2, 0, 0])
print(type(dq_sym).__name__)  # DualQuaternionSymbolic

Verifying the Study condition

A dual quaternion represents a valid rigid-body displacement if and only if it satisfies the Study condition \(\mathbf{p} \cdot \mathbf{d} = 0\). The symbolic version uses SymPy simplification to check this exactly:

from rational_linkages import DualQuaternion, set_backend
from sympy import symbols


set_backend("sympy")


p0, p1, p2, p3 = symbols("p0 p1 p2 p3", real=True)

# Pure-rotation dual quaternion: dual part is zero, so p*d = 0 trivially.
dq_rot = DualQuaternion([p0, p1, p2, p3, 0, 0, 0, 0])
print(dq_rot.is_on_study_quadric())  # True

# Back-project an arbitrary dual quaternion onto the Study quadric.
p0, p1, p2, p3, d0, d1, d2, d3 = symbols("p0 p1 p2 p3 d0 d1 d2 d3", real=True)
dq_arb = DualQuaternion([p0, p1, p2, p3, d0, d1, d2, d3])
dq_proj = dq_arb.back_projection()
print(dq_proj.is_on_study_quadric())  # True


Conversion to transformation matrix

Please handle matrices carefully, as they are more as a helper than core objects. The Rational Linkages implements them with a convention that is less usual - the first row are projective coordinate, translation vector is in the first column, and the rotation part is in the lower right 3x3 block. More details can be found in Correspondence between Dual Quaternions and Transformation Matrices.

DualQuaternion.dq2matrix() returns a 4x4 SE(3) homogeneous transformation matrix (as numpy array). In the symbolic backend every entry is a SymPy expression:

import rational_linkages
rational_linkages.set_backend("sympy")

from rational_linkages import DualQuaternion, TransfMatrix
from sympy import pprint, symbols

# Pure translation along x by distance 'a'.
a = symbols("a", positive=True)
dq_trans = DualQuaternion([1, 0, 0, 0, 0, -a/2, 0, 0])

mat = TransfMatrix(dq_trans.dq2matrix())
pprint(mat)
# [[1, 0, 0, 0],
#  [a, 1, 0, 0],
#  [0, 0, 1, 0],
#  [0, 0, 0, 1]]
# note the convention with projective coordinates on the top row
# and the translation in the first column