Source code for rational_linkages.NormalizedLineSymbolic

from typing import Optional, Sequence, Union
from warnings import warn

import numpy
import sympy

from .NormalizedLine import NormalizedLine


PointHomogeneous = "PointHomogeneous"


[docs] class NormalizedLineSymbolic(NormalizedLine): """ Symbolic Plücker line backed by SymPy expressions. Subclass of :class:`.NormalizedLine` for algebraic computation. Typically, not instantiated directly — when the global backend is set to ``"sympy"`` via :func:`.set_backend`, :class:`.NormalizedLine` transparently returns instances of this class via its ``__new__`` factory. Parameters ---------- unit_screw 6-vector of Plücker coordinates ``[l1, l2, l3, m1, m2, m3]`` as SymPy expressions or plain numbers. If ``None``, the Z-axis through the origin is constructed. Attributes ---------- direction Object-dtype array of SymPy expressions ``[l1, l2, l3]``. moment Object-dtype array of SymPy expressions ``[m1, m2, m3]``. screw Object-dtype 6-vector ``[direction | moment]``. Examples -------- .. code-block:: python import rational_linkages rational_linkages.set_backend("sympy") from rational_linkages import NormalizedLine from sympy import symbols l1, l2, l3, m1, m2, m3 = symbols("l1 l2 l3 m1 m2 m3", real=True) line = NormalizedLine([l1, l2, l3, m1, m2, m3]) print(line.direction) # [l1, l2, l3] rational_linkages.set_backend("numpy") .. clear-namespace:: """ # ------------------------------------------------------------------ # Construction # ------------------------------------------------------------------ def __init__(self, unit_screw: Optional[Sequence] = None): # Bypass NormalizedLine.__init__ (which assumes float64) and redo # initialization with symbolic-aware helpers. self.direction, self.moment = self._initialize_components(unit_screw) self.screw = numpy.concatenate((self.direction, self.moment))
[docs] @classmethod def from_direction_and_point(cls, direction, point) -> "NormalizedLine": """ Construct a ``NormalizedLine`` from the given ``direction`` and ``point``. Parameters ---------- direction 3-vector of SymPy expressions or plain numbers representing the line's direction. Should be unit-length for correct behavior, but this is not enforced. point 3-vector of SymPy expressions or plain numbers representing a point on the line. Returns ------- NormalizedLine """ direction = numpy.array([sympy.sympify(v) for v in direction], dtype=object) point = numpy.array([sympy.sympify(v) for v in point], dtype=object) moment = numpy.array([ -direction[1] * point[2] + direction[2] * point[1], -direction[2] * point[0] + direction[0] * point[2], -direction[0] * point[1] + direction[1] * point[0], ], dtype=object) return cls(numpy.concatenate((direction, moment)))
[docs] @classmethod def from_direction_and_moment(cls, direction, moment) -> "NormalizedLine": """ Construct a ``NormalizedLine`` from the given ``direction`` and ``moment``. Parameters ---------- direction 3-vector of SymPy expressions or plain numbers representing the line's direction. Should be unit-length for correct behavior, but this is not enforced. moment 3-vector of SymPy expressions or plain numbers representing the line's moment. Returns ------- NormalizedLine """ direction = numpy.array([sympy.sympify(v) for v in direction], dtype=object) moment = numpy.array([sympy.sympify(v) for v in moment], dtype=object) return cls(numpy.concatenate((direction, moment)))
[docs] @classmethod def from_two_points(cls, pt0: Union[PointHomogeneous, Sequence[float]], pt1: Union[PointHomogeneous, Sequence[float]]) -> "NormalizedLine": """ Construct a ``NormalizedLine`` from the given ``pt0`` and ``pt1``. Parameters ---------- pt0 A :class:`.PointHomogeneous` or a 3-vector of SymPy expressions or numbers representing a point on the line. pt1 A :class:`.PointHomogeneous` or a 3-vector of SymPy expressions or numbers representing a different point on the line. Returns ------- NormalizedLine Raises ------ ValueError If the two points are identical (i.e. the direction vector has zero norm). """ from .PointHomogeneous import PointHomogeneous if isinstance(pt0, PointHomogeneous): pt0 = pt0.normalized_euclidean() else: pt0 = numpy.array([sympy.sympify(v) for v in pt0], dtype=object) if isinstance(pt1, PointHomogeneous): pt1 = pt1.normalized_euclidean() else: pt1 = numpy.array([sympy.sympify(v) for v in pt1], dtype=object) direction = pt1 - pt0 if sum(v**2 for v in direction) == sympy.Integer(0): raise ValueError("Cannot construct a line from two identical points.") moment = numpy.array([ -direction[1] * pt0[2] + direction[2] * pt0[1], -direction[2] * pt0[0] + direction[0] * pt0[2], -direction[0] * pt0[1] + direction[1] * pt0[0], ], dtype=object) return cls(numpy.concatenate((direction, moment)))
def _initialize_components( self, unit_screw: Optional[Sequence] ) -> tuple: """ Parse a 6-vector into symbolic direction and moment components. No normalization is applied — the caller is responsible for providing a unit-length direction when that is required. Parameters ---------- unit_screw 6-vector of SymPy expressions, or ``None`` for the Z-axis. Returns ------- tuple ``(direction, moment)`` each an object-dtype 3-vector. Warns ----- UserWarning If the direction vector simplifies to zero. """ if unit_screw is None: direction = numpy.array( [sympy.Integer(0), sympy.Integer(0), sympy.Integer(1)], dtype=object, ) moment = numpy.array( [sympy.Integer(0), sympy.Integer(0), sympy.Integer(0)], dtype=object, ) return direction, moment arr = numpy.array([sympy.sympify(v) for v in unit_screw], dtype=object) direction = arr[0:3] moment = arr[3:6] all_numeric = all( isinstance(v, (int, float)) or (hasattr(v, 'free_symbols') and len(v.free_symbols) == 0) for v in direction ) if all_numeric: norm_sq = sympy.simplify(sum(v**2 for v in direction)) if norm_sq == sympy.Integer(0): warn( "NormalizedLineSymbolic: direction vector has zero norm.", UserWarning, stacklevel=3, ) elif norm_sq != sympy.Integer(1): norm = sympy.sqrt(norm_sq) direction = numpy.array( [sympy.simplify(v / norm) for v in direction], dtype=object ) moment = numpy.array( [sympy.simplify(v / norm) for v in moment], dtype=object ) return direction, moment # ------------------------------------------------------------------ # Representation # ------------------------------------------------------------------ def __repr__(self) -> str: entries = ", ".join(str(v) for v in self.screw) return f"Ln([{entries}])" # ------------------------------------------------------------------ # Equality # ------------------------------------------------------------------ def __eq__(self, other: "NormalizedLine") -> bool: """ Coefficient-wise equality via symbolic simplification. Parameters ---------- other Line to compare against. Returns ------- bool ``True`` if all coordinate differences simplify to zero. """ return all( sympy.simplify(a - b) == sympy.Integer(0) for a, b in zip(self.screw, other.screw) ) # ------------------------------------------------------------------ # Core operations # ------------------------------------------------------------------
[docs] def array(self) -> numpy.ndarray: """ Return screw coordinates as an object-dtype NumPy array. Returns ------- Object-dtype 6-vector of SymPy expressions. """ return numpy.array(self.screw, dtype=object)
[docs] def line2dq_array(self) -> numpy.ndarray: """ Embed the line into dual quaternion space as an 8-vector. Maps ``[l, m]`` → ``[0, l1, l2, l3, 0, -m1, -m2, -m3]``. Returns ------- Object-dtype 8-vector of SymPy expressions. """ return numpy.array([ sympy.Integer(0), self.direction[0], self.direction[1], self.direction[2], sympy.Integer(0), -self.moment[0], -self.moment[1], -self.moment[2], ], dtype=object)
[docs] def contains_point( self, point: Union[PointHomogeneous, numpy.ndarray, Sequence], ) -> bool: """ Return ``True`` if *point* lies on the line (symbolic check). Parameters ---------- point A :class:`.PointHomogeneous` or a 3-vector of SymPy expressions / numbers. Returns ------- bool """ from .PointHomogeneous import PointHomogeneous # lazy import if isinstance(point, PointHomogeneous): pt = point.normalize().array()[1:] else: pt = numpy.array([sympy.sympify(v) for v in point], dtype=object) cross = numpy.array([ pt[1] * self.direction[2] - pt[2] * self.direction[1], pt[2] * self.direction[0] - pt[0] * self.direction[2], pt[0] * self.direction[1] - pt[1] * self.direction[0], ], dtype=object) return all( sympy.simplify(cross[i] - self.moment[i]) == sympy.Integer(0) for i in range(3) )
[docs] def common_perpendicular_to_other_line(self, other: "NormalizedLine") -> tuple: """ Compute the common perpendicular between this line and *other*. Returns the two foot-points, the distance, and the cosine of the angle between the lines. Falls back to the principal-point distance when the lines are parallel. Parameters ---------- other : The second line. Returns ------- tuple[list[numpy.ndarray], float, float] ``(points, distance, cos_angle)`` where ``points`` is a list of two 3-vectors. """ l0 = sympy.Matrix(self.direction) m0 = sympy.Matrix(self.moment) l1 = sympy.Matrix(other.direction) m1 = sympy.Matrix(other.moment) cross = l0.cross(l1) cross_norm_sq = sympy.simplify(cross.dot(cross)) if sympy.simplify(cross_norm_sq) != sympy.Integer(0): num0 = (-m0).cross(l1.cross(cross)) + l0 * m1.dot(cross) p0 = num0 / cross_norm_sq num1 = m1.cross(l0.cross(cross)) - l1 * m0.dot(cross) p1 = num1 / cross_norm_sq diff = p0 - p1 distance = sympy.sqrt(sympy.simplify(diff.dot(diff))) cos_angle = sympy.simplify( l0.dot(l1) / (sympy.sqrt(l0.dot(l0)) * sympy.sqrt(l1.dot(l1))) ) else: p0 = l0.cross(m0) p1 = l1.cross(m1) diff = p0 - p1 distance = sympy.sqrt(sympy.simplify(diff.dot(diff))) cos_angle = sympy.Integer(1) points = [ numpy.array([sympy.simplify(v) for v in p0], dtype=object), numpy.array([sympy.simplify(v) for v in p1], dtype=object), ] return points, sympy.simplify(distance), sympy.simplify(cos_angle)
[docs] def eval(self, subs: dict) -> "NormalizedLineSymbolic": """ Evaluate the line by substituting symbols with values. Parameters ---------- subs Mapping of SymPy symbols to values. Returns ------- NormalizedLineSymbolic New line with substitutions applied. Examples -------- .. code-block:: python import rational_linkages rational_linkages.set_backend("sympy") from rational_linkages import NormalizedLine from sympy import symbols t = symbols("t") line = NormalizedLine([0, 0, 1, t, 0, 0]) line_eval = line.eval({t: 3}) print(line_eval) rational_linkages.set_backend("numpy") .. clear-namespace:: """ return self.__class__([v.subs(subs) for v in self.screw])
[docs] def evaluate(self, param: float): """ Evaluates the line by substituting single value of t. Parameters ---------- param Parameter to evaluate. Returns ------- NormalizedLineSymbolic New line with substitutions applied. """ t = sympy.symbols('t') return self.__class__([v.subs({t: param}) for v in self.screw])
[docs] def evalf(self): """ Replace rational numbers by numerical ones. Returns ------- NormalizedLine Return normalized line with rational numbers replaced by numerical ones. """ return NormalizedLine( numpy.array([v.evalf() for v in self.coordinates], dtype=numpy.float64))