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
def _subdivide_nodes(nodes): """Subdivide a curve into two sub-curves. Does so by taking the unit interval (i.e. the domain of the surface) and splitting it into two sub-intervals by splitting down the middle. .. note:: There is also a Fortran implementation of this function, which will be used if it can be built. Args: nodes (numpy.ndarray): The nodes defining a B |eacute| zier curve. Returns: Tuple[numpy.ndarray, numpy.ndarray]: The nodes for the two sub-curves. """ _, num_nodes = np.shape(nodes) if num_nodes == 2: left_nodes = _helpers.matrix_product(nodes, _LINEAR_SUBDIVIDE_LEFT) right_nodes = _helpers.matrix_product(nodes, _LINEAR_SUBDIVIDE_RIGHT) elif num_nodes == 3: left_nodes = _helpers.matrix_product(nodes, _QUADRATIC_SUBDIVIDE_LEFT) right_nodes = _helpers.matrix_product( nodes, _QUADRATIC_SUBDIVIDE_RIGHT ) elif num_nodes == 4: left_nodes = _helpers.matrix_product(nodes, _CUBIC_SUBDIVIDE_LEFT) right_nodes = _helpers.matrix_product(nodes, _CUBIC_SUBDIVIDE_RIGHT) else: left_mat, right_mat = make_subdivision_matrices(num_nodes - 1) left_nodes = _helpers.matrix_product(nodes, left_mat) right_nodes = _helpers.matrix_product(nodes, right_mat) return left_nodes, right_nodes
def reduce_pseudo_inverse(nodes, degree): """Performs degree-reduction for a B |eacute| zier curve. Does so by using the pseudo-inverse of the degree elevation operator (which is overdetermined). Args: nodes (numpy.ndarray): The nodes in the curve. degree (int): The degree of the curve. Returns: numpy.ndarray: The reduced nodes. Raises: NotImplementedError: If the degree is not 1, 2, 3 or 4. """ if degree == 1: reduction = _REDUCTION0 denom = _REDUCTION_DENOM0 elif degree == 2: reduction = _REDUCTION1 denom = _REDUCTION_DENOM1 elif degree == 3: reduction = _REDUCTION2 denom = _REDUCTION_DENOM2 elif degree == 4: reduction = _REDUCTION3 denom = _REDUCTION_DENOM3 else: raise NotImplementedError(degree) result = _helpers.matrix_product(reduction, nodes) result /= denom return result
def _actually_inverse_helper(self, degree): from bezier import _curve_helpers from bezier import _helpers nodes = np.eye(degree + 2, order="F") reduction_mat = self._call_function_under_test(nodes) id_mat = np.eye(degree + 1, order="F") elevation_mat = _curve_helpers.elevate_nodes(id_mat) result = _helpers.matrix_product(elevation_mat, reduction_mat) return result, id_mat
def maybe_reduce(nodes): r"""Reduce nodes in a curve if they are degree-elevated. .. note:: This is a helper for :func:`_full_reduce`. Hence there is no corresponding Fortran speedup. We check if the nodes are degree-elevated by projecting onto the space of degree-elevated curves of the same degree, then comparing to the projection. We form the projection by taking the corresponding (right) elevation matrix :math:`E` (from one degree lower) and forming :math:`E^T \left(E E^T\right)^{-1} E`. Args: nodes (numpy.ndarray): The nodes in the curve. Returns: Tuple[bool, numpy.ndarray]: Pair of values. The first indicates if the ``nodes`` were reduced. The second is the resulting nodes, either the reduced ones or the original passed in. Raises: .UnsupportedDegree: If the curve is degree 5 or higher. """ _, num_nodes = nodes.shape if num_nodes < 2: return False, nodes elif num_nodes == 2: projection = _PROJECTION0 denom = _PROJ_DENOM0 elif num_nodes == 3: projection = _PROJECTION1 denom = _PROJ_DENOM1 elif num_nodes == 4: projection = _PROJECTION2 denom = _PROJ_DENOM2 elif num_nodes == 5: projection = _PROJECTION3 denom = _PROJ_DENOM3 else: raise _helpers.UnsupportedDegree( num_nodes - 1, supported=(0, 1, 2, 3, 4) ) projected = _helpers.matrix_product(nodes, projection) / denom relative_err = projection_error(nodes, projected) if relative_err < _REDUCE_THRESHOLD: return True, reduce_pseudo_inverse(nodes) else: return False, nodes
def _check_non_simple(coeffs): r"""Checks that a polynomial has no non-simple roots. Does so by computing the companion matrix :math:`A` of :math:`f'` and then evaluating the rank of :math:`B = f(A)`. If :math:`B` is not full rank, then :math:`f` and :math:`f'` have a shared factor. See: https://dx.doi.org/10.1016/0024-3795(70)90023-6 .. note:: This assumes that :math:`f \neq 0`. Args: coeffs (numpy.ndarray): ``d + 1``-array of coefficients in monomial / power basis. Raises: NotImplementedError: If the polynomial has non-simple roots. """ coeffs = _strip_leading_zeros(coeffs) num_coeffs, = coeffs.shape if num_coeffs < 3: return deriv_poly = polynomial.polyder(coeffs) companion = polynomial.polycompanion(deriv_poly) # NOTE: `polycompanion()` returns a C-contiguous array. companion = companion.T # Use Horner's method to evaluate f(companion) num_companion, _ = companion.shape id_mat = _helpers.eye(num_companion) evaluated = coeffs[-1] * id_mat for index in six.moves.xrange(num_coeffs - 2, -1, -1): coeff = coeffs[index] evaluated = (_helpers.matrix_product(evaluated, companion) + coeff * id_mat) if num_companion == 1: # NOTE: This relies on the fact that coeffs is normalized. if np.abs(evaluated[0, 0]) > _NON_SIMPLE_THRESHOLD: rank = 1 else: rank = 0 else: rank = np.linalg.matrix_rank(evaluated) if rank < num_companion: raise NotImplementedError(_NON_SIMPLE_ERR, coeffs)
def _maybe_reduce(nodes): r"""Reduce nodes in a curve if they are degree-elevated. We check if the nodes are degree-elevated by projecting onto the space of degree-elevated curves of the same degree, then comparing to the projection. We form the projection by taking the corresponding elevation matrix :math:`E` (from one degree lower) and forming :math:`E \left(E^T E\right)^{-1} E^T`. Args: nodes (numpy.ndarray): The nodes in the curve. Returns: Tuple[bool, numpy.ndarray]: Pair of values. The first indicates if the ``nodes`` were reduced. The second is the resulting nodes, either the reduced ones or the original passed in. Raises: NotImplementedError: If the curve is degree 5 or higher. """ num_nodes, _ = nodes.shape if num_nodes < 2: return False, nodes elif num_nodes == 2: projection = _PROJECTION0 denom = _PROJ_DENOM0 elif num_nodes == 3: projection = _PROJECTION1 denom = _PROJ_DENOM1 elif num_nodes == 4: projection = _PROJECTION2 denom = _PROJ_DENOM2 elif num_nodes == 5: projection = _PROJECTION3 denom = _PROJ_DENOM3 else: raise NotImplementedError(num_nodes) projected = _helpers.matrix_product(projection, nodes) / denom relative_err = _projection_error(nodes, projected) if relative_err < _REDUCE_THRESHOLD: return True, reduce_pseudo_inverse(nodes, num_nodes - 1) else: return False, nodes
def _reduce_pseudo_inverse(nodes): """Performs degree-reduction for a B |eacute| zier curve. Does so by using the pseudo-inverse of the degree elevation operator (which is overdetermined). .. note:: There is also a Fortran implementation of this function, which will be used if it can be built. Args: nodes (numpy.ndarray): The nodes in the curve. Returns: numpy.ndarray: The reduced nodes. Raises: .UnsupportedDegree: If the degree is not 1, 2, 3 or 4. """ _, num_nodes = np.shape(nodes) if num_nodes == 2: reduction = _REDUCTION0 denom = _REDUCTION_DENOM0 elif num_nodes == 3: reduction = _REDUCTION1 denom = _REDUCTION_DENOM1 elif num_nodes == 4: reduction = _REDUCTION2 denom = _REDUCTION_DENOM2 elif num_nodes == 5: reduction = _REDUCTION3 denom = _REDUCTION_DENOM3 else: raise _helpers.UnsupportedDegree(num_nodes - 1, supported=(1, 2, 3, 4)) result = _helpers.matrix_product(nodes, reduction) result /= denom return result
def subdivide(self): r"""Split the curve :math:`B(s)` into a left and right half. Takes the interval :math:`\left[0, 1\right]` and splits the curve into :math:`B_1 = B\left(\left[0, \frac{1}{2}\right]\right)` and :math:`B_2 = B\left(\left[\frac{1}{2}, 1\right]\right)`. In order to do this, also reparameterizes the curve, hence the resulting left and right halves have new nodes. .. image:: ../images/curve_subdivide.png :align: center .. doctest:: curve-subdivide :options: +NORMALIZE_WHITESPACE >>> nodes = np.asfortranarray([ ... [0.0 , 0.0], ... [1.25, 3.0], ... [2.0 , 1.0], ... ]) >>> curve = bezier.Curve(nodes, degree=2) >>> left, right = curve.subdivide() >>> left <Curve (degree=2, dimension=2, start=0, end=0.5)> >>> left.nodes array([[ 0. , 0. ], [ 0.625, 1.5 ], [ 1.125, 1.75 ]]) >>> right <Curve (degree=2, dimension=2, start=0.5, end=1)> >>> right.nodes array([[ 1.125, 1.75 ], [ 1.625, 2. ], [ 2. , 1. ]]) .. testcleanup:: curve-subdivide import make_images make_images.curve_subdivide(curve, left, right) Returns: Tuple[Curve, Curve]: The left and right sub-curves. """ if self._degree == 1: left_nodes = _helpers.matrix_product(_LINEAR_SUBDIVIDE_LEFT, self._nodes) right_nodes = _helpers.matrix_product(_LINEAR_SUBDIVIDE_RIGHT, self._nodes) elif self._degree == 2: left_nodes = _helpers.matrix_product(_QUADRATIC_SUBDIVIDE_LEFT, self._nodes) right_nodes = _helpers.matrix_product(_QUADRATIC_SUBDIVIDE_RIGHT, self._nodes) elif self._degree == 3: left_nodes = _helpers.matrix_product(_CUBIC_SUBDIVIDE_LEFT, self._nodes) right_nodes = _helpers.matrix_product(_CUBIC_SUBDIVIDE_RIGHT, self._nodes) else: left_mat, right_mat = _curve_helpers.make_subdivision_matrices( self._degree) left_nodes = _helpers.matrix_product(left_mat, self._nodes) right_nodes = _helpers.matrix_product(right_mat, self._nodes) midpoint = 0.5 * (self._start + self._end) left = Curve(left_nodes, self._degree, start=self._start, end=midpoint, root=self._root, _copy=False) right = Curve(right_nodes, self._degree, start=midpoint, end=self._end, root=self._root, _copy=False) return left, right
def _call_function_under_test(mat1, mat2): from bezier import _helpers return _helpers.matrix_product(mat1, mat2)
def subdivide(self): r"""Split the surface into four sub-surfaces. Does so by taking the unit triangle (i.e. the domain of the surface) and splitting it into four sub-triangles .. image:: ../images/surface_subdivide1.png :align: center Then the surface is re-parameterized via the map to / from the given sub-triangles and the unit triangle. For example, when a degree two surface is subdivided: .. image:: ../images/surface_subdivide2.png :align: center .. doctest:: surface-subdivide :options: +NORMALIZE_WHITESPACE >>> nodes = np.asfortranarray([ ... [-1.0 , 0.0 ], ... [ 0.5 , 0.5 ], ... [ 2.0 , 0.0 ], ... [ 0.25, 1.75], ... [ 2.0 , 3.0 ], ... [ 0.0 , 4.0 ], ... ]) >>> surface = bezier.Surface(nodes, degree=2) >>> _, sub_surface_b, _, _ = surface.subdivide() >>> sub_surface_b <Surface (degree=2, dimension=2, base=(0.5, 0.5), width=-0.5)> >>> sub_surface_b.nodes array([[ 1.5 , 2.5 ], [ 0.6875, 2.3125], [-0.125 , 1.875 ], [ 1.1875, 1.3125], [ 0.4375, 1.3125], [ 0.5 , 0.25 ]]) .. testcleanup:: surface-subdivide import make_images make_images.surface_subdivide1() make_images.surface_subdivide2(surface, sub_surface_b) Returns: Tuple[Surface, Surface, Surface, Surface]: The lower left, central, lower right and upper left sub-surfaces (in that order). """ if self._degree == 1: nodes_a = _helpers.matrix_product( _surface_helpers.LINEAR_SUBDIVIDE_A, self._nodes) nodes_b = _helpers.matrix_product( _surface_helpers.LINEAR_SUBDIVIDE_B, self._nodes) nodes_c = _helpers.matrix_product( _surface_helpers.LINEAR_SUBDIVIDE_C, self._nodes) nodes_d = _helpers.matrix_product( _surface_helpers.LINEAR_SUBDIVIDE_D, self._nodes) elif self._degree == 2: nodes_a = _helpers.matrix_product( _surface_helpers.QUADRATIC_SUBDIVIDE_A, self._nodes) nodes_b = _helpers.matrix_product( _surface_helpers.QUADRATIC_SUBDIVIDE_B, self._nodes) nodes_c = _helpers.matrix_product( _surface_helpers.QUADRATIC_SUBDIVIDE_C, self._nodes) nodes_d = _helpers.matrix_product( _surface_helpers.QUADRATIC_SUBDIVIDE_D, self._nodes) elif self._degree == 3: nodes_a = _helpers.matrix_product( _surface_helpers.CUBIC_SUBDIVIDE_A, self._nodes) nodes_b = _helpers.matrix_product( _surface_helpers.CUBIC_SUBDIVIDE_B, self._nodes) nodes_c = _helpers.matrix_product( _surface_helpers.CUBIC_SUBDIVIDE_C, self._nodes) nodes_d = _helpers.matrix_product( _surface_helpers.CUBIC_SUBDIVIDE_D, self._nodes) elif self._degree == 4: nodes_a = _helpers.matrix_product( _surface_helpers.QUARTIC_SUBDIVIDE_A, self._nodes) nodes_b = _helpers.matrix_product( _surface_helpers.QUARTIC_SUBDIVIDE_B, self._nodes) nodes_c = _helpers.matrix_product( _surface_helpers.QUARTIC_SUBDIVIDE_C, self._nodes) nodes_d = _helpers.matrix_product( _surface_helpers.QUARTIC_SUBDIVIDE_D, self._nodes) else: nodes_a = _surface_helpers.specialize_surface( self._nodes, self._degree, (1.0, 0.0, 0.0), (0.5, 0.5, 0.0), (0.5, 0.0, 0.5)) nodes_b = _surface_helpers.specialize_surface( self._nodes, self._degree, (0.0, 0.5, 0.5), (0.5, 0.0, 0.5), (0.5, 0.5, 0.0)) nodes_c = _surface_helpers.specialize_surface( self._nodes, self._degree, (0.5, 0.5, 0.0), (0.0, 1.0, 0.0), (0.0, 0.5, 0.5)) nodes_d = _surface_helpers.specialize_surface( self._nodes, self._degree, (0.5, 0.0, 0.5), (0.0, 0.5, 0.5), (0.0, 0.0, 1.0)) half_width = 0.5 * self._width shifted_x = self._base_x + half_width shifted_y = self._base_y + half_width return ( Surface(nodes_a, self._degree, base_x=self._base_x, base_y=self._base_y, width=half_width, _copy=False), Surface(nodes_b, self._degree, base_x=shifted_x, base_y=shifted_y, width=-half_width, _copy=False), Surface(nodes_c, self._degree, base_x=shifted_x, base_y=self._base_y, width=half_width, _copy=False), Surface(nodes_d, self._degree, base_x=self._base_x, base_y=shifted_y, width=half_width, _copy=False), )