# LivePlotting_10: Trick Out Your Instrument > [!abstract] The project in one line > Take the plotting-and-root-finding script our class built live over nine sessions, harden it into an instrument that states what it promises and proves it on demand, then extend it toward a goal you choose and defend with evidence. ## Introduction For multiple lectures, you watched one script grow. `LivePlotting_1` sampled a function and drew it. By `LivePlotting_5` it had anonymous functions, a quadratic Taylor root finder, and a title. By `LivePlotting_9` it carried finite-difference derivatives, [Newton's method](https://en.wikipedia.org/wiki/Newton%27s_method) with a stopping rule built on [machine epsilon](https://en.wikipedia.org/wiki/Machine_epsilon), and nine layers of commented-out history. It works. It is also a pile, and, quietly, it is wrong: run the Day-2 sign-change detector on our own class function $f(x) = x^2(x+1)(x-1)^3$ and it reports two roots. The function has three. Run the same scan on $f'$ and it finds three critical points. There are four. Professional numerical software does not get to be a pile, and it does not get to be quietly wrong. A library like the one behind `fzero` or `scipy.optimize` is an *instrument*: it states a contract (what it finds, to what accuracy, with what evidence), it ships with a [test suite](https://en.wikipedia.org/wiki/Unit_testing) that would catch the two failures above, and its figure output is a claim backed by numbers, not decoration. The gap between `LivePlotting_9` and an instrument is not more mathematics. It is engineering: [invariants](https://en.wikipedia.org/wiki/Invariant_(computer_science)), tests, honest reporting, and a design that survives functions it has never seen. This project closes that gap, then hands you the wheel. Everyone brings the class script up to the same base contract against a shipped test suite. Then you pick a direction and trick it out: a smarter seeding algorithm, a hardened multiplicity-aware core, a two-dimensional version, a performance pass, or an interface a stranger could use, or you pitch your own goal. The project is deliberately more programming and systems logic than derivation. The mathematics you need, you mostly already own. ![The Day-2 detector is blind to even multiplicity](Media/liveplot10_blindspots.png) ## Project Description The work moves in the usual four passes, worn engineering-style. First, the contract: three short results (why the sign scan goes blind, how Newton's step ratio reveals [multiplicity](https://en.wikipedia.org/wiki/Multiplicity_(mathematics)), where the finite-difference width should sit) worked by hand to checkable numbers. Second the mirror: your code reproduces those hand numbers, including the documented failures of the naive baseline. Third the build: the class script becomes `analyzeFunction(f, a, b)`, brought up to the contract against the shipped test suite. Fourth, your goal: a trick-out track chosen from the menu (or proposed in the open lane), scoped at Milestone 1, delivered with evidence. **Foundation task:** derive the three contract results by hand (blindness product, contraction ratios $\tfrac12$ and $\tfrac23$, the width $h^*$), then verify that your code reproduces every hand number through the `same()` helper, failures included. **Application task:** refactor the arc code into the instrument, meet the base contract on the shipped test suite (13 catalogued zeros across four functions: completeness 13/13, soundness PASS), and render the dashboard figure in which every marker is backed by a diagnostics row. **Key deliverable:** your trick-out. One track from the menu below or an approved proposal of your own, delivered as working code plus the evidence that its stated contract holds, and defended in your write-up from the diagnostics, not from effort. Two companions support the milestone forms: the [[MATH307Su26 - LivePlotting - Trick Out Your Instrument (Milestone Map)|Milestone Map]] says which artifact from this handout answers each form field, and the [[MATH307Su26 - LivePlotting_10 - Trick Out Your Instrument (Verification Table)|Verification Table]] is filled in as you verify and submitted with Milestone 2. ## Mathematical Background: the contract, and the three small results it needs **Idea.** An instrument is trustworthy when it states what it promises and can prove, on demand, that the promise held. For a zero-finder the promise has two halves, borrowed from logic: [soundness](https://en.wikipedia.org/wiki/Soundness) (everything reported is real) and [completeness](https://en.wikipedia.org/wiki/Completeness_(logic)) (everything real is reported). "It ran" is neither. $\boxed{\begin{aligned} &\textbf{The base contract for } \texttt{analyzeFunction}(f, a, b):\\[2pt] &\text{I1 (sound): every reported location } r \text{ satisfies } a \le r \le b \text{ and } |f(r)| \le \tau_{\text{res}},\\ &\qquad\ \text{and every marker on the figure has a row in the diagnostics table.}\\ &\text{I2 (complete): every catalogued zero in } [a,b] \text{ is reported within } \tau_{\text{loc}} = 10^{-3}.\\ &\text{I3 (honest): every report carries its evidence: residual, iteration count,}\\ &\qquad\ \text{step ratio, multiplicity estimate } \hat m, \text{ and a classification or an explicit "inconclusive".} \end{aligned}}$ The suite ships with the catalogs, so I2 is checkable. Three small results, each one Taylor-series long, tell you what the instrument is up against. **Result 1 (why the Day-2 scan goes blind).** Near a zero $r$ of multiplicity $m$, Taylor gives $f(x) \approx C\,(x-r)^m$ with $C = f^{(m)}(r)/m!$. If $m$ is odd, $f$ changes sign across $r$ and the [intermediate value theorem](https://en.wikipedia.org/wiki/Intermediate_value_theorem) scan sees it. If $m$ is even, $f$ has the *same* sign on both sides and the scan sees nothing. Our class function has $f(x) \approx -x^2$ near $0$ (take $x^2 \cdot (x+1)(x-1)^3 \to x^2 \cdot 1 \cdot (-1)$), so by hand $f(-0.1) = -0.011979, \qquad f(0.1) = -0.008019, \qquad f(-0.1)\,f(0.1) = 9.606 \times 10^{-5} > 0,$ and the scan walks straight past a root. The same argument applied to $f'$ explains the missing critical point: $x=1$ is a *double* zero of $f'$ (because it is a triple zero of $f$), so the critical-point scan is blind there too. One cause, two failures. **Result 2 (Newton's step ratio reads the multiplicity).** Apply Newton to the pure power $f(x) = C(x-r)^m$. The update is $x_{k+1} = x_k - \frac{C(x_k-r)^m}{mC(x_k-r)^{m-1}} = x_k - \frac{x_k - r}{m}, \qquad\text{so}\qquad e_{k+1} = \Big(1 - \frac{1}{m}\Big)\,e_k$ exactly, where $e_k = x_k - r$. Simple roots ($m=1$) collapse superlinearly, the quadratic cliff from Day 7. Multiple roots tiptoe in *linearly* with ratio $1 - 1/m$: a double root contracts by $\tfrac12$ per step, a triple root by $\tfrac23$. Reading the measured ratio $\rho$ backwards gives a multiplicity meter, $\boxed{\;\hat m = \operatorname{round}\!\Big(\frac{1}{1-\rho}\Big),\;}$ and knowing $m$ repairs the tiptoe: the modified step $x_{k+1} = x_k - m\,f(x_k)/f'(x_k)$ lands the pure power in one step and restores fast convergence in general. There is a hard limit, though. Near a multiple root $|f| \approx |C|\,e^m$ sinks below the roundoff floor $\tau_f \approx 10^{-14}$ while $e$ is still large, so *no* method can locate the root better than $e_{\text{floor}} \approx \Big(\frac{\tau_f}{|C|}\Big)^{1/m},$ about $10^{-7}$ for our double root ($C=-1$) and $1.7 \times 10^{-5}$ for our triple root ($f \approx 2(x-1)^3$ near $1$, so $C=2$). Invariant I3 exists precisely so the instrument admits this instead of printing sixteen confident digits. **Result 3 (where the stencil width should sit).** The 3-point stencil's total error balances truncation against roundoff, exactly as on Day 8: $E(h) \approx \frac{M_3}{6}h^2 + \frac{2\epsilon\,|f|}{h}, \qquad E'(h^*) = 0 \;\Rightarrow\; \boxed{\;h^* = \Big(\frac{6\,\epsilon\,|f|}{M_3}\Big)^{1/3}\;}$ with $M_3$ a bound on $|f'''|$ and $\epsilon \approx 2.22 \times 10^{-16}$. At $x = 0.5$ our class function has $|f(0.5)| = 0.046875$ and $M_3 = |f'''(0.5)| = |15 - 30 + 12| = 3$, so $h^* = 2.751 \times 10^{-6}$. The `h = 10^-6` that sat in the live code all arc was in the right decade the whole time; now you can prove it. > [!note] Where this comes from > Nothing here is new machinery. Result 1 is the Day-2 scan read through Taylor's lens, Result 2 is the Day-7 convergence analysis pushed one case further, and Result 3 is the Day-8 truncation-vs-roundoff balance. The new content is the contract wrapped around them. ### Worked example, run to completion Collect the hand numbers your code must reproduce in Step 1: 1. Blindness product: $f(-0.1)\,f(0.1) = (-0.011979)(-0.008019) = 9.606 \times 10^{-5} > 0$; the scan misses $x=0$. 2. Predicted contraction ratios: $m=2 \Rightarrow \rho = 0.5$; $m=3 \Rightarrow \rho = 0.6667$. Multiplicity meter check: $\rho = 0.5 \Rightarrow \hat m = \operatorname{round}(1/0.5) = 2$; $\rho = 0.6667 \Rightarrow \hat m = \operatorname{round}(1/0.3333) = 3$. 3. Accuracy floors: double root $e_{\text{floor}} \approx (10^{-14}/1)^{1/2} = 10^{-7}$; triple root $e_{\text{floor}} \approx (10^{-14}/2)^{1/3} = 1.7 \times 10^{-5}$. 4. Stencil width: $h^* = \big(6 \cdot 2.22 \times 10^{-16} \cdot 0.046875 / 3\big)^{1/3} = 2.751 \times 10^{-6}$. Every one of these appears in a code step below, checked through `same()`. ## Implementation Guidelines The workflow is the same in every language, so we state it once and then implement it step by step. > [!abstract] The workflow (state it once, run it in any language) > Given $f$ on $[a,b]$ and the contract (I1, I2, I3): > 1. **Warm up**: define `same()`, then reproduce the four hand numbers from the worked example. > 2. **Stencils and polisher**: port `fPrime`/`fPrimePrime` as $D_h f(x) = \frac{f(x+h)-f(x-h)}{2h}$ and the 5-point second difference; wrap Newton with the $\text{eps}(x)$ stopping rule into `newton_polish`; mirror two class runs and measure the ratios $\rho$. > 3. **The detector**: seeds = sign-change midpoints PLUS the $n$ grid points of smallest $|f|$ (the LivePlotting_7 rescue; this is what catches even multiplicity); polish every seed; gate by window and residual (I1); deduplicate; estimate $\hat m = \operatorname{round}(1/(1-\rho))$; classify critical points by $f''$ with an explicit "inconclusive" branch (I3). > 4. **The test suite**: score completeness and soundness against the shipped catalogs (I2); score the naive baseline too, and watch it fail. > 5. **The width sweep**: measure $E(h)$ across twelve decades and confirm the minimum sits in $h^*s decade. > 6. **The dashboard**: one figure; every marker backed by a diagnostics row; legend built from what was actually found. Pick your language below; each callout contains the steps in order, run top to bottom. Code is four-language and portable for the BASE contract (Python is the run-verified reference; Mathematica is numerical only, no symbolic derivatives anywhere, that is the point). Your trick-out track (later section) is built in ONE language of your choice. Nothing here needs a download; the test suite is analytic and lives in the code. > [!example]- Python (reference) > > **Step 1: define the helper, then warm up on the hand examples.** `same()` is the only judge in this project; every claim runs through it. > ```python > import numpy as np > EPS = np.finfo(float).eps > > def same(name, X, Y, tol=1e-8): > diff = np.max(np.abs(np.asarray(X, float) - np.asarray(Y, float))) > print(f"same({name}): max diff = {diff:.3e} ->", > "OK" if diff <= tol else "CHECK THIS") > > f = lambda x: x**2 * (x + 1) * (x - 1)**3 # the class function > > print(f(-0.1), f(0.1), f(-0.1) * f(0.1) > 0) # blindness product > print(1 - 1/2, 1 - 1/3) # predicted ratios > h_star = (6 * EPS * abs(f(0.5)) / 3) ** (1/3) # M3 = |f'''(0.5)| = 3 > print(h_star) > ``` > *Look for:* `-0.011979... -0.008019... True`, ratios `0.5` and `0.6666...`, and `h_star = 2.751e-06`, the worked-example numbers exactly. > > **Step 2: stencils and the polisher; mirror the class runs.** `newton_polish` is LivePlotting_9's loop with a name: FD derivative, step update, stop when the step falls below the local spacing `np.spacing(x)` or the residual reaches its noise floor `gtol` (pass a looser `gtol` when the target is itself a finite difference; an FD-sampled function bottoms out near $\epsilon|f|/h$). > ```python > def fd1(g, x, h): # fPrime, middle read > return (g(x + h) - g(x - h)) / (2 * h) > > def fd2(g, x, h): # fPrimePrime, middle read > return (-g(x-2*h) + 16*g(x-h) - 30*g(x) + 16*g(x+h) - g(x+2*h)) / (12*h**2) > > def newton_polish(g, x0, h=1e-6, maxit=200, gtol=1e-13): > x, steps = float(x0), [] > for k in range(1, maxit + 1): > d = fd1(g, x, h) > if d == 0.0: break > step = g(x) / d; x -= step; steps.append(abs(step)) > if abs(step) <= np.spacing(x) or abs(g(x)) < gtol: > return x, k, steps > return x, maxit, steps > > def measured_ratio(g, x0, n=14, h=1e-6, floor=1e-7): > x, steps = float(x0), [] > for _ in range(n): > d = fd1(g, x, h) > if d == 0.0: break > step = g(x) / d; x -= step; steps.append(abs(step)) > r = np.array(steps) > keep = (r[:-1] > floor) & (r[1:] > floor) > ratios = r[1:][keep] / r[:-1][keep] > if len(ratios) < 2: return 0.0 # superlinear collapse: simple root > return float(np.median(ratios[-5:] if len(ratios) >= 5 else ratios)) > > print(newton_polish(f, -0.75)[:2]) # the LivePlotting_9 start > print(newton_polish(lambda t: fd1(f, t, 1e-6), 2.0, h=1e-4, gtol=1e-9)[:2]) > rho2 = measured_ratio(f, 0.20); rho3 = measured_ratio(f, 1.30) > same("double-root ratio vs 1/2", rho2, 0.5, tol=0.02) > same("triple-root ratio vs 2/3", rho3, 2/3, tol=0.02) > > x = 1.30 # the repair: knowing m fixes the tiptoe > for k in range(1, 61): > step = 3 * f(x) / fd1(f, x, 1e-6); x -= step > if abs(step) < 1e-8: break > print(f"modified Newton (m=3): {x} in {k} iterations") > ``` > *Look for:* the $-0.75$ start launches off the flat spot and lands at `1.0000325...` after 28 iterations (seeding matters); the critical-point run from $2$ lands at `1.0000114...` after 19 iterations, a quiet tiptoe because $x=1$ is a double zero of $f'$; both `same()` checks OK with `rho2 = 0.4999`, `rho3 = 0.6699`; from $1.30$ plain Newton needs 23 iterations, the modified step lands in 5. > > **Step 3: the detector, the multiplicity meter, and the classifier.** Seeds from sign changes alone reproduce the Day-2 blindness; adding the smallest-$|f|$ grid points is the whole repair. Every candidate is polished, gated (I1), deduplicated, measured ($\hat m$), and classified with an explicit inconclusive branch (I3). > ```python > def estimate_multiplicity(g, r, offset=0.01, n=14, h=1e-6): > rho = measured_ratio(g, r + offset, n=n, h=h) > if not np.isfinite(rho) or rho < 0.2: return 1, rho > return int(round(1 / (1 - rho))), rho > > def detect(g, a, b, N=70, n_extra=10, res_tol=1e-8, match_tol=1e-3, gtol=1e-13): > xg = np.linspace(a, b, N); yg = g(xg) > seeds = [(xg[i]+xg[i+1])/2 for i in range(N-1) if yg[i]*yg[i+1] < 0] > seeds += list(xg[np.argsort(np.abs(yg))[:n_extra]]) > found = [] > for s in seeds: > r, it, _ = newton_polish(g, s, gtol=gtol) > if a <= r <= b and abs(g(r)) <= res_tol: # soundness gate > if all(abs(r - q["x"]) > match_tol for q in found): > found.append({"x": r, "res": abs(g(r)), "it": it}) > return sorted(found, key=lambda q: q["x"]) > > fp_num = lambda t: fd1(f, t, 1e-6) > roots = detect(f, -2, 2) > cps = detect(fp_num, -2, 2, res_tol=1e-6, gtol=1e-9) > for q in roots: > q["m"], q["rho"] = estimate_multiplicity(f, q["x"]) > print("root", q) > for q in cps: > v = fd2(f, q["x"], 1e-4) > q["cls"] = ("local min" if v > 1e-3 else > "local max" if v < -1e-3 else "DEGENERATE (test inconclusive)") > print("crit pt", q["x"], q["cls"], f"f''={v:+.4f}") > ``` > *Look for:* three roots ($-1$ with $\hat m=1$, $\approx 2\times10^{-7}$ with $\hat m=2$, $\approx 0.99997$ with $\hat m=3$) and four critical points, with $x \approx 0.4343$ and $-0.7676$ as local minima, $0$ as a local max, and $x \approx 1$ flagged DEGENERATE, the row the naive scan never had. Note the locations of the multiple roots carry exactly the accuracy floors from the worked example ($2.1\times10^{-7}$ and $3.3\times10^{-5}$): the instrument is at the wall, not being sloppy. > > **Step 4: the test suite.** Four functions, catalogs shipped in the code, contract scored. Then score the Day-2 baseline and watch I2 fail. > ```python > SUITE = [ > ("T1 class", f, [-1.0, 0.0, 1.0]), > ("T2 quintic", lambda x: (x+1.5)*(x+.5)*(x-.3)*(x-1.1)*(x-1.7), [-1.5,-0.5,0.3,1.1,1.7]), > ("T3 edge", lambda x: (x-1.95)*(x+0.25)**2, [-0.25, 1.95]), > ("T4 sin(3x)", lambda x: np.sin(3*x), [-np.pi/3, 0.0, np.pi/3]), > ] > tot = hit = 0 > for name, g, cat in SUITE: > got = detect(g, -2, 2) > h_ = sum(any(abs(q["x"]-r) <= 1e-3 for q in got) for r in cat) > ghosts = sum(not any(abs(q["x"]-r) <= 1e-3 for r in cat) for q in got) > sound = all(abs(g(q["x"])) <= 1e-8 for q in got) and ghosts == 0 > tot += len(cat); hit += h_ > print(f"{name}: completeness {h_}/{len(cat)} ghosts {ghosts} " > f"soundness {'PASS' if sound else 'FAIL'}") > print(f"SUITE TOTAL: {hit}/{tot}") > > def naive_scan(g, a, b, N=70): > xg = np.linspace(a, b, N); yg = g(xg) > return [xg[i] - yg[i]*(xg[i+1]-xg[i])/(yg[i+1]-yg[i]) > for i in range(N-1) if yg[i]*yg[i+1] < 0] > print("naive on T1 finds:", [round(float(v), 4) for v in naive_scan(f, -2, 2)]) > ``` > *Look for:* `completeness 3/3, 5/5, 2/2, 3/3`, `ghosts 0`, `soundness PASS` on all four, `SUITE TOTAL: 13/13`. The naive scan returns `[-0.9977, 1.0121]`: two of three, the documented failure your Milestone 2 form cites. > > **Step 5: the width sweep.** Day 8's tradeoff, measured on your own function. > ```python > fps = lambda x: 6*x**5 - 10*x**4 + 6*x**2 - 2*x # analytic f', checks only > hs = np.logspace(-12, -1, 45) > errs = np.array([abs(fd1(f, 0.5, h) - fps(0.5)) for h in hs]) > print(f"best h = {hs[np.argmin(errs)]:.3e}, error {errs.min():.3e}, hand h* = {h_star:.3e}") > ``` > *Look for:* `best h = 1.778e-06` with error `1.264e-12`, the same decade as the hand $h^* = 2.751 \times 10^{-6}$. > > **Step 6: the dashboard.** One figure, course conventions kept (origin-crossing axes, $[-2,2]$ window, black $f$, blue $f'$, red $f''$), and three upgrades the arc kept postponing: a LaTeX title, a legend built dynamically from what the detector actually reported, and classification carried by marker SHAPE as well as color so the encoding survives grayscale printing and colorblind readers. > ```python > import matplotlib.pyplot as plt > OI = {"orange":"#E69F00","sky":"#56B4E9","green":"#009E73","purple":"#CC79A7"} > xx = np.linspace(-2, 2, 800) > fig, ax = plt.subplots(figsize=(9, 5.6)) > hs_ = [ax.plot(xx, f(xx), "-k", lw=1.6, label=r"$f(x)=x^2(x+1)(x-1)^3quot;)[0], > ax.plot(xx, [fd1(f, v, 1e-6) for v in xx], "-b", lw=1.1, label=r"$f'$ (3-point FD)")[0], > ax.plot(xx, [fd2(f, v, 1e-4) for v in xx], "-r", lw=1.1, label=r"$f''$ (5-point FD)")[0]] > ax.set_ylim(-2, 2) > ax.spines["left"].set_position("zero"); ax.spines["bottom"].set_position("zero") > ax.spines["top"].set_visible(False); ax.spines["right"].set_visible(False) > ax.grid(True, alpha=0.25, lw=0.5) > show = lambda v: f"{(0.0 if abs(v) < 1e-4 else v):.4g}" # display rounding only > for q in roots: > hs_.append(ax.plot(q["x"], f(q["x"]), "*", ms=15, mfc=OI["orange"], mec="k", > label=f"root $x={show(q['x'])}$ ($\\hat m={q['m']}$)")[0]) > mk = {"local min": ("^", OI["green"]), "local max": ("v", OI["sky"]), > "DEGENERATE (test inconclusive)": ("s", OI["purple"])} > for q in cps: > s_, c_ = mk[q["cls"]] > hs_.append(ax.plot(q["x"], f(q["x"]), s_, ms=9, mfc=c_, mec="k", > label=f"{q['cls'].lower()} $x={show(q['x'])}quot;)[0]) > ax.legend(handles=hs_, fontsize=7.5, loc="upper left", ncol=2) > ax.set_xlabel("$xquot;); ax.set_ylabel("$yquot;) > ax.set_title(r"analyzeFunction($f$, $-2$, $2$): every marker is backed by a diagnostics row") > fig.tight_layout(); fig.savefig("liveplot10_dashboard.png", dpi=150) > ``` > *Look for:* your figure matches the shipped dashboard below (the shipped version adds an inset of the convergence traces; an inset is optional polish): seven markers, four shapes, a two-column legend that lists exactly what was found, and nothing on the plot that is missing from the printed table. > [!example]- MATLAB > > MATLAB requires local functions at the END of a script file: paste the *functions block* from Step 2 at the bottom, then run the script top to bottom. `eps(x)` is the local-spacing stopping rule (LivePlotting_9's `eps(x)` line, now doing contract duty). > > **Step 1: define the helper, then warm up on the hand examples.** > ```matlab > f = @(x) x.^2.*(x+1).*(x-1).^3; % the class function > fprintf('%.6f %.6f %d\n', f(-0.1), f(0.1), f(-0.1)*f(0.1) > 0) > fprintf('predicted ratios: %.4f %.4f\n', 1-1/2, 1-1/3) > h_star = (6*eps*abs(f(0.5))/3)^(1/3) % M3 = |f'''(0.5)| = 3 > ``` > *Look for:* `-0.011979 -0.008019 1`, ratios `0.5000 0.6667`, `h_star = 2.7509e-06`. > > **Step 2: stencils and the polisher; mirror the class runs.** The functions block below is `fPrime`/`fPrimePrime` with the middle read built in, plus the polisher and the ratio meter. > ```matlab > [xr, itr] = newton_polish(f, -0.75, 1e-6, 200, 1e-13); > fprintf('from -0.75: x = %.10f after %d iterations\n', xr, itr) > fpn = @(t) fd1(f, t, 1e-6); > [xc, itc] = newton_polish(fpn, 2.0, 1e-4, 200, 1e-9); > fprintf('on f'' from 2: c = %.10f after %d iterations\n', xc, itc) > rho2 = measured_ratio(f, 0.20); rho3 = measured_ratio(f, 1.30); > same('double-root ratio vs 1/2', rho2, 0.5, 0.02) > same('triple-root ratio vs 2/3', rho3, 2/3, 0.02) > > x = 1.30; % the repair: knowing m fixes the tiptoe > for k = 1:60 > st = 3*f(x)/fd1(f, x, 1e-6); x = x - st; > if abs(st) < 1e-8, break; end > end > fprintf('modified Newton (m=3): %.10f in %d iterations\n', x, k) > > % ---- functions block: paste at the END of the script file ---- > function same(name, X, Y, tol) > d = max(abs(X(:) - Y(:))); > if d <= tol, v = 'OK'; else, v = 'CHECK THIS'; end > fprintf('same(%s): max diff = %.3e -> %s\n', name, d, v); > end > function d = fd1(g, x, h), d = (g(x+h) - g(x-h)) ./ (2*h); end > function d = fd2(g, x, h) > d = (-g(x-2*h) + 16*g(x-h) - 30*g(x) + 16*g(x+h) - g(x+2*h)) ./ (12*h^2); > end > function [x, k, steps] = newton_polish(g, x0, h, maxit, gtol) > x = x0; steps = []; > for k = 1:maxit > d = fd1(g, x, h); > if d == 0, break; end > step = g(x)/d; x = x - step; steps(end+1) = abs(step); %#ok<AGROW> > if abs(step) <= eps(x) || abs(g(x)) < gtol, return; end > end > end > function rho = measured_ratio(g, x0) > x = x0; s = []; > for k = 1:14 > d = fd1(g, x, 1e-6); > if d == 0, break; end > st = g(x)/d; x = x - st; s(end+1) = abs(st); %#ok<AGROW> > end > keep = s(1:end-1) > 1e-7 & s(2:end) > 1e-7; > r = s(2:end)./s(1:end-1); r = r(keep); > if numel(r) < 2, rho = 0; else, rho = median(r(max(1,end-4):end)); end > end > ``` > *Look for:* the $-0.75$ start lands at `1.0000325...` after 28 iterations; the $f'$ run from 2 tiptoes to `1.0000114...` in 19; both `same()` checks OK (`rho2` $\approx$ `0.4999`, `rho3` $\approx$ `0.6699`); from $1.30$ the modified step lands in 5 iterations where plain Newton needed 23. > > **Step 3: the detector, multiplicity meter, classifier.** Add these to the functions block, then call them. > ```matlab > roots = detect(f, -2, 2, 1e-8, 1e-13); > cps = detect(fpn, -2, 2, 1e-6, 1e-9); > for i = 1:numel(roots) > m = est_mult(f, roots(i).x); > fprintf('root %14.8f res %.2e it %3d m %d\n', ... > roots(i).x, roots(i).res, roots(i).it, m) > end > for i = 1:numel(cps) > v = fd2(f, cps(i).x, 1e-4); > if v > 1e-3, cls = 'local min'; elseif v < -1e-3, cls = 'local max'; > else, cls = 'DEGENERATE (test inconclusive)'; end > fprintf('crit %14.8f f''''=%+.4f %s\n', cps(i).x, v, cls) > end > > % ---- add to the functions block ---- > function m = est_mult(g, r) > rho = measured_ratio(g, r + 0.01); > if ~isfinite(rho) || rho < 0.2, m = 1; else, m = round(1/(1-rho)); end > end > function found = detect(g, a, b, res_tol, gtol) > N = 70; n_extra = 10; match_tol = 1e-3; > xg = linspace(a, b, N); yg = arrayfun(g, xg); > seeds = []; > for i = 1:N-1 > if yg(i)*yg(i+1) < 0, seeds(end+1) = (xg(i)+xg(i+1))/2; end %#ok<AGROW> > end > [~, idx] = sort(abs(yg)); seeds = [seeds, xg(idx(1:n_extra))]; > found = struct('x', {}, 'res', {}, 'it', {}); > for s = seeds > [r, it] = newton_polish(g, s, 1e-6, 200, gtol); > if r >= a && r <= b && abs(g(r)) <= res_tol > if all(abs(r - [found.x]) > match_tol) > found(end+1) = struct('x', r, 'res', abs(g(r)), 'it', it); %#ok<AGROW> > end > end > end > [~, ord] = sort([found.x]); found = found(ord); > end > ``` > *Look for:* three roots ($-1$, $\approx 0$, $\approx 1$ with $m = 1, 2, 3$) and four critical points with $x \approx 1$ DEGENERATE. Multiple-root locations are only good to their accuracy floors ($\sim 10^{-7}$ and $\sim 3\times10^{-5}$); that is the wall, not a bug. > > **Step 4: the test suite.** > ```matlab > suite = { 'T1 class', f, [-1.0, 0.0, 1.0]; > 'T2 quintic', @(x)(x+1.5).*(x+.5).*(x-.3).*(x-1.1).*(x-1.7), [-1.5,-0.5,0.3,1.1,1.7]; > 'T3 edge', @(x)(x-1.95).*(x+0.25).^2, [-0.25, 1.95]; > 'T4 sin(3x)', @(x) sin(3*x), [-pi/3, 0.0, pi/3] }; > tot = 0; hit = 0; > for i = 1:size(suite, 1) > g = suite{i,2}; cat = suite{i,3}; > got = detect(g, -2, 2, 1e-8, 1e-13); gx = [got.x]; > h_ = 0; for r = cat, h_ = h_ + any(abs(gx - r) <= 1e-3); end > ghosts = 0; for j = 1:numel(gx), ghosts = ghosts + ~any(abs(gx(j)-cat) <= 1e-3); end > ok = all(arrayfun(@(v) abs(g(v)), gx) <= 1e-8) && ghosts == 0; > tot = tot + numel(cat); hit = hit + h_; > fprintf('%-10s completeness %d/%d ghosts %d soundness %s\n', ... > suite{i,1}, h_, numel(cat), ghosts, string(ok)) > end > fprintf('SUITE TOTAL: %d/%d\n', hit, tot) > ``` > *Look for:* `3/3, 5/5, 2/2, 3/3`, all sound, `SUITE TOTAL: 13/13`. Re-run the Day-2 sign scan on T1 and it still finds only two of three; keep that printout for Milestone 2. > > **Step 5: the width sweep.** > ```matlab > fps = @(x) 6*x.^5 - 10*x.^4 + 6*x.^2 - 2*x; % analytic f', checks only > hs = logspace(-12, -1, 45); > errs = arrayfun(@(h) abs(fd1(f, 0.5, h) - fps(0.5)), hs); > [emin, imin] = min(errs); > fprintf('best h = %.3e (error %.3e), hand h* = %.3e\n', hs(imin), emin, h_star) > ``` > *Look for:* best measured $h$ within a factor of a few of $h^* = 2.75\times10^{-6}$ (the exact argmin varies in the roundoff-noise regime; the decade is the claim). > > **Step 6: the dashboard.** > ```matlab > xx = linspace(-2, 2, 800); > figure; hold on > plot(xx, f(xx), '-k', 'LineWidth', 1.6, 'DisplayName', '$f(x)=x^2(x+1)(x-1)^3) > plot(xx, arrayfun(@(v) fd1(f,v,1e-6), xx), '-b', 'DisplayName', "$f'$ (3-point FD)") > plot(xx, arrayfun(@(v) fd2(f,v,1e-4), xx), '-r', 'DisplayName', "$f''$ (5-point FD)") > ylim([-2 2]); ax = gca; > ax.XAxisLocation = 'origin'; ax.YAxisLocation = 'origin'; grid on > for i = 1:numel(roots) > m = est_mult(f, roots(i).x); > plot(roots(i).x, f(roots(i).x), 'p', 'MarkerSize', 14, ... > 'MarkerFaceColor', '#E69F00', 'MarkerEdgeColor', 'k', ... > 'DisplayName', sprintf('root x=%.4g (m=%d)', roots(i).x, m)) > end > for i = 1:numel(cps) > v = fd2(f, cps(i).x, 1e-4); > if v > 1e-3, mk = '^'; col = '#009E73'; cls = 'local min'; > elseif v < -1e-3, mk = 'v'; col = '#56B4E9'; cls = 'local max'; > else, mk = 's'; col = '#CC79A7'; cls = 'degenerate'; end > plot(cps(i).x, f(cps(i).x), mk, 'MarkerSize', 9, 'MarkerFaceColor', col, ... > 'MarkerEdgeColor', 'k', 'DisplayName', sprintf('%s x=%.4g', cls, cps(i).x)) > end > legend('show', 'Interpreter', 'latex', 'Location', 'northwest', ... > 'NumColumns', 2, 'FontSize', 7) > title("analyzeFunction($f$, $-2$, $2$): every marker backed by a diagnostics row", ... > 'Interpreter', 'latex') > xlabel('x'); ylabel('y'); hold off > ``` > *Look for:* the same seven markers and dynamic legend as the shipped dashboard; the legend labels came from the detector's output, not from a hand-typed list (the LivePlotting_5 legend problem, finally solved). > [!example]- R > > Base R throughout; no packages. `.Machine$double.eps` plays the role of `eps`, and `abs(x) * .Machine$double.eps` is a serviceable stand-in for MATLAB's `eps(x)` local spacing. > > **Step 1: define the helper, then warm up on the hand examples.** > ```r > EPS <- .Machine$double.eps > same <- function(name, X, Y, tol = 1e-8) { > d <- max(abs(X - Y)) > cat(sprintf("same(%s): max diff = %.3e -> %s\n", name, d, > if (d <= tol) "OK" else "CHECK THIS")) > } > f <- function(x) x^2 * (x + 1) * (x - 1)^3 # the class function > cat(f(-0.1), f(0.1), f(-0.1) * f(0.1) > 0, "\n") > cat("predicted ratios:", 1 - 1/2, 1 - 1/3, "\n") > h_star <- (6 * EPS * abs(f(0.5)) / 3)^(1/3) # M3 = |f'''(0.5)| = 3 > print(h_star) > ``` > *Look for:* `-0.011979 -0.008019 TRUE`, ratios `0.5 0.6666667`, `h_star = 2.750865e-06`. > > **Step 2: stencils and the polisher; mirror the class runs.** > ```r > fd1 <- function(g, x, h) (g(x + h) - g(x - h)) / (2 * h) > fd2 <- function(g, x, h) > (-g(x-2*h) + 16*g(x-h) - 30*g(x) + 16*g(x+h) - g(x+2*h)) / (12*h^2) > > newton_polish <- function(g, x0, h = 1e-6, maxit = 200, gtol = 1e-13) { > x <- x0; steps <- numeric(0) > for (k in 1:maxit) { > d <- fd1(g, x, h); if (d == 0) break > step <- g(x) / d; x <- x - step; steps <- c(steps, abs(step)) > if (abs(step) <= abs(x) * EPS || abs(g(x)) < gtol) > return(list(x = x, it = k, steps = steps)) > } > list(x = x, it = maxit, steps = steps) > } > measured_ratio <- function(g, x0, n = 14, h = 1e-6, floor = 1e-7) { > x <- x0; s <- numeric(0) > for (k in 1:n) { > d <- fd1(g, x, h); if (d == 0) break > st <- g(x) / d; x <- x - st; s <- c(s, abs(st)) > } > keep <- s[-length(s)] > floor & s[-1] > floor > r <- (s[-1] / s[-length(s)])[keep] > if (length(r) < 2) return(0) # superlinear collapse > median(tail(r, 5)) > } > > print(newton_polish(f, -0.75)[c("x", "it")]) > fpn <- function(t) fd1(f, t, 1e-6) > print(newton_polish(fpn, 2.0, h = 1e-4, gtol = 1e-9)[c("x", "it")]) > rho2 <- measured_ratio(f, 0.20); rho3 <- measured_ratio(f, 1.30) > same("double-root ratio vs 1/2", rho2, 0.5, tol = 0.02) > same("triple-root ratio vs 2/3", rho3, 2/3, tol = 0.02) > > x <- 1.30 # the repair: knowing m fixes the tiptoe > for (k in 1:60) { > st <- 3 * f(x) / fd1(f, x, 1e-6); x <- x - st > if (abs(st) < 1e-8) break > } > cat("modified Newton (m=3):", x, "in", k, "iterations\n") > ``` > *Look for:* `x = 1.00003..., it = 28` from the $-0.75$ start; `c = 1.0000114..., it = 19` on $f'$; both `same()` checks OK; the modified step lands in 5 iterations where plain Newton needed 23. > > **Step 3: the detector, multiplicity meter, classifier.** > ```r > est_mult <- function(g, r) { > rho <- measured_ratio(g, r + 0.01) > if (!is.finite(rho) || rho < 0.2) return(c(1, rho)) > c(round(1 / (1 - rho)), rho) > } > detect <- function(g, a, b, N = 70, n_extra = 10, > res_tol = 1e-8, match_tol = 1e-3, gtol = 1e-13) { > xg <- seq(a, b, length.out = N); yg <- sapply(xg, g) > seeds <- (xg[-N] + xg[-1])[yg[-N] * yg[-1] < 0] / 2 > seeds <- c(seeds, xg[order(abs(yg))[1:n_extra]]) > found <- data.frame(x = numeric(0), res = numeric(0), it = integer(0)) > for (s in seeds) { > p <- newton_polish(g, s, gtol = gtol) > if (p$x >= a && p$x <= b && abs(g(p$x)) <= res_tol) > if (all(abs(p$x - found$x) > match_tol)) > found <- rbind(found, data.frame(x = p$x, res = abs(g(p$x)), it = p$it)) > } > found[order(found$x), ] > } > > roots <- detect(f, -2, 2) > cps <- detect(fpn, -2, 2, res_tol = 1e-6, gtol = 1e-9) > for (i in seq_len(nrow(roots))) { > mr <- est_mult(f, roots$x[i]) > cat(sprintf("root %14.8f res %.2e it %3d m %d\n", > roots$x[i], roots$res[i], roots$it[i], mr[1])) > } > for (i in seq_len(nrow(cps))) { > v <- fd2(f, cps$x[i], 1e-4) > cls <- if (v > 1e-3) "local min" else if (v < -1e-3) "local max" > else "DEGENERATE (test inconclusive)" > cat(sprintf("crit %14.8f f''=%+.4f %s\n", cps$x[i], v, cls)) > } > ``` > *Look for:* the same seven rows as the reference: roots with $m = 1, 2, 3$; the fourth critical point at $x \approx 1$ present and flagged DEGENERATE. > > **Step 4: the test suite.** > ```r > suite <- list( > list("T1 class", f, c(-1.0, 0.0, 1.0)), > list("T2 quintic", function(x) (x+1.5)*(x+.5)*(x-.3)*(x-1.1)*(x-1.7), c(-1.5,-0.5,0.3,1.1,1.7)), > list("T3 edge", function(x) (x-1.95)*(x+0.25)^2, c(-0.25, 1.95)), > list("T4 sin(3x)", function(x) sin(3*x), c(-pi/3, 0, pi/3))) > tot <- 0; hit <- 0 > for (tc in suite) { > g <- tc[[2]]; cat_ <- tc[[3]] > got <- detect(g, -2, 2) > h_ <- sum(sapply(cat_, function(r) any(abs(got$x - r) <= 1e-3))) > ghosts <- sum(sapply(got$x, function(v) !any(abs(v - cat_) <= 1e-3))) > ok <- all(abs(sapply(got$x, g)) <= 1e-8) && ghosts == 0 > tot <- tot + length(cat_); hit <- hit + h_ > cat(sprintf("%-10s completeness %d/%d ghosts %d soundness %s\n", > tc[[1]], h_, length(cat_), ghosts, if (ok) "PASS" else "FAIL")) > } > cat(sprintf("SUITE TOTAL: %d/%d\n", hit, tot)) > ``` > *Look for:* `13/13`, `ghosts 0`, all PASS. > > **Step 5: the width sweep.** > ```r > fps <- function(x) 6*x^5 - 10*x^4 + 6*x^2 - 2*x # analytic f', checks only > hs <- 10^seq(-12, -1, length.out = 45) > errs <- sapply(hs, function(h) abs(fd1(f, 0.5, h) - fps(0.5))) > cat(sprintf("best h = %.3e (error %.3e), hand h* = %.3e\n", > hs[which.min(errs)], min(errs), h_star)) > ``` > *Look for:* the argmin in $h^*s decade ($\sim 10^{-6}$); the exact winner wobbles in the noise regime, the decade is the claim. > > **Step 6: the dashboard.** > ```r > xx <- seq(-2, 2, length.out = 800) > png("liveplot10_dashboard.png", width = 1350, height = 840, res = 150) > plot(xx, sapply(xx, f), type = "l", lwd = 2, ylim = c(-2, 2), > xlab = "x", ylab = "y", > main = "analyzeFunction(f, -2, 2): every marker backed by a diagnostics row") > abline(h = 0, v = 0, col = "gray40"); grid(col = "gray85") > lines(xx, sapply(xx, function(v) fd1(f, v, 1e-6)), col = "blue") > lines(xx, sapply(xx, function(v) fd2(f, v, 1e-4)), col = "red") > leg <- c("f", "f' (3-point FD)", "f'' (5-point FD)") > pchv <- c(NA, NA, NA); colv <- c("black", "blue", "red") > for (i in seq_len(nrow(roots))) { > points(roots$x[i], f(roots$x[i]), pch = 8, cex = 1.6, col = "#E69F00", lwd = 2) > mr <- est_mult(f, roots$x[i]) > leg <- c(leg, sprintf("root x=%.4g (m=%d)", roots$x[i], mr[1])) > pchv <- c(pchv, 8); colv <- c(colv, "#E69F00") > } > for (i in seq_len(nrow(cps))) { > v <- fd2(f, cps$x[i], 1e-4) > sty <- if (v > 1e-3) c(24, "#009E73", "local min") > else if (v < -1e-3) c(25, "#56B4E9", "local max") > else c(22, "#CC79A7", "degenerate") > points(cps$x[i], f(cps$x[i]), pch = as.numeric(sty[1]), bg = sty[2], cex = 1.3) > leg <- c(leg, sprintf("%s x=%.4g", sty[3], cps$x[i])) > pchv <- c(pchv, as.numeric(sty[1])); colv <- c(colv, sty[2]) > } > legend("topleft", legend = leg, col = colv, pt.bg = colv, pch = pchv, > lty = c(1, 1, 1, rep(NA, length(leg) - 3)), cex = 0.6, ncol = 2) > dev.off() > ``` > *Look for:* seven markers, legend assembled from the detector's actual output; every legend entry corresponds to a printed diagnostics row. > [!example]- Mathematica (numerical only) > > No symbolic shortcuts: derivatives come from the same finite-difference stencils as every other language (no `D[...]`), which is the point of the exercise. E, N, D, and C are protected names; we use `nn`, `dd`, and friends. > > **Step 1: define the helper, then warm up on the hand examples.** > ```wolfram > same[name_, x_, y_, tol_ : 10^-8] := Module[{d = Max[Abs[Flatten[{x}] - Flatten[{y}]]]}, > Print["same(", name, "): max diff = ", ScientificForm[d, 3], " -> ", > If[d <= tol, "OK", "CHECK THIS"]]]; > f[x_?NumericQ] := x^2 (x + 1.) (x - 1.)^3; > Print[f[-0.1], " ", f[0.1], " ", f[-0.1] f[0.1] > 0]; > Print["predicted ratios: ", 1. - 1/2, " ", N[1 - 1/3]]; > hstar = (6 $MachineEpsilon Abs[f[0.5]]/3)^(1/3) > ``` > *Look for:* `-0.011979 -0.008019 True`, ratios `0.5 0.666667`, `hstar = 2.75087*10^-6`. > > **Step 2: stencils and the polisher; mirror the class runs.** > ```wolfram > fd1[g_, x_, h_] := (g[x + h] - g[x - h])/(2 h); > fd2[g_, x_, h_] := (-g[x - 2 h] + 16 g[x - h] - 30 g[x] + 16 g[x + h] - g[x + 2 h])/(12 h^2); > > newtonPolish[g_, x0_, h_ : 10.^-6, maxit_ : 200, gtol_ : 10.^-13] := > Module[{x = N[x0], dd, step, k = 0, steps = {}}, > While[k < maxit, k++; > dd = fd1[g, x, h]; If[dd == 0., Break[]]; > step = g[x]/dd; x = x - step; AppendTo[steps, Abs[step]]; > If[Abs[step] <= $MachineEpsilon Abs[x] || Abs[g[x]] < gtol, Break[]]]; > {x, k, steps}]; > > measuredRatio[g_, x0_, n_ : 14, h_ : 10.^-6, floor_ : 10.^-7] := > Module[{x = N[x0], dd, st, s = {}, keep, r}, > Do[dd = fd1[g, x, h]; If[dd == 0., Break[]]; > st = g[x]/dd; x = x - st; AppendTo[s, Abs[st]], {n}]; > keep = Table[s[[i]] > floor && s[[i + 1]] > floor, {i, Length[s] - 1}]; > r = Pick[Rest[s]/Most[s], keep]; > If[Length[r] < 2, 0., Median[Take[r, -Min[5, Length[r]]]]]]; > > Print[newtonPolish[f, -0.75][[1 ;; 2]]]; > fpn[t_?NumericQ] := fd1[f, t, 10.^-6]; > Print[newtonPolish[fpn, 2.0, 10.^-4, 200, 10.^-9][[1 ;; 2]]]; > rho2 = measuredRatio[f, 0.20]; rho3 = measuredRatio[f, 1.30]; > same["double-root ratio vs 1/2", rho2, 0.5, 0.02]; > same["triple-root ratio vs 2/3", rho3, N[2/3], 0.02]; > > x = 1.30; kk = 0; (* the repair: knowing m fixes the tiptoe *) > While[kk < 60, kk++; > st = 3 f[x]/fd1[f, x, 10.^-6]; x = x - st; > If[Abs[st] < 10.^-8, Break[]]]; > Print["modified Newton (m=3): ", x, " in ", kk, " iterations"]; > ``` > *Look for:* `{1.00003, 28}` from the $-0.75$ start; `{1.00001, 19}` on $f'$; both checks OK; the modified step lands in 5 iterations where plain Newton needed 23. > > **Step 3: the detector, multiplicity meter, classifier.** > ```wolfram > estMult[g_, r_] := Module[{rho = measuredRatio[g, r + 0.01]}, > If[! NumericQ[rho] || rho < 0.2, {1, rho}, {Round[1/(1 - rho)], rho}]]; > > detect[g_, a_, b_, resTol_ : 10.^-8, gtol_ : 10.^-13] := > Module[{nn = 70, nExtra = 10, matchTol = 10.^-3, xg, yg, seeds, found = {}, p}, > xg = Subdivide[N[a], N[b], nn - 1]; yg = g /@ xg; > seeds = Table[If[yg[[i]] yg[[i + 1]] < 0, (xg[[i]] + xg[[i + 1]])/2, Nothing], > {i, nn - 1}]; > seeds = Join[seeds, xg[[Ordering[Abs[yg], nExtra]]]]; > Do[p = newtonPolish[g, s, 10.^-6, 200, gtol]; > If[a <= p[[1]] <= b && Abs[g[p[[1]]]] <= resTol && > AllTrue[found, Abs[p[[1]] - #[[1]]] > matchTol &], > AppendTo[found, {p[[1]], Abs[g[p[[1]]]], p[[2]]}]], {s, seeds}]; > SortBy[found, First]]; > > roots = detect[f, -2, 2]; > cps = detect[fpn, -2, 2, 10.^-6, 10.^-9]; > Do[Print["root ", NumberForm[q[[1]], {12, 8}], " res ", ScientificForm[q[[2]], 2], > " it ", q[[3]], " m ", estMult[f, q[[1]]][[1]]], {q, roots}]; > Do[Module[{v = fd2[f, q[[1]], 10.^-4], cls}, > cls = Which[v > 10.^-3, "local min", v < -10.^-3, "local max", > True, "DEGENERATE (test inconclusive)"]; > Print["crit ", NumberForm[q[[1]], {12, 8}], " f''=", NumberForm[v, {5, 4}], > " ", cls]], {q, cps}]; > ``` > *Look for:* the same seven rows; $x \approx 1$ DEGENERATE with $\hat m = 2$ as a zero of $f'$. > > **Step 4: the test suite.** > ```wolfram > suite = { > {"T1 class", f, {-1., 0., 1.}}, > {"T2 quintic", Function[x, (x + 1.5) (x + .5) (x - .3) (x - 1.1) (x - 1.7)], > {-1.5, -0.5, 0.3, 1.1, 1.7}}, > {"T3 edge", Function[x, (x - 1.95) (x + 0.25)^2], {-0.25, 1.95}}, > {"T4 sin(3x)", Function[x, Sin[3 x]], {-Pi/3., 0., Pi/3.}}}; > tot = 0; hit = 0; > Do[Module[{g = tc[[2]], cat = tc[[3]], got, gx, h1, ghosts, ok}, > got = detect[g, -2, 2]; gx = First /@ got; > h1 = Count[cat, r_ /; AnyTrue[gx, Abs[# - r] <= 10.^-3 &]]; > ghosts = Count[gx, v_ /; ! AnyTrue[cat, Abs[v - #] <= 10.^-3 &]]; > ok = AllTrue[gx, Abs[g[#]] <= 10.^-8 &] && ghosts == 0; > tot += Length[cat]; hit += h1; > Print[tc[[1]], ": completeness ", h1, "/", Length[cat], " ghosts ", ghosts, > " soundness ", If[ok, "PASS", "FAIL"]]], {tc, suite}]; > Print["SUITE TOTAL: ", hit, "/", tot]; > ``` > *Look for:* `13/13`, all PASS. > > **Step 5: the width sweep.** > ```wolfram > fps[x_?NumericQ] := 6 x^5 - 10 x^4 + 6 x^2 - 2 x; (* analytic f', checks only *) > hs = 10.^Subdivide[-12., -1., 44]; > errs = Abs[fd1[f, 0.5, #] - fps[0.5]] & /@ hs; > Print["best h = ", ScientificForm[hs[[First[Ordering[errs, 1]]]], 3], > " (error ", ScientificForm[Min[errs], 3], "), hand h* = ", ScientificForm[hstar, 3]]; > ``` > *Look for:* the argmin in $h^*s decade ($\sim 10^{-6}$). > > **Step 6: the dashboard.** > ```wolfram > classify[q_] := Module[{v = fd2[f, q[[1]], 10.^-4]}, > Which[v > 10.^-3, {"local min", "\[FilledUpTriangle]", RGBColor["#009E73"]}, > v < -10.^-3, {"local max", "\[FilledDownTriangle]", RGBColor["#56B4E9"]}, > True, {"degenerate", "\[FilledSquare]", RGBColor["#CC79A7"]}]]; > rootPts = {RGBColor["#E69F00"], PointSize[0.018], Point[{#[[1]], f[#[[1]]]}]} & /@ roots; > cpPts = Module[{c = classify[#]}, {c[[3]], PointSize[0.014], > Point[{#[[1]], f[#[[1]]]}]}] & /@ cps; > Plot[{f[x], fd1[f, x, 10.^-6], fd2[f, x, 10.^-4]}, {x, -2, 2}, > PlotRange -> {-2, 2}, PlotStyle -> {Black, Blue, Red}, > AxesLabel -> {"x", "y"}, GridLines -> Automatic, > GridLinesStyle -> Directive[Gray, Opacity[0.25]], > PlotLegends -> {"f", "f' (3-point FD)", "f'' (5-point FD)"}, > Epilog -> Join[rootPts, cpPts], > PlotLabel -> "analyzeFunction(f, -2, 2): every marker backed by a diagnostics row"] > ``` > *Look for:* seven marks over the three curves; pair the figure with the printed table from Step 3, that pairing IS invariant I3 (Mathematica's `Epilog` legend support is thin, so the table carries the labels here). What the guided run should produce: ![The finished dashboard: every marker backed by a diagnostics row](Media/liveplot10_dashboard.png) ![Newton's step ratio reads the multiplicity](Media/liveplot10_convergence.png) ![The width sweep: truncation vs roundoff, with the hand h* marked](Media/liveplot10_hsweep.png) > [!warning] Verify against ground truth (required) > Record these checked numbers; "it ran" is not verification. All of them come from one verified run of the reference code. > - Blindness product: $f(-0.1)\,f(0.1) = 9.606 \times 10^{-5} > 0$ (the scan misses $x=0$). > - Measured contraction ratios: $\rho \approx 0.4999$ at the double root (predicted $0.5$) and $\rho \approx 0.6699$ at the triple root (predicted $0.6667$), both `same()` OK at tolerance $0.02$. > - Class mirrors: Newton from $-0.75$ lands at $1.0000325...$ after $28$ iterations; Newton on $f'$ from $2$ lands at $1.0000114...$ after $19$. > - Repair: from $1.30$, plain Newton needs $23$ iterations; the modified step with $m=3$ lands in $5$. > - Detector on the class function: three roots ($\hat m = 1, 2, 3$) and four critical points, with $x \approx 1$ flagged DEGENERATE ($|f''| \approx 10^{-4}$, below threshold). > - Accuracy floors, measured: $|{\hat r} - 0| = 2.1 \times 10^{-7}$ (double), $|\hat r - 1| = 3.3 \times 10^{-5}$ (triple), matching the hand estimates $10^{-7}$ and $1.7 \times 10^{-5}$ in order of magnitude. > - Test suite: completeness $13/13$, ghosts $0$, soundness PASS on all four functions; the naive baseline scores $2/3$ on T1, finding only $[-0.9977,\ 1.0121]$. > - Width sweep: best measured $h = 1.778 \times 10^{-6}$ (error $1.264 \times 10^{-12}$), same decade as the hand $h^* = 2.751 \times 10^{-6}$. ### Trick-out tracks: widen the instrument (choose one) The base contract is the shared floor. From here you pick ONE track and build it in ONE language of your choice. Each track states its own contract; your Milestone 1 form commits to a track (or proposes your own), and your final write-up defends the contract with evidence, not adjectives. Scope is negotiated at Milestone 1: a well-scoped track done with evidence beats an ambitious track done on faith. **Track S: Seeding and search.** The base seeder is a uniform grid plus a smallest-$|f|$ rescue. Beat it. Options: [Chebyshev-spaced](https://en.wikipedia.org/wiki/Chebyshev_nodes) seed grids, adaptive refinement where $|f|$ dips, a [bisection](https://en.wikipedia.org/wiki/Bisection_method) safeguard so no polished root ever escapes its bracket, restart policies for divergent seeds. *Contract:* on an extended suite you design (at least three new functions, including one with two roots closer together than the base grid spacing and one with a root within $0.01$ of the window edge), completeness stays $100\%$, ghosts stay $0$, and you report total function evaluations, which must beat the base detector's count on at least one family. *Evidence:* the suite table, the evaluation counts, and a paragraph on the tradeoff you bought. **Track H: Hardening.** Make the instrument unbreakable and honest under abuse. Options: multiplicity-aware acceptance (report $e_{\text{floor}}$ next to every multiple root), resolve DEGENERATE critical points by escalating to third and fourth FD derivatives (our $x=1$ is a flat inflection with a horizontal tangent; prove your resolver says so), divergence detection with a documented failure message, an evaluation budget the polisher cannot exceed. *Contract:* correct $\hat m$ and a correct or explicitly-inconclusive classification on every catalog entry; no input among your adversarial cases (at least: a function with no roots in window, a constant, a function with a pole) crashes it or produces a silent lie. *Evidence:* the adversarial suite and its printout. **Track D: Two dimensions.** Rebuild the detector for surfaces: FD gradient, FD [Hessian](https://en.wikipedia.org/wiki/Hessian_matrix), Newton on $\nabla f$, classification by the Hessian's eigenvalue signs. Use the surface $f(x,y) = 2x^3 + xy^2 + 5x^2 + y^2$ on $[-4,2]\times[-3,3]$. *Contract:* find all four critical points, classify them (one minimum, one maximum, two [saddle points](https://en.wikipedia.org/wiki/Saddle_point)), verify each gradient residual, and render a contour dashboard with shape-coded markers. *Evidence:* the diagnostics table and figure. This track runs ahead of the linear-algebra arc we are about to start; expect to teach yourself the eigenvalue sign test from the handout of the Multivariate Taylor project, and expect the payoff when the 2D lectures land. **Track P: Performance.** Instrument the instrument. Options: vectorize the seeding pass, cache function evaluations, count every call to $f$ honestly (the FD derivative costs two per iteration; make the ledger balance), and measure wall-clock time properly (median of repeated runs). *Contract:* an evaluation-count and timing table across grid sizes $N \in \{35, 70, 140, 280\}$ on the full suite, a stated completeness at each $N$, and one optimization that provably reduces evaluations at fixed completeness, with before/after counts. *Evidence:* the table and the diff of the one optimization. **Track U: Interface.** Make it usable by a stranger. Options: an options structure with documented defaults (tolerances, $N$, verbosity), input validation with error messages that say what to fix, a `--help`-quality docstring, a README with a five-line quick start. *Contract:* a classmate who has never seen your code runs it against a function THEY choose, using only your README, and gets a correct dashboard and table on the first try; their signed note (or the bug they filed instead) is your evidence. This track is exercised at Milestone 4 by construction. **The open lane.** Propose your own goal on the Milestone 1 form using the same four fields every track above states: goal, invariants, tests, evidence. If the contract is checkable and the scope survives a conversation with me, it is approved. Uncheckable contracts ("make it better", "add AI") are returned. ### Your own goal (the finale) Whatever the track, the finale is the same three artifacts, and the diagnostics come FIRST: 1. **The instrument**: base contract passing, trick-out installed, one language, runnable top to bottom. 2. **The evidence**: your track's contract, tested and printed. Every number in your write-up traceable to a run. 3. **The dashboard**: the figure your instrument draws for a function (or surface) of your choosing, with every marker backed by a diagnostics row, and your display choices (window, $N$, tolerances, marker encoding) stated and defended from the diagnostics, not from taste. #### Student task loop for implementation, analysis and reflection 1. **Specify** the goal as a contract: invariants, tests, evidence (Milestone 1; the pseudocode template holds your algorithm, the derivation template holds your track's supporting mathematics). 2. **Predict** what each test should show before you run it, including which baseline failures should disappear. 3. **Implement** the track; keep the base suite passing the whole time (a trick-out that breaks I1 or I2 is a regression, not a feature). 4. **Compare and defend**: result vs prediction vs contract, in the write-up, from the printed diagnostics. ## Reflection Framework Address these (2-3 focused questions per category, no more): ### Contracts and evidence - The naive scan and your detector disagree about how many roots $f$ has. What, precisely, settles the disagreement, and what would it take for your detector to be wrong the same quiet way? - Invariant I3 forced the instrument to print an accuracy admission ($e_{\text{floor}}$) next to multiple roots. Where else in your scientific computing life would you now want that admission printed? ### Design choices and cost - The smallest-$|f|$ rescue seeds cost extra polishing runs, most of which are duplicates. What did completeness cost in function evaluations, and when would you refuse to pay it? - Your stopping rule mixes three currencies: step size vs $\text{eps}(x)$, residual vs noise floor, and an iteration cap. Which one actually fired at each of your seven detections, and what does that tell you? ### Mathematical insights - [Multiplicity](https://en.wikipedia.org/wiki/Multiplicity_(mathematics)) as information: the step ratio $\rho = 1 - 1/m$ turns a convergence *nuisance* into a *measurement*. State one other place in the course where a failure rate is itself the data. - The accuracy floor $e_{\text{floor}} = (\tau_f/|C|)^{1/m}$ says a triple root cannot be located to better than about five digits in double precision, by ANY method. Reconcile this with the sixteen digits your language happily prints. ## (Optional) Mathematical Extensions - **Deflation.** After finding $r$, hunt on $u(x) = f(x)/(x - r)$ to stop rediscovering the same root; compare against your dedup gate for reliability and evaluation cost. - **[Halley's method](https://en.wikipedia.org/wiki/Halley%27s_method).** One more Taylor term gives cubic convergence; measure its order empirically with the Day-7 slope trick and check it also degrades to linear at multiple roots. - **Global root finding by proxy.** [Chebyshev-polynomial](https://en.wikipedia.org/wiki/Chebyshev_polynomials) approximation reduces "all roots of $f$ on $[a,b]quot; to a polynomial eigenvalue problem, the strategy behind Chebfun; prototype the idea by seeding your detector with the roots of a degree-30 interpolant. ## (Optional) Real-World Context ### Applications - **[Brent's method](https://en.wikipedia.org/wiki/Brent%27s_method):** the safeguarded design you built in miniature (fast step when safe, bracket when not) is the default scalar root finder in SciPy (`brentq`), MATLAB (`fzero`), and R (`uniroot`). - **[Test suites for numerical libraries](https://en.wikipedia.org/wiki/Regression_testing):** LAPACK, the linear-algebra core under nearly every language you will ever use, ships thousands of contract tests that run on every change; your suite is the same idea at classroom scale. ### Technical challenges - **[Floating-point arithmetic](https://en.wikipedia.org/wiki/Floating-point_arithmetic):** the accuracy floor you measured is a hard consequence of representation, not an implementation flaw; production libraries document it the way your I3 rows do. ### Why it matters A double-precision number carries just under 16 decimal digits ($\epsilon \approx 2.22 \times 10^{-16}$), and your instrument now reports how many of them it actually delivered at each marker: five at a triple root, seven at a double, all sixteen at a simple one. That printed admission is the entire difference between a plot and an instrument. <!-- ============================================================================ AUTHORING CHECKLIST (internal, delete before publishing to students) - [x] Front-matter filled; points/stages metadata only; NO grading content in the body. - [x] Four-beat arc present in systems-led mode (v0.5.2): contract -> mirror (failures included) -> build-to-contract vs shipped suite + track menu -> own-goal finale. - [ ] N/A: no Su25 source (new project, not a port); no derivations cut. - [x] Language-first callouts, all six steps in each of the four languages for the BASE contract; per-step Look-for lines; workflow as ONE [!abstract] callout; Python run-verified in the sandbox (code/liveplot10/run_printout.txt). - [x] All published numbers from ONE verified run (liveplot10.py, 2026-07-12 sandbox); handout callouts and code README generated from the same printout. Verification Table companion: TO BUILD from the same printout. - [x] Figures from the verified Python; 4 figures in Media/; dashboard/convergence/ hsweep shipped, blindspots as the motivating figure. - [x] Companions BUILT 2026-07-14 (Milestone Map, Verification Table; expected values drawn from code/liveplot10/run_printout.txt, the same verified run as the handout callout; documented-failure rows and the track-contract block included). code/liveplot10/ has .py + README + printout, .m/.R/.wl assembled from the callouts, still NEED local MATLAB/R/Mathematica smoke runs per README TEST_RESULTS. - [x] Assignments WIRED 2026-07-14 (handout + companions + a no-downloads line noting the instructor-held Milestone 4 mystery function). - [x] Wiki links generous; no em dashes; section order ends Reflection -> (Optional) Extensions -> (Optional) Real-World Context; optional markers parenthesized. - [x] Systems-led firsts LOGGED: first v0.5.2 systems-led handout; language rule is 4-lang base + 1-lang track; the "track contract" pattern replaces personalities. Track D surface matches the planned 2D arc's documented test surface. ============================================================================ -->