๐ผ๏ธ The classic continuation cartoon, from real dataยถ
Every introduction to homotopy continuation draws the same picture: smooth start points at \(t=1\) on the right, paths flowing left to the targetโs solutions at \(t=0\), past an endgame boundary; the endpoints come in three flavors โ nonsingular, singular, and โat infinityโ (paths that diverge). Here is the hand-drawn version:
This tutorial reproduces that drawing from a real solve โ every curve is genuine tracked data โ with each path styled by the kind of endpoint it reaches. It builds on the observer machinery from ๐ Watching the paths: observers and path data.
A small system with one of each flavorยถ
We want only a handful of paths, with a singular endpoint, a few nonsingular ones, and some that diverge. This system delivers exactly that:
Substituting \(y = x^2\) gives \((x-1)^2 (x+2)(x-4) = 0\): a double root at \(x=1\) โ a singular endpoint reached by two paths โ and simple roots at \(x=-2, 4\) (nonsingular). The total-degree start system has 6 paths, so the remaining two diverge to infinity (the homogenizing coordinate goes to zero).
Collecting the paths, classified by endpointยถ
Attach a SolutionPathCollector to the solver; it captures each path (the whole journey to
\(t \to 0\)). After solving, the per-path solution_metadata() tells us each endpointโs
flavor, so we can style each curve:
def classify(meta):
if not meta.is_finite:
return "infinite"
return "singular" if meta.is_singular else "nonsingular"
def height(df):
"""A fixed GENERIC complex projection of the dehomogenized point down to one real number.
A single coordinate's real part collapses distinct points onto a few heights; mixing all
coordinates with fixed complex weights separates them. ``df`` is one path's samples (columns
``t``, ``z0`` = homogenizing coord, ``z1..`` = the rest).
"""
import numpy as np
hom = df["z{}".format(HOMVAR_INDEX)].to_numpy()
n_aff = sum(c.startswith("z") for c in df.columns) - 1 # affine coords (drop homvar)
with np.errstate(divide="ignore", invalid="ignore"):
proj = np.zeros(len(df), dtype=complex)
for k in range(n_aff):
proj += PROJECTION[k % len(PROJECTION)] * (df["z{}".format(k + 1)].to_numpy() / hom)
return np.real(proj)
SEED = 12 # chosen for a clean picture: start points well separated, finite paths not grazing infinity,
# and BOTH diverging paths leaving upward (so their infinity markers sit above the plot)
def collect(seed=SEED):
"""Solve and return [(flavor, DataFrame), ...], one per path."""
pb.random.set_random_seed(seed)
# total-degree LINEAR-PRODUCT start: its start points are intersections of random linear forms,
# generically separated -- unlike the binomial total-degree start's scaled roots of unity, which
# can land on top of each other under the height projection.
zd = ZeroDimSolver(target_system(), mptype="adaptive", startsystem="linearproduct")
coll = nobs.SolutionPathCollector()
zd.add_observer(coll)
zd.solve()
md = zd.solution_metadata()
out = []
for series in coll.series:
if len(series) == 0:
continue
out.append((classify(md[series.path_index]), series.as_dataframe()))
return out
Drawing itยถ
A few choices turn the data into the cartoon:
height is a fixed generic complex projection of the dehomogenized coordinates down to one real number. A single coordinateโs real part would make distinct points land on top of each other; a generic projection separates them.
the start system is the total-degree linear product (start points are intersections of random linear forms), so the start points on the right are generically separated rather than piling up.
the vertical axis auto-fits the finite paths โ robustly (the central bulk of every finite path, so a path momentarily grazing infinity does not blow up the window), so swapping the system needs no manual axis tweaking. The two paths that diverge to infinity run off the top; each is drawn up to where it leaves the window and cut there, with an \(\infty\) just outside the axis at that exit point (no endpoint marker) โ the cartoonโs โburstโ.
each flavor gets a distinct line style and color and endpoint marker (so it survives grayscale / color-blind viewing): nonsingular solid
โฝ, singular dashedโ, infinite dash-dot.a bullseye marks the target \(f(z)=0\) at \(t=0\), the start points (gold) sit on the right at \(t=1\), and the endgame boundary is drawn at \(t=0.1\).
Run it (needs matplotlib):
python python/docs/source/tutorials/observing_metadata_more/classic_continuation_cartoon/classic_continuation_cartoon.py .
The same shape as the hand drawing โ start points on the right, three flavors of endpoint on the left, two paths funnelling into the single singular point, two diverging to infinity โ but now every wiggle is a real tracked path of an actual homotopy.
Note
The total-degree start points being scaled roots of unity (a structured set, not generic) is
exactly why a naive single-coordinate height collapses them. That structure is a known wart in
the current TotalDegreeLinearProduct start system; a genuinely randomized (linear-product) total-degree
start would put them in general position.