Exemplo n.º 1
0
def eval_intersection_polynomial(nodes1, nodes2, t):
    r"""Evaluates a parametric curve **on** an implicitized algebraic curve.

    Uses :func:`evaluate` to evaluate :math:`f_1(x, y)`, the implicitization
    of ``nodes1``. Then plugs ``t`` into the second parametric curve to
    get an ``x``- and ``y``-coordinate and evaluate the
    **intersection polynomial**:

    .. math::

       g(t) = f_1\left(x_2(t), y_2(t)right)

    Args:
        nodes1 (numpy.ndarray): The nodes in the first curve.
        nodes2 (numpy.ndarray): The nodes in the second curve.
        t (float): The parameter along ``nodes2`` where we evaluate
            the function.

    Returns:
        float: The computed value of :math:`f_1(x_2(t), y_2(t))`.
    """
    (x_val,), (y_val,) = _curve_helpers.evaluate_multi(
        nodes2, np.asfortranarray([t])
    )
    return evaluate(nodes1, x_val, y_val)
Exemplo n.º 2
0
    def evaluate_multi(self, s_vals):
        r"""Evaluate :math:`B(s)` for multiple points along the curve.

        This is done via a modified Horner's method (vectorized for
        each ``s``-value).

        .. doctest:: curve-eval-multi
           :options: +NORMALIZE_WHITESPACE

           >>> nodes = np.asfortranarray([
           ...     [0.0, 0.0, 0.0],
           ...     [1.0, 2.0, 3.0],
           ... ])
           >>> curve = bezier.Curve(nodes, degree=1)
           >>> curve
           <Curve (degree=1, dimension=3)>
           >>> s_vals = np.linspace(0.0, 1.0, 5)
           >>> curve.evaluate_multi(s_vals)
           array([[ 0.  , 0.  , 0.  ],
                  [ 0.25, 0.5 , 0.75],
                  [ 0.5 , 1.  , 1.5 ],
                  [ 0.75, 1.5 , 2.25],
                  [ 1.  , 2.  , 3.  ]])

        Args:
            s_vals (numpy.ndarray): Parameters along the curve (as a
                1D array).

        Returns:
            numpy.ndarray: The points on the curve. As a two dimensional
            NumPy array, with the rows corresponding to each ``s``
            value and the columns to the dimension.
        """
        return _curve_helpers.evaluate_multi(self._nodes, s_vals)
Exemplo n.º 3
0
    def _points_check(self, nodes, pts_exponent=5):
        from bezier import _curve_helpers

        left, right = self._call_function_under_test(nodes)
        # Using the exponent means that ds = 1/2**exp, which
        # can be computed without roundoff.
        num_pts = 2**pts_exponent + 1
        left_half = np.linspace(0.0, 0.5, num_pts)
        right_half = np.linspace(0.5, 1.0, num_pts)
        unit_interval = np.linspace(0.0, 1.0, num_pts)
        pairs = [(left, left_half), (right, right_half)]
        for sub_curve, half in pairs:
            # Make sure sub_curve([0, 1]) == curve(half)
            self.assertEqual(
                _curve_helpers.evaluate_multi(nodes, half),
                _curve_helpers.evaluate_multi(sub_curve, unit_interval),
            )
Exemplo n.º 4
0
    def __call__(self, s, t):
        r"""This computes :math:`F = B_1(s) - B_2(t)` and :math:`DF(s, t)`.

        .. note::

           There is **almost** identical code in :func:`._newton_refine`, but
           that code can avoid computing the ``first_deriv1`` and
           ``first_deriv2`` nodes in cases that :math:`F(s, t) = 0` whereas
           this function assumes they have been given.

        In the case that :math:`DF(s, t)` is singular, the assumption is that
        the intersection has a multiplicity higher than one (i.e. the root is
        non-simple). **Near** a simple root, it must be the case that
        :math:`DF(s, t)` has non-zero determinant, so due to continuity, we
        assume the Jacobian will be invertible nearby.

        Args:
            s (float): The parameter where we'll compute :math:`B_1(s)` and
                :math:`DF(s, t)`.
            t (float): The parameter where we'll compute :math:`B_2(t)` and
                :math:`DF(s, t)`.

        Returns:
            Tuple[Optional[numpy.ndarray], numpy.ndarray]: Pair of

            * The LHS matrix ``DF``, a ``2 x 2`` array. If ``F == 0`` then
              this matrix won't be computed and :data:`None` will be returned.
            * The RHS vector ``F``, a ``2 x 1`` array.
        """
        s_vals = np.asfortranarray([s])
        b1_s = _curve_helpers.evaluate_multi(self.nodes1, s_vals)
        t_vals = np.asfortranarray([t])
        b2_t = _curve_helpers.evaluate_multi(self.nodes2, t_vals)
        func_val = b1_s - b2_t
        if np.all(func_val == 0.0):
            return None, func_val

        else:
            jacobian = np.empty((2, 2), order="F")
            jacobian[:, :1] = _curve_helpers.evaluate_multi(
                self.first_deriv1, s_vals
            )
            jacobian[:, 1:] = -_curve_helpers.evaluate_multi(
                self.first_deriv2, t_vals
            )
            return jacobian, func_val
Exemplo n.º 5
0
def intersect_curves(nodes1, nodes2):
    r"""Intersect two parametric B |eacute| zier curves.

    Args:
        nodes1 (numpy.ndarray): The nodes in the first curve.
        nodes2 (numpy.ndarray): The nodes in the second curve.

    Returns:
        numpy.ndarray: ``Nx2`` array of intersection parameters.
        Each row contains a pair of values :math:`s` and :math:`t`
        (each in :math:`\left[0, 1\right]`) such that the curves
        intersect: :math:`B_1(s) = B_2(t)`.

    Raises:
        NotImplementedError: If the "intersection polynomial" is
        all zeros -- which indicates coincident curves.
    """
    nodes1 = _curve_helpers.full_reduce(nodes1)
    nodes2 = _curve_helpers.full_reduce(nodes2)

    num_nodes1, _ = nodes1.shape
    num_nodes2, _ = nodes2.shape
    swapped = False
    if num_nodes1 > num_nodes2:
        nodes1, nodes2 = nodes2, nodes1
        swapped = True

    coeffs = normalize_polynomial(to_power_basis(nodes1, nodes2))
    if np.all(coeffs == 0.0):
        raise NotImplementedError(_COINCIDENT_ERR)

    _check_non_simple(coeffs)
    t_vals = roots_in_unit_interval(coeffs)

    final_s = []
    final_t = []
    for t_val in t_vals:
        (x_val,
         y_val), = _curve_helpers.evaluate_multi(nodes2,
                                                 np.asfortranarray([t_val]))
        s_val = locate_point(nodes1, x_val, y_val)
        if s_val is not None:
            _resolve_and_add(nodes1, s_val, final_s, nodes2, t_val, final_t)

    result = np.zeros((len(final_s), 2), order='F')
    if swapped:
        final_s, final_t = final_t, final_s

    result[:, 0] = final_s
    result[:, 1] = final_t

    return result
Exemplo n.º 6
0
    def __call__(self, s, t):
        r"""This computes :math:`DG^T G` and :math:`DG^T DG`.

        If :math:`DG^T DG` is not full rank, this means either :math:`DG`
        was not full rank or that it was, but with a relatively high condition
        number. So, in the case that :math:`DG^T DG` is singular, the
        assumption is that the intersection has a multiplicity higher than two.

        Args:
            s (float): The parameter where we'll compute :math:`G(s, t)` and
                :math:`DG(s, t)`.
            t (float): The parameter where we'll compute :math:`G(s, t)` and
                :math:`DG(s, t)`.

        Returns:
            Tuple[Optional[numpy.ndarray], Optional[numpy.ndarray]]: Pair of

            * The LHS matrix ``DG^T DG``, a ``2 x 2`` array. If ``G == 0`` then
              this matrix won't be computed and :data:`None` will be returned.
            * The RHS vector ``DG^T G``, a ``2 x 1`` array.
        """
        s_vals = np.asfortranarray([s])
        b1_s = _curve_helpers.evaluate_multi(self.nodes1, s_vals)
        b1_ds = _curve_helpers.evaluate_multi(self.first_deriv1, s_vals)
        t_vals = np.asfortranarray([t])
        b2_t = _curve_helpers.evaluate_multi(self.nodes2, t_vals)
        b2_dt = _curve_helpers.evaluate_multi(self.first_deriv2, t_vals)
        func_val = np.empty((3, 1), order="F")
        func_val[:2, :] = b1_s - b2_t
        func_val[2, :] = _helpers.cross_product(b1_ds[:, 0], b2_dt[:, 0])
        if np.all(func_val == 0.0):
            return None, func_val[:2, :]

        else:
            jacobian = np.empty((3, 2), order="F")
            jacobian[:2, :1] = b1_ds
            jacobian[:2, 1:] = -b2_dt
            if self.second_deriv1.size == 0:
                jacobian[2, 0] = 0.0
            else:
                jacobian[2, 0] = _helpers.cross_product(
                    _curve_helpers.evaluate_multi(self.second_deriv1, s_vals)[
                        :, 0
                    ],
                    b2_dt[:, 0],
                )
            if self.second_deriv2.size == 0:
                jacobian[2, 1] = 0.0
            else:
                jacobian[2, 1] = _helpers.cross_product(
                    b1_ds[:, 0],
                    _curve_helpers.evaluate_multi(self.second_deriv2, t_vals)[
                        :, 0
                    ],
                )
            modified_lhs = _helpers.matrix_product(jacobian.T, jacobian)
            modified_rhs = _helpers.matrix_product(jacobian.T, func_val)
            return modified_lhs, modified_rhs
Exemplo n.º 7
0
    def evaluate(self, s):
        r"""Evaluate :math:`B(s)` along the curve.

        This method acts as a (partial) inverse to :meth:`locate`.

        See :meth:`evaluate_multi` for more details.

        .. image:: ../../images/curve_evaluate.png
           :align: center

        .. doctest:: curve-eval
           :options: +NORMALIZE_WHITESPACE

           >>> nodes = np.asfortranarray([
           ...     [0.0, 0.625, 1.0],
           ...     [0.0, 0.5  , 0.5],
           ... ])
           >>> curve = bezier.Curve(nodes, degree=2)
           >>> curve.evaluate(0.75)
           array([[0.796875],
                  [0.46875 ]])

        .. testcleanup:: curve-eval

           import make_images
           make_images.curve_evaluate(curve)

        Args:
            s (float): Parameter along the curve.

        Returns:
            numpy.ndarray: The point on the curve (as a two dimensional
            NumPy array with a single column).
        """
        return _curve_helpers.evaluate_multi(
            self._nodes, np.asfortranarray([s])
        )
Exemplo n.º 8
0
def _newton_refine(s, nodes1, t, nodes2):
    r"""Apply one step of 2D Newton's method.

    .. note::

       There is also a Fortran implementation of this function, which
       will be used if it can be built.

    We want to use Newton's method on the function

    .. math::

       F(s, t) = B_1(s) - B_2(t)

    to refine :math:`\left(s_{\ast}, t_{\ast}\right)`. Using this,
    and the Jacobian :math:`DF`, we "solve"

    .. math::

       \left[\begin{array}{c}
           0 \\ 0 \end{array}\right] \approx
           F\left(s_{\ast} + \Delta s, t_{\ast} + \Delta t\right) \approx
           F\left(s_{\ast}, t_{\ast}\right) +
           \left[\begin{array}{c c}
               B_1'\left(s_{\ast}\right) &
               - B_2'\left(t_{\ast}\right) \end{array}\right]
           \left[\begin{array}{c}
               \Delta s \\ \Delta t \end{array}\right]

    and refine with the component updates :math:`\Delta s` and
    :math:`\Delta t`.

    .. note::

       This implementation assumes the curves live in
       :math:`\mathbf{R}^2`.

    For example, the curves

    .. math::

        \begin{align*}
        B_1(s) &= \left[\begin{array}{c} 0 \\ 0 \end{array}\right] (1 - s)^2
            + \left[\begin{array}{c} 2 \\ 4 \end{array}\right] 2s(1 - s)
            + \left[\begin{array}{c} 4 \\ 0 \end{array}\right] s^2 \\
        B_2(t) &= \left[\begin{array}{c} 2 \\ 0 \end{array}\right] (1 - t)
            + \left[\begin{array}{c} 0 \\ 3 \end{array}\right] t
        \end{align*}

    intersect at the point
    :math:`B_1\left(\frac{1}{4}\right) = B_2\left(\frac{1}{2}\right) =
    \frac{1}{2} \left[\begin{array}{c} 2 \\ 3 \end{array}\right]`.

    However, starting from the wrong point we have

    .. math::

        \begin{align*}
        F\left(\frac{3}{8}, \frac{1}{4}\right) &= \frac{1}{8}
            \left[\begin{array}{c} 0 \\ 9 \end{array}\right] \\
        DF\left(\frac{3}{8}, \frac{1}{4}\right) &=
            \left[\begin{array}{c c}
            4 & 2 \\ 2 & -3 \end{array}\right] \\
        \Longrightarrow \left[\begin{array}{c} \Delta s \\ \Delta t
            \end{array}\right] &= \frac{9}{64} \left[\begin{array}{c}
            -1 \\ 2 \end{array}\right].
        \end{align*}

    .. image:: ../images/newton_refine1.png
       :align: center

    .. testsetup:: newton-refine1, newton-refine2, newton-refine3

       import numpy as np
       import bezier
       from bezier._intersection_helpers import newton_refine

       machine_eps = np.finfo(np.float64).eps

       def realroots(*coeffs):
           all_roots = np.roots(coeffs)
           return all_roots[np.where(all_roots.imag == 0.0)].real

    .. doctest:: newton-refine1

       >>> nodes1 = np.asfortranarray([
       ...     [0.0, 2.0, 4.0],
       ...     [0.0, 4.0, 0.0],
       ... ])
       >>> nodes2 = np.asfortranarray([
       ...     [2.0, 0.0],
       ...     [0.0, 3.0],
       ... ])
       >>> s, t = 0.375, 0.25
       >>> new_s, new_t = newton_refine(s, nodes1, t, nodes2)
       >>> 64.0 * (new_s - s)
       -9.0
       >>> 64.0 * (new_t - t)
       18.0

    .. testcleanup:: newton-refine1

       import make_images
       curve1 = bezier.Curve(nodes1, degree=2)
       curve2 = bezier.Curve(nodes2, degree=1)
       make_images.newton_refine1(s, new_s, curve1, t, new_t, curve2)

    For "typical" curves, we converge to a solution quadratically.
    This means that the number of correct digits doubles every
    iteration (until machine precision is reached).

    .. image:: ../images/newton_refine2.png
       :align: center

    .. doctest:: newton-refine2

       >>> nodes1 = np.asfortranarray([
       ...     [0.0, 0.25,  0.5, 0.75, 1.0],
       ...     [0.0, 2.0 , -2.0, 2.0 , 0.0],
       ... ])
       >>> nodes2 = np.asfortranarray([
       ...     [0.0, 0.25, 0.5, 0.75, 1.0],
       ...     [1.0, 0.5 , 0.5, 0.5 , 0.0],
       ... ])
       >>> # The expected intersection is the only real root of
       >>> # 28 s^3 - 30 s^2 + 9 s - 1.
       >>> expected, = realroots(28, -30, 9, -1)
       >>> s_vals = [0.625, None, None, None, None]
       >>> t = 0.625
       >>> np.log2(abs(expected - s_vals[0]))
       -4.399...
       >>> s_vals[1], t = newton_refine(s_vals[0], nodes1, t, nodes2)
       >>> np.log2(abs(expected - s_vals[1]))
       -7.901...
       >>> s_vals[2], t = newton_refine(s_vals[1], nodes1, t, nodes2)
       >>> np.log2(abs(expected - s_vals[2]))
       -16.010...
       >>> s_vals[3], t = newton_refine(s_vals[2], nodes1, t, nodes2)
       >>> np.log2(abs(expected - s_vals[3]))
       -32.110...
       >>> s_vals[4], t = newton_refine(s_vals[3], nodes1, t, nodes2)
       >>> np.allclose(s_vals[4], expected, rtol=6 * machine_eps, atol=0.0)
       True

    .. testcleanup:: newton-refine2

       import make_images
       curve1 = bezier.Curve(nodes1, degree=4)
       curve2 = bezier.Curve(nodes2, degree=4)
       make_images.newton_refine2(s_vals, curve1, curve2)

    However, when the intersection occurs at a point of tangency,
    the convergence becomes linear. This means that the number of
    correct digits added each iteration is roughly constant.

    .. image:: ../images/newton_refine3.png
       :align: center

    .. doctest:: newton-refine3

       >>> nodes1 = np.asfortranarray([
       ...     [0.0, 0.5, 1.0],
       ...     [0.0, 1.0, 0.0],
       ... ])
       >>> nodes2 = np.asfortranarray([
       ...     [0.0, 1.0],
       ...     [0.5, 0.5],
       ... ])
       >>> expected = 0.5
       >>> s_vals = [0.375, None, None, None, None, None]
       >>> t = 0.375
       >>> np.log2(abs(expected - s_vals[0]))
       -3.0
       >>> s_vals[1], t = newton_refine(s_vals[0], nodes1, t, nodes2)
       >>> np.log2(abs(expected - s_vals[1]))
       -4.0
       >>> s_vals[2], t = newton_refine(s_vals[1], nodes1, t, nodes2)
       >>> np.log2(abs(expected - s_vals[2]))
       -5.0
       >>> s_vals[3], t = newton_refine(s_vals[2], nodes1, t, nodes2)
       >>> np.log2(abs(expected - s_vals[3]))
       -6.0
       >>> s_vals[4], t = newton_refine(s_vals[3], nodes1, t, nodes2)
       >>> np.log2(abs(expected - s_vals[4]))
       -7.0
       >>> s_vals[5], t = newton_refine(s_vals[4], nodes1, t, nodes2)
       >>> np.log2(abs(expected - s_vals[5]))
       -8.0

    .. testcleanup:: newton-refine3

       import make_images
       curve1 = bezier.Curve(nodes1, degree=2)
       curve2 = bezier.Curve(nodes2, degree=1)
       make_images.newton_refine3(s_vals, curve1, curve2)

    Unfortunately, the process terminates with an error that is not close
    to machine precision :math:`\varepsilon` when
    :math:`\Delta s = \Delta t = 0`.

    .. testsetup:: newton-refine3-continued

       import numpy as np
       import bezier
       from bezier._intersection_helpers import newton_refine

       nodes1 = np.asfortranarray([
           [0.0, 0.5, 1.0],
           [0.0, 1.0, 0.0],
       ])
       nodes2 = np.asfortranarray([
           [0.0, 1.0],
           [0.5, 0.5],
       ])

    .. doctest:: newton-refine3-continued

       >>> s1 = t1 = 0.5 - 0.5**27
       >>> np.log2(0.5 - s1)
       -27.0
       >>> s2, t2 = newton_refine(s1, nodes1, t1, nodes2)
       >>> s2 == t2
       True
       >>> np.log2(0.5 - s2)
       -28.0
       >>> s3, t3 = newton_refine(s2, nodes1, t2, nodes2)
       >>> s3 == t3 == s2
       True

    Due to round-off near the point of tangency, the final error
    resembles :math:`\sqrt{\varepsilon}` rather than machine
    precision as expected.

    .. note::

       The following is not implemented in this function. It's just
       an exploration on how the shortcomings might be addressed.

    However, this can be overcome. At the point of tangency, we want
    :math:`B_1'(s) \parallel B_2'(t)`. This can be checked numerically via

    .. math::

        B_1'(s) \times B_2'(t) = 0.

    For the last example (the one that converges linearly), this is

    .. math::

        0 = \left[\begin{array}{c} 1 \\ 2 - 4s \end{array}\right] \times
            \left[\begin{array}{c} 1 \\ 0 \end{array}\right] = 4 s - 2.

    With this, we can modify Newton's method to find a zero of the
    over-determined system

    .. math::

        G(s, t) = \left[\begin{array}{c} B_0(s) - B_1(t) \\
            B_1'(s) \times B_2'(t) \end{array}\right] =
            \left[\begin{array}{c} s - t \\ 2 s (1 - s) - \frac{1}{2} \\
            4 s - 2\end{array}\right].

    Since :math:`DG` is :math:`3 \times 2`, we can't invert it. However,
    we can find a least-squares solution:

    .. math::

        \left(DG^T DG\right) \left[\begin{array}{c}
            \Delta s \\ \Delta t \end{array}\right] = -DG^T G.

    This only works if :math:`DG` has full rank. In this case, it does
    since the submatrix containing the first and last rows has rank two:

    .. math::

        DG = \left[\begin{array}{c c} 1 & -1 \\
            2 - 4 s & 0 \\
            4 & 0 \end{array}\right].

    Though this avoids a singular system, the normal equations have a
    condition number that is the square of the condition number of the matrix.

    Starting from :math:`s = t = \frac{3}{8}` as above:

    .. testsetup:: newton-refine4

       import numpy as np
       from bezier import _helpers

       def modified_update(s, t):
           minus_G = np.asfortranarray([
               [t - s],
               [0.5 - 2.0 * s * (1.0 - s)],
               [2.0 - 4.0 * s],
           ])
           DG = np.asfortranarray([
               [1.0, -1.0],
               [2.0 - 4.0 * s, 0.0],
               [4.0, 0.0],
           ])
           DG_t = np.asfortranarray(DG.T)

           LHS = _helpers.matrix_product(DG_t, DG)
           RHS = _helpers.matrix_product(DG_t, minus_G)
           delta_params = np.linalg.solve(LHS, RHS)
           delta_s, delta_t = delta_params.flatten()
           return s + delta_s, t + delta_t

    .. doctest:: newton-refine4

       >>> s0, t0 = 0.375, 0.375
       >>> np.log2(0.5 - s0)
       -3.0
       >>> s1, t1 = modified_update(s0, t0)
       >>> s1 == t1
       True
       >>> 1040.0 * s1
       519.0
       >>> np.log2(0.5 - s1)
       -10.022...
       >>> s2, t2 = modified_update(s1, t1)
       >>> s2 == t2
       True
       >>> np.log2(0.5 - s2)
       -31.067...
       >>> s3, t3 = modified_update(s2, t2)
       >>> s3 == t3 == 0.5
       True

    Args:
        s (float): Parameter of a near-intersection along the first curve.
        nodes1 (numpy.ndarray): Nodes of first curve forming intersection.
        t (float): Parameter of a near-intersection along the second curve.
        nodes2 (numpy.ndarray): Nodes of second curve forming intersection.

    Returns:
        Tuple[float, float]: The refined parameters from a single Newton
        step.

    Raises:
        ValueError: If the Jacobian is singular at ``(s, t)``.
    """
    # NOTE: We form -F(s, t) since we want to solve -DF^{-1} F(s, t).
    func_val = _curve_helpers.evaluate_multi(
        nodes2, np.asfortranarray([t])
    ) - _curve_helpers.evaluate_multi(nodes1, np.asfortranarray([s]))
    if np.all(func_val == 0.0):
        # No refinement is needed.
        return s, t

    # NOTE: This assumes the curves are 2D.
    jac_mat = np.empty((2, 2), order="F")
    jac_mat[:, :1] = _curve_helpers.evaluate_hodograph(s, nodes1)
    jac_mat[:, 1:] = -_curve_helpers.evaluate_hodograph(t, nodes2)
    # Solve the system.
    singular, delta_s, delta_t = _helpers.solve2x2(jac_mat, func_val[:, 0])
    if singular:
        raise ValueError("Jacobian is singular.")

    return s + delta_s, t + delta_t