Skip to content

Commit

Permalink
Integrating full_newton() into curve-curve intersection.
Browse files Browse the repository at this point in the history
This mostly "completes" the robustness fix that has been
in flight, but there is still a little bit to resolve. This
fix has introduced spurious (i.e. repeat or very inaccurate)
intersections for curve-curve functional test cases 12, 17, 43 and 44.

The fix for these spurious intersections will likely be to address
issue #109.
  • Loading branch information
dhermes committed Mar 2, 2018
1 parent 7e9030d commit fe453c3
Show file tree
Hide file tree
Showing 20 changed files with 3,463 additions and 3,219 deletions.
53 changes: 34 additions & 19 deletions docs/curve-curve-intersection.rst
Original file line number Diff line number Diff line change
Expand Up @@ -218,9 +218,13 @@ with zero error:
... ])
>>> curve2 = bezier.Curve(nodes2, degree=2)
>>> intersections = curve1.intersect(curve2)
Traceback (most recent call last):
...
NotImplementedError: Parameters need help.
>>> intersections
array([[0.5 , 0.5 , 0.8333..., 0.8333...],
[0.16666..., 0.16666..., 0.5 , 0.5 ]])
>>> s_vals = np.asfortranarray(intersections[0, :])
>>> curve1.evaluate_multi(s_vals)
array([[0.375 , 0.375 , 0.625 , 0.625 ],
[0.46875, 0.46875, 0.46875, 0.46875]])

.. image:: images/curves14_and_16.png
:align: center
Expand Down Expand Up @@ -550,9 +554,13 @@ Intersections at Endpoints
... ])
>>> curve2 = bezier.Curve(nodes2, degree=2)
>>> intersections = curve1.intersect(curve2)
Traceback (most recent call last):
...
NotImplementedError: Parameters need help.
>>> intersections
array([[0.333...],
[1. ]])
>>> s_vals = np.asfortranarray(intersections[0, :])
>>> curve1.evaluate_multi(s_vals)
array([[3.],
[4.]])

.. image:: images/curves10_and_17.png
:align: center
Expand Down Expand Up @@ -634,14 +642,20 @@ successfully terminates
.. image:: images/curves1_and_6.png
:align: center

However this library mostly avoids (for now) computing tangent
intersections. For example, the curves
This library makes an earnest effort to compute tangent intersections.
For example, when the curves

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

have a tangent intersection that this library fails to
compute:
have been subdivided and approximated by lines, the corresponding
segments are parallel, hence don't intersect. At this point, this library
detects the problematic intersection point and switches to a more robust
Newton's method that is built to handle the numerical issue caused by
the double root.

Unlike the first tangent example, this intersection occurs at parameters
which are not **exact** floating point numbers:

.. doctest:: intersect-14-15
:options: +NORMALIZE_WHITESPACE
Expand All @@ -656,16 +670,17 @@ compute:
... [0.625, 0.25 , 1.0],
... ])
>>> curve2 = bezier.Curve(nodes2, degree=2)
>>> curve1.intersect(curve2)
Traceback (most recent call last):
...
NotImplementedError: Parameters need help.

This failure comes from the fact that the linear approximations
of the curves near the point of intersection are parallel.
>>> intersections = curve1.intersect(curve2)
>>> intersections
array([[0.6666...],
[0.3333...]])
>>> s_vals = np.asfortranarray(intersections[0, :])
>>> curve1.evaluate_multi(s_vals)
array([[0.5],
[0.5]])

As above, we can find some cases where tangent intersections
are resolved:
See another case where one parameter is an exact floating point
number and the other is not:

.. doctest:: intersect-10-23
:options: +NORMALIZE_WHITESPACE
Expand Down
2 changes: 1 addition & 1 deletion nox.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ def lint(session):
'--disable=missing-docstring',
'--disable=protected-access',
'--disable=too-many-public-methods',
'--max-module-lines=2294',
'--max-module-lines=2368',
get_path('tests'),
)

Expand Down
32 changes: 16 additions & 16 deletions src/bezier/_geometric_intersection.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,6 @@
_NO_CONVERGE_TEMPLATE = (
'Curve intersection failed to converge to approximately linear '
'subdivisions after {:d} iterations.')
# Allow wiggle room for ``s`` and ``t`` computed during segment
# intersection. Any over- or under-shooting will (hopefully) be
# resolved in the Newton refinement step. If it isn't resolved, the
# call to _wiggle_interval() will fail the intersection.
_WIGGLE_START = -0.5**16
_WIGGLE_END = 1.0 - _WIGGLE_START
# Number of bits allowed in ``add_intersection()`` to consider two
# intersections to be "identical".
_SIMILAR_ULPS = 1
Expand Down Expand Up @@ -763,7 +757,6 @@ def from_linearized(first, second, intersections):
of ``0.0`` (i.e. they are both lines). This is because this
function expects the caller to have used :func:`check_lines`
already.
NotImplementedError: If the segment intersection fails.
"""
# pylint: disable=too-many-return-statements
s, t, success = segment_intersection(
Expand All @@ -784,19 +777,26 @@ def from_linearized(first, second, intersections):
t = 0.5

if do_full_newton:
if convex_hull_collide(first.curve.nodes, second.curve.nodes):
raise NotImplementedError('Parameters need help.')
else:
# In the unlikely case that we have parallel segments or segments
# that intersect outside of [0, 1] x [0, 1], we can still exit
# if the convex hulls don't intersect.
if not convex_hull_collide(first.curve.nodes, second.curve.nodes):
return

# Now, promote ``s`` and ``t`` onto the original curves.
orig_s = (1 - s) * first.curve.start + s * first.curve.end
orig_t = (1 - t) * second.curve.start + t * second.curve.end
# Perform one step of Newton iteration to refine the computed
# values of s and t.
refined_s, refined_t = _intersection_helpers.newton_refine(
orig_s, first.curve.original_nodes,
orig_t, second.curve.original_nodes)
if do_full_newton:
refined_s, refined_t = _intersection_helpers.full_newton(
orig_s, first.curve.original_nodes,
orig_t, second.curve.original_nodes)
else:
# Perform one step of Newton iteration to refine the computed
# values of s and t.
refined_s, refined_t = _intersection_helpers.newton_refine(
orig_s, first.curve.original_nodes,
orig_t, second.curve.original_nodes)

refined_s, success = _helpers.wiggle_interval(refined_s)
if not success:
return # pragma: NO COVER
Expand Down Expand Up @@ -1404,7 +1404,7 @@ def _all_intersections(nodes_first, nodes_second):
# mitigations. As a result, there is no unit test
# to trigger this line (no case has been discovered
# yet).
raise NotImplementedError( # pragma: NO COVER
raise NotImplementedError(
_TOO_MANY_TEMPLATE.format(len(candidates)))
else:
intersections = params
Expand Down
5 changes: 3 additions & 2 deletions src/bezier/_intersection_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -741,7 +741,8 @@ def full_newton_nonzero(s, nodes1, t, nodes2):
Newton's method converged to.
Raises:
ValueError: If Newton's method doesn't converge.
NotImplementedError: If Newton's method doesn't converge in either the
multiplicity 1 or 2 cases.
"""
# NOTE: We somewhat replicate code in ``evaluate_hodograph()``
# here. This is so we don't re-compute the nodes for the first
Expand Down Expand Up @@ -770,7 +771,7 @@ def full_newton_nonzero(s, nodes1, t, nodes2):
if converged:
return current_s, current_t

raise ValueError(NEWTON_NO_CONVERGE)
raise NotImplementedError(NEWTON_NO_CONVERGE)


def full_newton(s, nodes1, t, nodes2):
Expand Down
Loading

0 comments on commit fe453c3

Please sign in to comment.