Skip to content

Commit

Permalink
Moving most of Curve.subdivide() into helpers.
Browse files Browse the repository at this point in the history
This is so that other functions in `_curve_helpers` can
use the low-level "subdivide the nodes" rather than having
to rely on using a `Curve` object.
  • Loading branch information
dhermes committed Aug 14, 2017
1 parent 9052e46 commit 1fc80e5
Show file tree
Hide file tree
Showing 3 changed files with 246 additions and 133 deletions.
62 changes: 62 additions & 0 deletions src/bezier/_curve_helpers.py
Expand Up @@ -93,6 +93,36 @@
[ -1.5, 6.0, -9.0, 6.0, 103.5], # noqa: E201
])
_REDUCTION_DENOM3 = 105.0
_LINEAR_SUBDIVIDE_LEFT = np.asfortranarray([
[1.0, 0.0],
[0.5, 0.5],
])
_LINEAR_SUBDIVIDE_RIGHT = np.asfortranarray([
[0.5, 0.5],
[0.0, 1.0],
])
_QUADRATIC_SUBDIVIDE_LEFT = np.asfortranarray([
[1.0 , 0.0, 0.0 ],
[0.5 , 0.5, 0.0 ],
[0.25, 0.5, 0.25],
])
_QUADRATIC_SUBDIVIDE_RIGHT = np.asfortranarray([
[0.25, 0.5, 0.25],
[0.0 , 0.5, 0.5 ],
[0.0 , 0.0, 1.0 ],
])
_CUBIC_SUBDIVIDE_LEFT = np.asfortranarray([
[1.0 , 0.0 , 0.0 , 0.0 ],
[0.5 , 0.5 , 0.0 , 0.0 ],
[0.25 , 0.5 , 0.25 , 0.0 ],
[0.125, 0.375, 0.375, 0.125],
])
_CUBIC_SUBDIVIDE_RIGHT = np.asfortranarray([
[0.125, 0.375, 0.375, 0.125],
[0.0 , 0.25 , 0.5 , 0.25 ],
[0.0 , 0.0 , 0.5 , 0.5 ],
[0.0 , 0.0 , 0.0 , 1.0 ],
])
# pylint: enable=bad-whitespace


Expand Down Expand Up @@ -124,6 +154,38 @@ def make_subdivision_matrices(degree):
return left, right


def subdivide_nodes(nodes, degree):
"""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.
Args:
nodes (numpy.ndarray): The nodes defining a B |eacute| zier curve.
degree (int): The degree of the curve (assumed to be one less than
the number of ``nodes``.
Returns:
Tuple[numpy.ndarray, numpy.ndarray]: The nodes for the two sub-curves.
"""
if degree == 1:
left_nodes = _helpers.matrix_product(_LINEAR_SUBDIVIDE_LEFT, nodes)
right_nodes = _helpers.matrix_product(_LINEAR_SUBDIVIDE_RIGHT, nodes)
elif degree == 2:
left_nodes = _helpers.matrix_product(_QUADRATIC_SUBDIVIDE_LEFT, nodes)
right_nodes = _helpers.matrix_product(
_QUADRATIC_SUBDIVIDE_RIGHT, nodes)
elif degree == 3:
left_nodes = _helpers.matrix_product(_CUBIC_SUBDIVIDE_LEFT, nodes)
right_nodes = _helpers.matrix_product(_CUBIC_SUBDIVIDE_RIGHT, nodes)
else:
left_mat, right_mat = make_subdivision_matrices(degree)
left_nodes = _helpers.matrix_product(left_mat, nodes)
right_nodes = _helpers.matrix_product(right_mat, nodes)

return left_nodes, right_nodes


def _evaluate_multi(nodes, s_vals):
r"""Computes multiple points along a curve.
Expand Down
160 changes: 153 additions & 7 deletions tests/test__curve_helpers.py
Expand Up @@ -39,23 +39,169 @@ def _helper(self, degree, expected_l, expected_r):
self.assertEqual(right, expected_r)

def test_linear(self):
from bezier import curve
from bezier import _curve_helpers

self._helper(
1, curve._LINEAR_SUBDIVIDE_LEFT, curve._LINEAR_SUBDIVIDE_RIGHT)
1, _curve_helpers._LINEAR_SUBDIVIDE_LEFT,
_curve_helpers._LINEAR_SUBDIVIDE_RIGHT)

def test_quadratic(self):
from bezier import curve
from bezier import _curve_helpers

self._helper(
2, curve._QUADRATIC_SUBDIVIDE_LEFT,
curve._QUADRATIC_SUBDIVIDE_RIGHT)
2, _curve_helpers._QUADRATIC_SUBDIVIDE_LEFT,
_curve_helpers._QUADRATIC_SUBDIVIDE_RIGHT)

def test_cubic(self):
from bezier import curve
from bezier import _curve_helpers

self._helper(
3, curve._CUBIC_SUBDIVIDE_LEFT, curve._CUBIC_SUBDIVIDE_RIGHT)
3, _curve_helpers._CUBIC_SUBDIVIDE_LEFT,
_curve_helpers._CUBIC_SUBDIVIDE_RIGHT)

def test_quartic(self):
from bezier import _curve_helpers

expected_l = np.asfortranarray([
[1.0, 0.0, 0.0, 0.0, 0.0],
[1.0, 1.0, 0.0, 0.0, 0.0],
[1.0, 2.0, 1.0, 0.0, 0.0],
[1.0, 3.0, 3.0, 1.0, 0.0],
[1.0, 4.0, 6.0, 4.0, 1.0],
])
expected_r = np.asfortranarray([
[1.0, 4.0, 6.0, 4.0, 1.0],
[0.0, 1.0, 3.0, 3.0, 1.0],
[0.0, 0.0, 1.0, 2.0, 1.0],
[0.0, 0.0, 0.0, 1.0, 1.0],
[0.0, 0.0, 0.0, 0.0, 1.0],
])

row_scaling = np.asfortranarray([[1.0], [2.0], [4.0], [8.0], [16.0]])
expected_l /= row_scaling
expected_r /= row_scaling[::-1, :]

self._helper(4, expected_l, expected_r)


class Test_subdivide_nodes(utils.NumPyTestCase):

@staticmethod
def _call_function_under_test(nodes, degree):
from bezier import _curve_helpers

return _curve_helpers.subdivide_nodes(nodes, degree)

def _helper(self, nodes, degree, expected_l, expected_r):
left, right = self._call_function_under_test(nodes, degree)
self.assertEqual(left, expected_l)
self.assertEqual(right, expected_r)

def _points_check(self, nodes, degree, pts_exponent=5):
from bezier import _curve_helpers

# Using the exponent means that ds = 1/2**exp, which
# can be computed without roundoff.
num_pts = 2**pts_exponent + 1
left, right = self._call_function_under_test(nodes, degree)

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)
main_vals = _curve_helpers.evaluate_multi(nodes, half)
sub_vals = _curve_helpers.evaluate_multi(sub_curve, unit_interval)
self.assertEqual(main_vals, sub_vals)

def test_line(self):
nodes = np.asfortranarray([
[0.0, 1.0],
[4.0, 6.0],
])
expected_l = np.asfortranarray([
[0.0, 1.0],
[2.0, 3.5],
])
expected_r = np.asfortranarray([
[2.0, 3.5],
[4.0, 6.0],
])
self._helper(nodes, 1, expected_l, expected_r)

def test_line_check_evaluate(self):
# Use a fixed seed so the test is deterministic and round
# the nodes to 8 bits of precision to avoid round-off.
nodes = utils.get_random_nodes(
shape=(2, 2), seed=88991, num_bits=8)
self._points_check(nodes, 1)

def test_quadratic(self):
nodes = np.asfortranarray([
[0.0, 1.0],
[4.0, 6.0],
[7.0, 3.0],
])
expected_l = np.asfortranarray([
[0.0, 1.0],
[2.0, 3.5],
[3.75, 4.0],
])
expected_r = np.asfortranarray([
[3.75, 4.0],
[5.5, 4.5],
[7.0, 3.0],
])
self._helper(nodes, 2, expected_l, expected_r)

def test_quadratic_check_evaluate(self):
# Use a fixed seed so the test is deterministic and round
# the nodes to 8 bits of precision to avoid round-off.
nodes = utils.get_random_nodes(
shape=(3, 2), seed=10764, num_bits=8)
self._points_check(nodes, 2)

def test_cubic(self):
nodes = np.asfortranarray([
[0.0, 1.0],
[4.0, 6.0],
[7.0, 3.0],
[6.0, 5.0],
])
expected_l = np.asfortranarray([
[0.0, 1.0],
[2.0, 3.5],
[3.75, 4.0],
[4.875, 4.125],
])
expected_r = np.asfortranarray([
[4.875, 4.125],
[6.0, 4.25],
[6.5, 4.0],
[6.0, 5.0],
])
self._helper(nodes, 3, expected_l, expected_r)

def test_cubic_check_evaluate(self):
# Use a fixed seed so the test is deterministic and round
# the nodes to 8 bits of precision to avoid round-off.
nodes = utils.get_random_nodes(
shape=(4, 2), seed=990077, num_bits=8)
self._points_check(nodes, 3)

def test_dynamic_subdivision_matrix(self):
degree = 4
shape = (degree + 1, 2)
# Use a fixed seed so the test is deterministic and round
# the nodes to 8 bits of precision to avoid round-off.
nodes = utils.get_random_nodes(
shape=shape, seed=103, num_bits=8)
self._points_check(nodes, degree)


class Test__evaluate_multi_barycentric(utils.NumPyTestCase):
Expand Down

0 comments on commit 1fc80e5

Please sign in to comment.