๐ŸŽจ The continuation cartoon, from real data โ€” and seeing precision changeยถ

Every introduction to homotopy continuation draws the same cartoon: smooth start points on the right at \(t=1\), paths flowing left to the solutions of the target at \(t=0\), past an endgame boundary, with a few paths shooting off to infinity. It is a drawing.

This tutorial makes that picture from a real solve โ€” and uses the same machinery to see the one thing the cartoon never shows: where an adaptive-precision tracker decides double precision is no longer enough and raises its working precision. It builds directly on ๐Ÿ‘€ Watching the paths: observers and path data (read that first for how observers and PathDataCollector work).

The ideaยถ

We solve cyclic-5 with the adaptive-precision Cauchy solver and attach a SolutionPathCollector. That meta-observer attaches a fresh PathDataCollector to whichever tracker actually runs each path, and harvests the whole journey to \(t \to 0\) (endgame included) into one pandas DataFrame per path. Each row is one accepted step, with the time, the space point, and the per-step diagnostics condition_number and precision.


# we are here to WATCH tracking: with ambient recording on, a repeated identical run
# would RECALL instead of tracking, and the observers would collect nothing to draw
pb.recording(False)


N = 5
ENDGAME_BOUNDARY = 0.1   # default t at which the endgame begins
HOMVAR_INDEX = 0         # homogenize() prepends the homogenizing coordinate


def cyclic_system(n):
    """The cyclic-n system (same construction as examples/solve_cyclic.py)."""
    x = [pb.Variable('x{}'.format(i)) for i in range(n)]
    w = x + x
    sys = pb.System()
    for length in range(1, n):
        sys.add_function(np.sum([np.prod(w[start:start + length]) for start in range(n)]))
    sys.add_function(np.prod(x) - 1)
    sys.add_variable_group(pb.VariableGroup(x))
    return sys


def collect_paths():
    """Solve cyclic-5 with AMP+Cauchy, capturing every solution path via an observer.

    Uses the purpose-built SolutionPathCollector: attached to the SOLVER, it grabs the tracker that
    actually runs each path (clone-safe under threading) and collects the whole journey to t -> 0,
    endgame included -- one PathDataCollector per solution path.  Returns a list of per-path
    DataFrames.
    """
    pb.random.set_random_seed(1)   # deterministic homotopy
    solver = pb.ZeroDimSolver(cyclic_system(N), endgame='cauchy', mptype='adaptive', startsystem='binomial')

    collector = pb.SolutionPathCollector()
    solver.add_observer(collector)
    solver.solve()

    return [s.as_dataframe() for s in collector.series if len(s) > 0]

Each pathโ€™s DataFrame carries everything the picture needs:

>>> paths = collect_paths()
>>> paths[0].columns.tolist()
['t', 'z0', 'z1', 'z2', 'z3', 'z4', 'z5', 'abs_t', 'condition_number', 'precision', 'stepsize']

Drawing itยถ

We plot, for every path:

  • x = \(\log_{10}|t|\), so the start (\(t=1\)) is on the right and the target (\(t \to 0\)) is on the left โ€” logarithmic time, because all the interesting structure is near \(t=0\);

  • height = the real part of a dehomogenized coordinate. Paths weave and fan out to the distinct endpoints; a path going to infinity has its homogenizing coordinate go to zero, so the dehomogenized height shoots off โ€” that is the cartoonโ€™s โ€œinfinite endpointsโ€, straight from data (the height axis is symlog so those are visible alongside the finite ones);

  • color = the condition number along the path (log scale);

  • a vertical line at the endgame boundary (\(t = 0.1\));

  • a star wherever the tracker raised its working precision.

The full plotting routine is in the example script; the heart of it is coloring each path by its condition number with a LineCollection and scattering a marker at each precision increase:

        pts = np.array([x, y]).T.reshape(-1, 1, 2)
        segs = np.concatenate([pts[:-1], pts[1:]], axis=1)
        lc = LineCollection(segs, cmap=cmap, norm=norm, linewidths=1.0, alpha=0.75)
        lc.set_array(np.clip(cond[:-1], norm.vmin, norm.vmax))
        ax.add_collection(lc)

        inc = np.where(np.diff(prec) > 0)[0] + 1   # indices where precision rose
        esc_x.extend(x[inc]); esc_y.extend(y[inc])

Run the whole thing (needs matplotlib and pandas):

python python/docs/source/tutorials/homotopy_cartoon_from_real_data/amp_precision_cartoon.py .
cyclic-5 homotopy paths from real data, colored by condition number, with precision-change markers

Reading the pictureยถ

  • On the right (near \(t=1\)) the paths emerge from a tight cluster of start points and are uniformly dark โ€” low condition number, double precision, no stars. Pre-endgame tracking is easy.

  • Color rises (toward yellow) as paths approach \(t=0\), and the precision-raise stars appear almost entirely to the left of the endgame boundary โ€” precision tracks conditioning, and the hard conditioning lives in the endgame.

  • The paths that diverge to infinity (homogenizing coordinate \(\to 0\)) are the highest-condition ones (bright) and shoot to the top/bottom of the height axis; they legitimately need multiprecision before the solver truncates them.

  • The finite, well-conditioned paths โ€” the ones whose endpoints are the actual cyclic-5 solutions โ€” stay dark and starless essentially all the way in.

That last point is the payoff of a specific fix. Before it (ADR-0038), the cost model fed a predictor error-proportionality constant (size_proportion, which blows up to ~\(10^{51}\) in the endgame roundoff regime) into AMP Criterion B as if it were the latest Newton residual, and forced 39 of the 70 finite paths into multiprecision even though all 70 converge in pure double. With Criterion B reading the actual Newton residual instead, almost every finite path now stays in double โ€” visible here as the near-absence of stars on the well-conditioned paths. The same observer machinery that drew this picture is exactly how that diagnosis was made: attach, collect, look.

Note

This is real data, so it is messier than the textbook drawing โ€” the endgameโ€™s Cauchy sample circles make \(|t|\) wander rather than march monotonically to zero, which shows up as the vertical excursions on the left. That messiness is the point: it is what continuation actually does. For a deliberately clean, cartoon-faithful recreation (one singular endpoint, several nonsingular, a couple diverging, styled by endpoint type) see the model drawing in doc_resources/images/homotopycontinuation_generic.png.