
Topics in Digital Heritage:
Numerical Methods for Digital Reconstruction
Fall 2026
Newton’s method (Class 5)
Minimizes any smooth \(f(\mathbf{x})\) by repeatedly solving \[\nabla^2 f(\mathbf{x}_k)\, \mathbf{d}_k = -\nabla f(\mathbf{x}_k)\] and stepping to \(\mathbf{x}_{k+1}=\mathbf{x}_k+\mathbf{d}_k\).
Conjugate gradients (Class 6)
Minimizes exactly the quadratic energy \[f(\mathbf{x})=\tfrac12\mathbf{x}^\top\mathbf{A}\mathbf{x}-\mathbf{b}^\top\mathbf{x}\] matrix-free, guaranteed in \(\le n\) steps.
Real objectives are rarely “any smooth \(f\)” or “exactly quadratic”. They often have exploitable structure:
Today: turn each kind of structure into an algorithm that is faster, simpler, or more robust than calling generic Newton or CG.
Recall linear least squares from Linear Systems 2: minimize \(\|\mathbf{A}\mathbf{x}-\mathbf{b}\|_2^2\).
Nonlinear least squares
Given functions \(f_1(\mathbf{x}),\dots,f_k(\mathbf{x})\) we want \(\approx \mathbf{0}\), minimize \[ E(\mathbf{x}) \equiv \frac{1}{2}\sum_i [f_i(\mathbf{x})]^2. \]
Example: fit \(y=ce^{ax}\) to data \((x_1,y_1),\dots,(x_k,y_k)\):

Linearize each \(f_i\) around the current estimate \(\mathbf{x}_t\) (Taylor expansion): \[ f_i(\color{red}{\mathbf{x}}) \approx f_i(\mathbf{x}_t) + \nabla f_i(\mathbf{x}_t)\cdot(\color{red}{\mathbf{x}}-\mathbf{x}_t). \]
Stacking the \(f_i\)’s into \(F(\mathbf{x})\) with Jacobian \(DF\), this turns \(E(\mathbf{x})\) into a linear least-squares problem in \(\color{blue}{\delta\mathbf{x}}=\color{red}{\mathbf{x}}-\mathbf{x}_t\): \[ \min_{\color{blue}{\delta\mathbf{x}}} \ \tfrac12\|F(\mathbf{x}_t) + DF(\mathbf{x}_t)\,\color{blue}{\delta\mathbf{x}}\|_2^2\]
which has the closed-form solution (derived shortly)
\[\boxed{\mathbf{x}_{t+1} = \mathbf{x}_t - \big(DF(\mathbf{x}_t)^\top DF(\mathbf{x}_t)\big)^{-1} DF(\mathbf{x}_t)^\top F(\mathbf{x}_t)} \]
Iterating this update is Gauss-Newton. It only ever needs the first-order Jacobian \(DF\), never a Hessian, yet converges as fast as Newton’s method once close to the answer.
Stack residuals into \(F(\mathbf{x})\in\mathbb{R}^k\) and gradients (as rows) into the Jacobian \(DF(\mathbf{x})\in\mathbb{R}^{k\times n}\). All \(k\) tangents then become one vector equation, with row \(i\) being exactly the tangent of \(f_i\): \[ \underbrace{\begin{bmatrix} f_1(\mathbf{x})\\ \vdots\\ f_k(\mathbf{x})\end{bmatrix}}_{F(\mathbf{x})} \approx \begin{bmatrix} f_1(\mathbf{x}_t) + \nabla f_1(\mathbf{x}_t)^\top\delta\mathbf{x}\\ \vdots\\ f_k(\mathbf{x}_t) + \nabla f_k(\mathbf{x}_t)^\top\delta\mathbf{x}\end{bmatrix} = \underbrace{\begin{bmatrix} f_1(\mathbf{x}_t)\\ \vdots\\ f_k(\mathbf{x}_t)\end{bmatrix}}_{F(\mathbf{x}_t)} + \underbrace{\begin{bmatrix} \nabla f_1(\mathbf{x}_t)^\top\\ \vdots\\ \nabla f_k(\mathbf{x}_t)^\top\end{bmatrix}}_{DF(\mathbf{x}_t)}\delta\mathbf{x}. \]
Since \(E(\mathbf{x}) = \tfrac12\sum_i f_i(\mathbf{x})^2 = \tfrac12\|F(\mathbf{x})\|_2^2\), just substitute the approximation: \[ E(\mathbf{x}_t+\delta\mathbf{x}) \;\approx\; \tfrac12\big\|F(\mathbf{x}_t) + DF(\mathbf{x}_t)\,\delta\mathbf{x}\big\|_2^2. \]

Rename \(A \equiv DF(\mathbf{x}_t)\) and \(\mathbf{b} \equiv -F(\mathbf{x}_t)\). The problem is exactly linear regression: \[ \min_{\delta\mathbf{x}}\ \tfrac12\|F(\mathbf{x}_t) + DF(\mathbf{x}_t)\,\delta\mathbf{x}\|_2^2 \;=\; \min_{\delta\mathbf{x}}\ \tfrac12\|A\,\delta\mathbf{x} - \mathbf{b}\|_2^2 . \]
Set the gradient to zero, which gives the normal equations: \[\begin{align*} \nabla_{\delta\mathbf{x}}\,\tfrac12\|A\,\delta\mathbf{x} - \mathbf{b}\|_2^2 = A^\top(A\,\delta\mathbf{x} - \mathbf{b}) = \mathbf{0} &\implies A^\top A\,\delta\mathbf{x} = A^\top\mathbf{b} \\ &\implies \delta\mathbf{x} = (A^\top A)^{-1}A^\top\mathbf{b}. \end{align*}\]
Rename back (\(A = DF(\mathbf{x}_t)\), \(\mathbf{b} = -F(\mathbf{x}_t)\)) and take the step \(\mathbf{x}_{t+1} = \mathbf{x}_t + \delta\mathbf{x}\): \[ \boxed{\mathbf{x}_{t+1} = \mathbf{x}_t - \big(DF(\mathbf{x}_t)^\top DF(\mathbf{x}_t)\big)^{-1} DF(\mathbf{x}_t)^\top F(\mathbf{x}_t)} \] This needs \(DF(\mathbf{x}_t)\) to have full column rank. We will see an alternative later in case this fails (Levenberg-Marquardt).
np.linalg.lstsq (automatically chooses the best method).import numpy as np
import matplotlib.pyplot as plt
rng = np.random.default_rng(0)
a_true, c_true = 0.6, 2.0
x_data = np.linspace(0, 4, 25)
y_data = c_true * np.exp(a_true * x_data) + rng.normal(scale=0.4, size=x_data.size)
def residual(theta):
a, c = theta
return y_data - c * np.exp(a * x_data)
def jacobian(theta):
a, c = theta
e = np.exp(a * x_data)
J = np.zeros((x_data.size, 2))
J[:, 0] = -c * x_data * e
J[:, 1] = -e
return J
def loss(theta):
r = residual(theta)
return 0.5 * r @ r
def gradient_descent(theta0, lr, max_iter):
theta = np.array(theta0, dtype=float)
hist = [theta.copy()]
for _ in range(max_iter):
theta = theta - lr * (jacobian(theta).T @ residual(theta))
hist.append(theta.copy())
return np.array(hist)
def gauss_newton(theta0, max_iter):
theta = np.array(theta0, dtype=float)
hist = [theta.copy()]
for _ in range(max_iter):
J, r = jacobian(theta), residual(theta)
delta, *_ = np.linalg.lstsq(J, -r, rcond=None)
theta = theta + delta
hist.append(theta.copy())
return np.array(hist)
theta0_good = [0.3, 1.0]
h_gd = gradient_descent(theta0_good, lr=5e-5, max_iter=20000)
h_gn = gauss_newton(theta0_good, max_iter=8)
fig, axes = plt.subplots(1, 2, figsize=(8.5, 4))
xs = np.linspace(0, 4, 200)
axes[0].plot(x_data, y_data, "o", ms=4, color="gray", label="data")
axes[0].plot(xs, h_gn[-1][1] * np.exp(h_gn[-1][0] * xs), color="steelblue", lw=2, label="Gauss-Newton fit")
axes[0].set_title(f"Fitted $y=ce^{{ax}}$\n($a$={h_gn[-1][0]:.3f}, $c$={h_gn[-1][1]:.3f}, true: 0.6, 2.0)", fontsize=9)
axes[0].legend(fontsize=8)
x_gd = np.arange(1, len(h_gd) + 1)
x_gn = np.arange(1, len(h_gn) + 1)
axes[1].loglog(x_gd, [loss(t) for t in h_gd], color="firebrick", label=f"Gradient descent ({len(h_gd)-1} steps)")
axes[1].loglog(x_gn, [loss(t) for t in h_gn], "-o", ms=4, color="steelblue", label=f"Gauss-Newton ({len(h_gn)-1} steps)")
axes[1].set_xlabel("iteration $t$ (log scale)"); axes[1].set_ylabel("$E(\\theta_t)$")
axes[1].legend(fontsize=8)
axes[1].set_title("Same accuracy, 2500x fewer steps", fontsize=9)
plt.tight_layout()
plt.show()
Gauss-Newton is not guaranteed to converge:
Levenberg-Marquardt
Damp with \(\lambda>0\): \(\mathbf{x} = \mathbf{x}_0 - \big(DF(\mathbf{x}_0)^\top DF(\mathbf{x}_0) + \lambda \mathbf{I}\big)^{-1} DF(\mathbf{x}_0)^\top F(\mathbf{x}_0)\)

def levenberg_marquardt(theta0, lam0, max_iter):
theta = np.array(theta0, dtype=float)
lam = lam0
cur = loss(theta)
hist = [theta.copy()]
for _ in range(max_iter):
J, r = jacobian(theta), residual(theta)
delta = np.linalg.solve(J.T @ J + lam * np.eye(2), -J.T @ r)
trial = theta + delta
trial_loss = loss(trial)
if trial_loss < cur:
theta, cur, lam = trial, trial_loss, lam * 0.5
else:
lam *= 2.0
hist.append(theta.copy())
return np.array(hist)
theta0_bad = [0.05, 0.3]
h_gn_bad = gauss_newton(theta0_bad, max_iter=6)
h_lm_bad = levenberg_marquardt(theta0_bad, lam0=1.0, max_iter=30)
losses_gn_bad = np.clip([loss(t) for t in h_gn_bad], None, 1e6)
losses_lm_bad = [loss(t) for t in h_lm_bad]
fig, ax = plt.subplots(figsize=(8.5, 4))
ax.semilogy(range(len(losses_gn_bad)), losses_gn_bad, "-o", color="firebrick", label="Gauss-Newton (undamped)")
ax.semilogy(range(len(losses_lm_bad)), losses_lm_bad, "-o", ms=3, color="steelblue", label="Levenberg-Marquardt")
ax.axhline(loss([a_true, c_true]), color="forestgreen", ls="--", lw=1, label="noise floor")
ax.set_xlabel("iteration $t$"); ax.set_ylabel("$E(\\theta_t)$ (capped at $10^6$ for display)")
ax.set_title(f"Same problem, worse start $(a_0,c_0)=({theta0_bad[0]},{theta0_bad[1]})$", fontsize=10)
ax.legend(fontsize=9)
plt.tight_layout()
plt.show()
Undamped Gauss-Newton’s first step alone overshoots to a loss above \(10^{40}\) (clipped above for display) and then gets stuck; Levenberg-Marquardt spends a few steps growing \(\lambda\) to stay safe, then shrinks it again and converges cleanly.
Suppose \(f(\mathbf{x},\mathbf{y})\) is hard to minimize jointly, but easy to minimize over either variable with the other held fixed.
Alternating optimization
\[ \textbf{for } i=1,2,\dots \qquad \mathbf{x}_{i+1} \leftarrow \min_{\mathbf{x}} f(\mathbf{x},\mathbf{y}_i), \qquad \mathbf{y}_{i+1} \leftarrow \min_{\mathbf{y}} f(\mathbf{x}_{i+1},\mathbf{y}) \]
But: there is no guarantee of reaching a global (or even local) minimum. Even so, alternation is a great way to turn one hard joint problem into two easy sub-problems, often solvable in closed form.
k-means objective
Given data \(\vec{x}_1,\dots,\vec{x}_m\), find \(k\) centers \(\vec{y}_1,\dots,\vec{y}_k\) minimizing \[ E(\vec{y}_1,\dots,\vec{y}_k) \equiv \sum_{i=1}^m \min_{c\in\{1,\dots,k\}} \|\vec{x}_i-\vec{y}_c\|_2^2. \]
![]()
Introducing the assignment \(c_i\) explicitly splits the variables into two blocks:
\[\text{labels}\quad \{c_i\}\qquad\qquad\text{centers}\quad\{\vec{y}_j\}.\]

As-rigid-as-possible (ARAP) deformation
For a mesh with vertices \(v\) and edges \((v,w)\), find rotations \(R_v\) and new positions \(\vec{y}_v\) minimizing \[ \sum_{v}\sum_{(v,w)} \|R_v(\vec{x}_v-\vec{x}_w) - (\vec{y}_v-\vec{y}_w)\|_2^2 \qquad \text{subject to} \quad R_v\in SO(3). \]

Fix \(R_v\), optimize \(\vec{y}_v\):
A sparse SPD linear system, exactly what conjugate gradient was built for.
Fix \(\vec{y}_v\), optimize \(R_v\):
Decouples per vertex into a \(2\times2\) closed-form SVD (a rotation-fitting/Procrustes problem).
A hard, nonconvex joint problem, split by alternation into two problems we already know how to solve.
Recall the image-alignment demo:

Graduated optimization generalizes this:
| Method | Idea | Use when… |
|---|---|---|
| IRLS | repeatedly re-weight and re-solve a linear least-squares problem | objective like \(\sum_i f_i(\mathbf{x})[g_i(\mathbf{x})]^2\), e.g. \(L^1\)/robust regression |
| Homotopy continuation | continuously morph an easy problem into the hard one | related to graduated optimization, framed via topology |
| Particle swarm / simulated annealing | randomized, (mostly) gradient-free global search | gradients unavailable or unreliable; many local minima |
| Online optimization (regret, FTRL) | adapt as the objective itself changes over time | the objective \(f_t\) changes every iteration (e.g. streaming data) |
Rule of thumb: the more specific structure an objective has (sum-of-squares, blocks, convexity), the lighter and faster an algorithm can exploit it.
Implement Gauss-Newton, Levenberg-Marquardt, and k-means yourself, then play with the interactive demo above:
Open specialized_optimization_demo.ipynb
scipy.optimize.least_squares.
Class 7: Specialized Optimization Methods