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 poly_to_power_basis(bezier_coeffs): """Convert a B |eacute| zier curve to polynomial in power basis. .. note:: This assumes, but does not verify, that the "B |eacute| zier degree" matches the true degree of the curve. Callers can guarantee this by calling :func:`.full_reduce`. Args: bezier_coeffs (numpy.ndarray): A 1D array of coefficients in the Bernstein basis. Returns: numpy.ndarray: 1D array of coefficients in monomial basis. Raises: UnsupportedDegree: If the degree of the curve is not among 0, 1, 2 or 3. """ (num_coeffs, ) = bezier_coeffs.shape if num_coeffs == 1: return bezier_coeffs elif num_coeffs == 2: # C0 (1 - s) + C1 s = C0 + (C1 - C0) s coeff0, coeff1 = bezier_coeffs return np.asfortranarray([coeff0, coeff1 - coeff0]) elif num_coeffs == 3: # C0 (1 - s)^2 + C1 2 (1 - s) s + C2 s^2 # = C0 + 2(C1 - C0) s + (C2 - 2 C1 + C0) s^2 coeff0, coeff1, coeff2 = bezier_coeffs return np.asfortranarray( [coeff0, 2.0 * (coeff1 - coeff0), coeff2 - 2.0 * coeff1 + coeff0]) elif num_coeffs == 4: # C0 (1 - s)^3 + C1 3 (1 - s)^2 + C2 3 (1 - s) s^2 + C3 s^3 # = C0 + 3(C1 - C0) s + 3(C2 - 2 C1 + C0) s^2 + # (C3 - 3 C2 + 3 C1 - C0) s^3 coeff0, coeff1, coeff2, coeff3 = bezier_coeffs return np.asfortranarray([ coeff0, 3.0 * (coeff1 - coeff0), 3.0 * (coeff2 - 2.0 * coeff1 + coeff0), coeff3 - 3.0 * coeff2 + 3.0 * coeff1 - coeff0, ]) else: raise _py_helpers.UnsupportedDegree(num_coeffs - 1, supported=(0, 1, 2, 3))
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 _compute_valid(self): r"""Determines if the current triangle is "valid". Does this by checking if the Jacobian of the map from the reference triangle is everywhere positive. Returns: bool: Flag indicating if the current triangle is valid. Raises: NotImplementedError: If the triangle is in a dimension other than :math:`\mathbf{R}^2`. .UnsupportedDegree: If the degree is not 1, 2 or 3. """ if self._dimension != 2: raise NotImplementedError("Validity check only implemented in R^2") poly_sign = None if self._degree == 1: # In the linear case, we are only invalid if the points # are collinear. first_deriv = self._nodes[:, 1:] - self._nodes[:, :-1] # pylint: disable=assignment-from-no-return poly_sign = _SIGN(np.linalg.det(first_deriv)) # pylint: enable=assignment-from-no-return elif self._degree == 2: bernstein = _py_triangle_helpers.quadratic_jacobian_polynomial( self._nodes ) poly_sign = _py_triangle_helpers.polynomial_sign(bernstein, 2) elif self._degree == 3: bernstein = _py_triangle_helpers.cubic_jacobian_polynomial( self._nodes ) poly_sign = _py_triangle_helpers.polynomial_sign(bernstein, 4) else: raise _py_helpers.UnsupportedDegree( self._degree, supported=(1, 2, 3) ) return poly_sign == 1
def evaluate(nodes, x_val, y_val): r"""Evaluate the implicitized bivariate polynomial containing the curve. Assumes the `algebraic curve`_ containing :math:`B(s, t)` is given by :math:`f(x, y) = 0`. This function evaluates :math:`f(x, y)`. .. note:: This assumes, but doesn't check, that ``nodes`` has 2 rows. .. note:: This assumes, but doesn't check, that ``nodes`` is not degree-elevated. If it were degree-elevated, then the Sylvester matrix will always have zero determinant. Args: nodes (numpy.ndarray): ``2 x N`` array of nodes in a curve. x_val (float): ``x``-coordinate for evaluation. y_val (float): ``y``-coordinate for evaluation. Returns: float: The computed value of :math:`f(x, y)`. Raises: ValueError: If the curve is a point. UnsupportedDegree: If the degree is not 1, 2 or 3. """ _, num_nodes = nodes.shape if num_nodes == 1: raise ValueError("A point cannot be implicitized") if num_nodes == 2: # x(s) - x = (x0 - x) (1 - s) + (x1 - x) s # y(s) - y = (y0 - y) (1 - s) + (y1 - y) s # Modified Sylvester: [x0 - x, x1 - x] # [y0 - y, y1 - y] return (nodes[0, 0] - x_val) * (nodes[1, 1] - y_val) - ( nodes[0, 1] - x_val) * (nodes[1, 0] - y_val) if num_nodes == 3: # x(s) - x = (x0 - x) (1 - s)^2 + 2 (x1 - x) s(1 - s) + (x2 - x) s^2 # y(s) - y = (y0 - y) (1 - s)^2 + 2 (y1 - y) s(1 - s) + (y2 - y) s^2 # Modified Sylvester: [x0 - x, 2(x1 - x), x2 - x, 0] = A|B|C|0 # [ 0, x0 - x, 2(x1 - x), x2 - x] 0|A|B|C # [y0 - y, 2(y1 - y), y2 - y, 0] D|E|F|0 # [ 0, y0 - y, 2(y1 - y), y2 - y] 0|D|E|F val_a, val_b, val_c = nodes[0, :] - x_val val_b *= 2 val_d, val_e, val_f = nodes[1, :] - y_val val_e *= 2 # [A, B, C] [E, F, 0] # det [E, F, 0] = - det [A, B, C] = -E (BF - CE) + F(AF - CD) # [D, E, F] [D, E, F] sub1 = val_b * val_f - val_c * val_e sub2 = val_a * val_f - val_c * val_d sub_det_a = -val_e * sub1 + val_f * sub2 # [B, C, 0] # det [A, B, C] = B (BF - CE) - C (AF - CD) # [D, E, F] sub_det_d = val_b * sub1 - val_c * sub2 return val_a * sub_det_a + val_d * sub_det_d if num_nodes == 4: return _evaluate3(nodes, x_val, y_val) raise _py_helpers.UnsupportedDegree(num_nodes - 1, supported=(1, 2, 3))