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 = _py_curve_helpers.evaluate_multi(self.nodes1, s_vals) b1_ds = _py_curve_helpers.evaluate_multi(self.first_deriv1, s_vals) t_vals = np.asfortranarray([t]) b2_t = _py_curve_helpers.evaluate_multi(self.nodes2, t_vals) b2_dt = _py_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, :] = _py_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] = _py_helpers.cross_product( _py_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] = _py_helpers.cross_product( b1_ds[:, 0], _py_curve_helpers.evaluate_multi(self.second_deriv2, t_vals)[:, 0], ) modified_lhs = _py_helpers.matrix_product(jacobian.T, jacobian) modified_rhs = _py_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 curve) 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 = _py_helpers.matrix_product(nodes, _LINEAR_SUBDIVIDE_LEFT) right_nodes = _py_helpers.matrix_product(nodes, _LINEAR_SUBDIVIDE_RIGHT) elif num_nodes == 3: left_nodes = _py_helpers.matrix_product(nodes, _QUADRATIC_SUBDIVIDE_LEFT) right_nodes = _py_helpers.matrix_product(nodes, _QUADRATIC_SUBDIVIDE_RIGHT) elif num_nodes == 4: left_nodes = _py_helpers.matrix_product(nodes, _CUBIC_SUBDIVIDE_LEFT) right_nodes = _py_helpers.matrix_product(nodes, _CUBIC_SUBDIVIDE_RIGHT) else: left_mat, right_mat = make_subdivision_matrices(num_nodes - 1) left_nodes = _py_helpers.matrix_product(nodes, left_mat) right_nodes = _py_helpers.matrix_product(nodes, right_mat) return left_nodes, right_nodes
def _actually_inverse_helper(self, degree): from bezier import _py_curve_helpers from bezier import _py_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 = _py_curve_helpers.elevate_nodes(id_mat) result = _py_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 _py_helpers.UnsupportedDegree(num_nodes - 1, supported=(0, 1, 2, 3, 4)) projected = _py_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 = np.eye(num_companion, order="F") evaluated = coeffs[-1] * id_mat for index in range(num_coeffs - 2, -1, -1): coeff = coeffs[index] evaluated = ( _py_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 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 _py_helpers.UnsupportedDegree(num_nodes - 1, supported=(1, 2, 3, 4)) result = _py_helpers.matrix_product(nodes, reduction) result /= denom return result
def _call_function_under_test(mat1, mat2): from bezier import _py_helpers return _py_helpers.matrix_product(mat1, mat2)