Inverse problems, regularization and seismic imaging
Before this lesson: linear algebra 08 (SVD and pseudoinverse), linear algebra 12 (least squares), linear algebra 13 (conditioning) and lesson 03 (the adjoint method). Helpful: optimization 10 (Gauss–Newton) and the probability track (Bayes' rule with Gaussians). Time: about 100 minutes, plus the notebook.
Why this matters #
- Seismology. Earthquake location, travel-time tomography and full-waveform inversion (FWI) are inverse problems. FWI images the Earth's interior, from oil and gas reservoirs to the whole mantle, by fitting complete seismograms.
- Weather and climate. Data assimilation (3D-Var and 4D-Var, lesson 06) is a regularized inverse problem: find the atmospheric state from sparse observations, with a forecast as the prior.
- Imaging. Medical CT and MRI, deblurring of images and telescope data, and many more.
- Machine learning. Ridge regression is Tikhonov regularization. Gaussian-process regression (lesson 07) is the same Bayesian computation in function space. Learned regularizers, unrolled networks and diffusion-model priors are active research on inverse problems.
1. Predict first #
2. The picture #
Forward and inverse problems #
A forward map sends a model (what we want to know) to data (what we can measure): , with noise . When is linear we write with a matrix .
| problem | model | data | forward map |
|---|---|---|---|
| earthquake location | hypocentre and origin time | arrival times at stations | (nonlinear) |
| travel-time tomography | slowness in each cell | travel times of rays | = length of ray in cell (linear) |
| full-waveform inversion | velocity everywhere | seismograms | solve the wave equation (nonlinear) |
| deblurring | sharp signal | blurred, noisy signal | convolution with a blur (linear) |
Hadamard's conditions. A problem is well-posed if (1) a solution exists, (2) it is unique, and (3) it depends continuously on the data (stability). A problem that fails one of them is ill-posed. Inverse problems usually fail (3), often (2), and with noise also (1): noisy data are not exactly for any . After discretization the solution always depends continuously on , but the dependence can be extremely sensitive: the matrix is ill-conditioned (linear algebra 13).
Why undoing a blur fails #
A blur averages neighbouring values, so it damps fine details (high frequencies) strongly and broad features (low frequencies) weakly. In the SVD (linear algebra 08), the right singular vectors are patterns of increasing frequency, and the singular values fall quickly. The noise has the same size in every direction . The naive solution divides each data component by :
Where is tiny, the noise part is divided by a tiny number and dominates. Regularization keeps the components with large and damps the rest.
Move the regularization weight and the noise level, and switch the method. Upper chart: the true model (blue), the blurred noisy data (aqua dots) and the reconstruction (orange). Lower chart, on a log scale (the Picard plot): the singular values (dark line), the data coefficients (aqua), the naive coefficients (magenta) and the filter factors (orange). Notice that stops falling at the noise level, and from there the naive coefficients grow. With "naive", the reconstruction leaves the chart.
has , and the data have noise in the direction . What error does this noise cause in the naive solution, along ?
The naive solution has the component along . The noise part is . The forward map damps by ; the inverse map amplifies by . Multiplying would be the forward direction, not the inverse.
3. Definitions and main results #
Linear inverse problems and the SVD #
Let have the SVD with (for the nonzero ones). The minimum-norm least-squares solution (the pseudoinverse solution, linear algebra 08 and 12) is
Noise amplification. Let with noise . Here (without index) is the noise level. Then
Because the are orthonormal, each is again . So the error along has standard deviation . The condition number bounds how much a relative error in can grow in .
The discrete Picard condition (Hansen 1990). The naive coefficients stay bounded only if the exact data coefficients decay faster than , on average. Real data contain noise, and for every . So in the Picard plot, follows the decay of and then levels off at the noise level. Every component after that point is mostly noise, and dividing it by makes it explode.
Regularization: truncated SVD and Tikhonov #
Truncated SVD (TSVD) keeps the first components: .
Tikhonov regularization (Tikhonov 1963) adds a penalty on the size of the model:
Theorem 1 (filter factors). In the SVD basis,
Proof.
- The objective is a convex quadratic. Its gradient is zero at the minimum, so . The matrix is positive definite for , so the solution is unique.
- With (full , orthogonal): and .
- So . The diagonal matrix in the middle has entries .
What the filter factors do.
- : , the component is kept.
- : , the component is removed.
- : . So is the singular value where the filter switches.
- The noise gain is at most (its maximum, at ). So noise can be amplified by at most , instead of .
TSVD uses ; Tikhonov switches smoothly. Larger means less noise in (less variance) but more lost detail (more bias).
Tikhonov regularization with weight . A component has singular value . What filter factor does it get?
. The weight marks the singular value where the filter switches from "keep" ( for ) to "remove" ( for ). A hard cut-off at is truncated SVD, not Tikhonov.
Choosing .
- Discrepancy principle (Morozov 1966). A good model should fit the data only as well as the noise allows: choose so that . It needs the noise level, and it tends to choose an slightly too large (slightly too smooth).
- L-curve (Hansen 1992). Plot against for many . The curve has the shape of an L. On the vertical part, a smaller fits a little more noise and the model norm explodes; on the horizontal part, a larger removes signal and the residual grows. Choose the corner. It does not need the noise level.
- Generalized cross-validation (GCV) is a third common choice; it predicts each datum from the others.
Move along the L-curve, and change the noise level. Upper chart: the L-curve on log–log axes, with the current as a dot. Lower chart: the true error (magenta) against . The vertical marks show the corner of the L (largest curvature, dark), the discrepancy-principle choice (aqua) and the smallest error (blue). Notice that both rules land near the flat valley of the error curve without knowing ; the discrepancy choice is usually closer to the best , and the corner can miss it by a factor of several.
General-form Tikhonov and sparsity
Instead of one often penalizes with a derivative matrix , so smooth models are preferred, not small ones. The solution uses the generalized SVD of . With an penalty, (total variation when is a first difference), sharp edges survive. The problem stays convex but is no longer quadratic; proximal methods solve it (optimization 09).
The Bayesian view: Tikhonov is MAP #
Model the noise and the prior as Gaussians: and . By Bayes' rule the posterior density is
Theorem 2 (Tikhonov = MAP). The maximum a posteriori (MAP) estimate is the Tikhonov solution with . Equivalently, Tikhonov with weight is MAP for the prior . The posterior is Gaussian with mean and covariance
Proof.
- Maximizing is minimizing : the Tikhonov objective with .
- The exponent is a quadratic in with Hessian . A density of the form is Gaussian with covariance ; its mean is the minimizer, . Multiply by and use for the second form.
In the SVD basis the posterior variance along is . For large it is about : the data determine this direction. For small it is about , the prior variance: the data say nothing about this direction, and we keep the prior.
General Gaussian prior and noise. With and , MAP minimizes
This is exactly the 3D-Var cost of data assimilation (lesson 06), with background state and observation operator . Its posterior mean, written with kernel matrices, is the posterior mean of Gaussian-process regression (lesson 07).
The data noise is . For which prior on is the Tikhonov solution the MAP estimate?
MAP minimizes . Multiply by : the weight on is , which must equal . So . A large means a narrow prior. A uniform prior gives plain least squares ().
Nonlinear problems: Gauss–Newton and earthquake location #
For a nonlinear forward map we minimize the misfit . Let be the residual and the Jacobian of at . Gauss–Newton linearizes and solves the linear least-squares problem for the step:
It uses in place of the Hessian of , which drops the second derivatives of (optimization 10). Adding to gives the Levenberg–Marquardt method, a regularized step.
Earthquake location (Geiger's method, 1912). Stations at known positions record the arrival time of the first wave. In a medium with constant speed ,
where is the hypocentre and the origin time ( has 4 entries in 3D; the figure uses 2D and 3 entries). Row of the Jacobian is : a unit vector from the station to the source, divided by , and a 1 for .
Station geometry. If all stations lie on one side, the unit vectors are almost parallel, the rows of are almost dependent, and is ill-conditioned. The linearized posterior covariance (for timing noise ) is then a long ellipse: the source can move along one direction (often away from the stations, with a later or earlier ) almost without changing the arrival times. Depth and origin time trade off in the same way when all stations are far away.
Drag the stations (triangles) and the starting guess (orange), then press Gauss–Newton step. The shading is the misfit (stronger colour is larger), minimized over the origin time; the blue star is the true epicentre. The dashed orange ellipse is the linearized 95% uncertainty region at the current estimate. Try "one side" and "in a line": the misfit valley becomes long, the ellipse stretches, and with stations in a line there are two mirror minima.
The adjoint-state method #
Many forward maps are given by a large equation for a state (a wavefield, a temperature):
for example , where samples the state at the receivers. We need for millions of parameters.
Theorem 3 (adjoint-state gradient). Let be invertible and differentiable in . Let the adjoint state solve
Then
for every .
Proof (implicit differentiation, as in calculus 09).
- Differentiate with respect to : , so .
- Chain rule: .
- The row vector is , because . So .
Cost. One forward solve for and one adjoint solve for , then a cheap product for each . The number of solves does not depend on the number of parameters. With several sources, the cost is one forward and one adjoint solve per source.
The same result with a Lagrangian
- Define . When solves the state equation, for every .
- , with .
- Choose so that : this is . The unknown then drops out.
- What remains is .
Lesson 03 is the special case where is the whole time-stepping scheme: the matrix is block lower triangular in time, so is block upper triangular, and solving with it runs backward in time.
Full-waveform inversion #
In 1D, the acoustic wave equation with squared slowness and a source at is
A receiver at records . FWI minimizes the waveform misfit
Theorem 4 (FWI gradient). Let solve the adjoint wave equation with the residual as a source at the receiver and final conditions:
Then
Why this is true. This is Theorem 3 with . The derivative of with respect to is , and the pairing is an integral over time. The wave operator is symmetric in space, and its transpose in time turns the initial conditions into final conditions: the adjoint equation runs backward. Because the wave equation is time-reversible, "the residual played backward from the receiver" is a wave that travels back into the model.
Derivation with the Lagrangian
- .
- Perturb by with at . Integrate by parts twice in and twice in (the boundary terms in vanish for a large domain or for matching boundary conditions): .
- The term from is .
- Setting all terms with to zero gives the adjoint equation and the final conditions at .
- What remains is .
We checked the discrete version of this formula (leapfrog time stepping, the discrete adjoint run backward) against finite differences of the misfit; they agree to 8 digits. The notebook repeats the check.
What the gradient looks like. At each point the gradient is the zero-lag cross-correlation of the forward field's acceleration with the adjoint field. In a reflection setting (source and receiver at the surface) the residual contains the reflected waves that the starting model misses. Sent back into the model, they meet the forward field exactly at the depth of the reflector. So the first gradient is an image of the reflectors: this is the link between FWI and reverse-time migration. Also, unknowns need only two simulations per source (forward and adjoint).
Press Play (or drag the time) to run the adjoint backward from to 0. The source and the receiver are at the surface ; the starting model has , and the true model has a layer (shaded). Upper chart: the forward wavefield of the starting model (blue) and the adjoint field (violet) at the current time. Middle chart: the recorded trace (aqua), the predicted trace (orange, dashed) and the residual (magenta). Lower chart: the gradient accumulated so far (magenta). Notice that it grows only where and meet, at the edges of the layer, and that the image of the top of the layer changes sign when you switch to a slower layer.
FWI with one seismic source and unknown velocity values. How many wave simulations does one gradient of the misfit need with the adjoint-state method?
The forward simulation gives and the residual; the adjoint simulation, driven by the time-reversed residual, gives . The gradient at every point is then , a cheap product. The adjoint alone is not enough: the gradient needs the forward field too, and the adjoint's source is the residual of the forward simulation.
Cycle skipping #
FWI minimizes with gradient methods, so it finds a local minimum. Consider one arrival: the predicted wavelet is the observed wavelet shifted in time by . For a wavelet of period , the misfit has local minima near , where the predicted wavelet matches the wrong cycle of the observed one. If the starting model predicts the arrival wrong by more than about half a period, gradient descent moves toward the wrong cycle: this is cycle skipping. For the Ricker wavelet with peak frequency we use here, the basin of the correct minimum ends at , a little less than half of the period .
Remedies.
- Low frequencies first (Bunks et al. 1995). The period of a low-frequency wavelet is long, so the same time error is a small fraction of a period. Invert low-pass filtered data first, then add higher frequencies step by step.
- A good starting model, for example from travel-time tomography.
- Other misfits that compare shapes instead of point values, for example optimal-transport distances (Engquist and Froese 2014) or envelopes.
Drag the starting velocity and change the high frequency. The curves are the misfit against the model velocity for a low-frequency (blue) and a high-frequency (orange) wavelet; the true velocity is 2 km/s. The dots show where gradient descent from the starting velocity ends. Notice that the high-frequency curve has local minima, and that descent finds the true velocity only when the time error is below about half a period.
Why does FWI usually start with the lowest frequencies in the data?
Cycle skipping happens when the predicted arrival is wrong by more than about half a period. A long period gives a wide basin around the correct model. Once the low frequencies have fixed the large-scale velocity, the higher frequencies add detail. Low frequencies give less resolution, not more.
Machine learning and inverse problems
- Learned regularizers. Replace by a penalty learned from examples of good models, for example an adversarially trained critic (Lunz, Öktem and Schönlieb 2018).
- Unrolled optimization. Take iterations of a gradient or proximal method, give each iteration learnable parameters, and train the whole chain end to end (LISTA, Gregor and LeCun 2010; learned primal-dual, Adler and Öktem 2018). The forward map and its adjoint stay inside the network.
- Generative priors. Plug-and-play methods use a denoiser as the prior step (Venkatakrishnan et al. 2013); diffusion posterior sampling uses a pretrained diffusion model as the prior and the gradient of the data misfit for guidance (Chung et al. 2023; lessons 12–13 for the diffusion side).
- Differentiable simulators. Write the forward model in an automatic-differentiation framework, and the adjoint-state gradient comes for free (the discrete adjoint of lesson 03). This also lets a neural network inside the simulator be trained through the physics.
- Amortized and simulation-based inference. Train a network on simulated pairs to output the posterior for any new in one pass (Cranmer, Brehmer and Louppe 2020).
- In seismology, deep networks already pick the arrival times that earthquake location uses (for example PhaseNet, Zhu and Beroza 2019).
Answers to section 1. (1) : the noise component is divided by . (2) One half: . (3) Two: one forward and one adjoint wave simulation, whatever the number of unknowns (Theorem 3).
4. Worked example #
(a) Regularization in two dimensions. Let and , and
is symmetric with eigenvalues and , so its SVD has , , , and . Take and noise .
- Data: (it lies along ), so .
- Coefficients: and .
- Naive solution: . A noise of size caused an error of size : amplification .
- Tikhonov with : and .
- . The error is instead of .
The regularized solution lost almost nothing along (bias ) and removed almost all of the noise along .
(b) The adjoint-state method by hand. Let
at .
- Forward solve: , , so .
- Residual: .
- Adjoint solve ( is symmetric): .
- Derivatives of the operator: , so ; likewise .
- Gradient: and .
Check: central finite differences of give and ✓. Two linear solves gave both derivatives; with parameters it would still be two solves.
5. In code #
NumPy checks the deblurring numbers, the discrepancy principle and the adjoint-state example. The notebook has these cells and the exercises.
import numpy as np
# 1. A 1D blur (deconvolution) problem: d = G m + noise
n = 64
x = (np.arange(n) + 0.5) / n
w = 0.03 # width of the Gaussian blur
G = np.exp(-(x[:, None] - x[None, :]) ** 2 / (2 * w**2)) / (np.sqrt(2 * np.pi) * w * n)
m_true = ((x > 0.15) & (x < 0.35)) + 0.8 * np.exp(-((x - 0.68) / 0.07) ** 2)
sigma = 1e-3 # noise level
d = G @ m_true + sigma * np.random.default_rng(0).normal(size=n)
U, s, Vt = np.linalg.svd(G)
print(f"{s[0]:.3f} {s[40]:.1e} {s[0] / s[-1]:.1e}") # 0.996 1.0e-03 3.3e+07
m_naive = np.linalg.solve(G, d)
print(f"{np.linalg.norm(m_naive - m_true):.1e}") # 5.1e+04: the naive inverse is useless
# 2. Tikhonov through the filter factors f_i = s_i^2 / (s_i^2 + alpha^2)
beta = U.T @ d # coefficients u_i^T d
def tikhonov(alpha):
f = s**2 / (s**2 + alpha**2)
return Vt.T @ (f * beta / s)
for alpha in [1e-4, 1e-3, 1e-2, 1e-1]:
m = tikhonov(alpha)
print(f"{alpha:.0e} residual {np.linalg.norm(G @ m - d):.2e} error {np.linalg.norm(m - m_true):.2f}")
# 1e-04 residual 3.09e-03 error 8.60
# 1e-03 residual 3.64e-03 error 1.17
# 1e-02 residual 4.91e-03 error 0.54
# 1e-01 residual 4.99e-02 error 0.69
# 3. Discrepancy principle: choose alpha with ||G m_alpha - d|| = sqrt(n) sigma (bisection in log alpha)
lo, hi = -6.0, 0.0
for _ in range(60):
mid = (lo + hi) / 2
r = np.linalg.norm(G @ tikhonov(10**mid) - d)
lo, hi = (mid, hi) if r < np.sqrt(n) * sigma else (lo, mid)
a_dp = 10**lo
print(f"alpha {a_dp:.3f} error {np.linalg.norm(tikhonov(a_dp) - m_true):.2f}") # alpha 0.025 error 0.57
# 4. Adjoint-state gradient for A(m) u = s, J = |u - d|^2 / 2, checked with finite differences
def A(m): return np.array([[2 + m[0], -1.0], [-1.0, 2 + m[1]]])
src, dat = np.array([1.0, 0.0]), np.array([0.5, 0.5])
def J(m): u = np.linalg.solve(A(m), src); return 0.5 * np.sum((u - dat) ** 2)
m0 = np.zeros(2)
u = np.linalg.solve(A(m0), src) # forward solve
lam = np.linalg.solve(A(m0).T, -(u - dat)) # adjoint solve: A^T lam = -grad_u J
grad = np.array([lam[0] * u[0], lam[1] * u[1]]) # lam^T (dA/dm_k) u, with dA/dm_k = e_k e_k^T
fd = [(J(m0 + 1e-6 * e) - J(m0 - 1e-6 * e)) / 2e-6 for e in np.eye(2)]
print(grad, np.round(fd, 8)) # [-0.03703704 0.01851852] [-0.03703704 0.01851852]
The residual stays near for small because the model can fit almost all of the data, noise included; the target of the discrepancy principle is . The smallest error in this list is at ; the discrepancy principle chooses a slightly larger , as expected.
6. Where it shows up #
- Global and regional seismology. Adjoint tomography fits full seismograms of many earthquakes with the adjoint-state gradient (Tromp, Tape and Liu 2005). Global models of the mantle have been built this way from hundreds of earthquakes (for example GLAD-M15, Bozdağ et al. 2016).
- Exploration geophysics. FWI is a standard step in building velocity models for reservoirs (review: Virieux and Operto 2009). The first gradient is closely related to reverse-time migration.
- Earthquake catalogs. Every earthquake catalog comes from solving a location problem for each event. Relative methods such as double-difference location (Waldhauser and Ellsworth 2000) reduce the effect of an imperfect velocity model.
- Weather and climate. 3D-Var and 4D-Var are regularized inverse problems with the forecast as the prior (lesson 06); 4D-Var uses the adjoint of the forecast model, as FWI uses the adjoint of the wave equation.
- Machine learning. Ridge regression is Tikhonov regularization with the data matrix as . Weight decay is the same penalty on network weights. Learned reconstruction methods for CT and MRI combine the forward operator with trained networks.
7. Common mistakes #
- Inverting an ill-conditioned matrix directly.
np.linalg.solve(G, d)returns an answer, but it is dominated by amplified noise. Look at the singular values and the Picard plot first. - Choosing by the best fit to the data. The residual always falls as ; the best fit is the noisiest model. Use the discrepancy principle, the L-curve, GCV or a held-out set.
- Mixing up and . With the objective the filter factor switches at and the prior variance is . Libraries often write with .
- A wrong sign or a missing time reversal in the adjoint. The adjoint wave equation has final conditions and runs backward. Check every adjoint gradient against finite differences on a small problem before using it.
- Starting FWI with high frequencies or a poor model. The misfit is not convex; cycle skipping gives a wrong model with a small gradient. Start from low frequencies and a smooth, travel-time-based model.
- Reading the regularized solution as the truth. Components with small come from the prior, not from the data. Report the posterior uncertainty or the resolution.
8. Check yourself #
- State Hadamard's three conditions. Which one fails for deblurring, and why does a discrete blur
matrix still "work" in exact arithmetic?
Answer
Existence, uniqueness, continuous dependence on the data. Stability fails: tiny changes in the data cause huge changes in the solution. A discrete blur matrix is usually invertible, so the solution depends continuously on , but the constant (the condition number, here about ) is so large that rounding and noise dominate. - Write the Tikhonov solution in the SVD basis. What is the largest possible amplification of a data
error, and for which singular value does it happen?
Answer
. The gain is largest at (set its derivative to zero), where it equals . - The noise level is and there are data. What residual norm does the
discrepancy principle aim for?
Answer
, the expected size of the noise vector. - Give the posterior covariance of the Tikhonov problem with noise , and its variance
along . What happens for ?
Answer
; along the variance is . For it tends to , the prior variance: the data do not constrain that direction. - Four stations lie on a line, and an earthquake happens off the line. What can go wrong in the
location, and why?
Answer
The mirror image of the source across the line has exactly the same distances to all stations, so it predicts the same arrival times: the problem has two solutions (uniqueness fails). Gauss–Newton converges to the one on the side of the starting guess. Near the line, the misfit is also very flat across it. - For with a fixed matrix , give the adjoint-state gradient of
.
Answer
Solve and . Since , : the gradient is the elementwise product .
State Hadamard's three conditions for a well-posed problem.
A solution exists, it is unique, and it depends continuously (stably) on the data.
Write the naive SVD solution of and say why it fails.
. The noise part explodes where is tiny.
What are the Tikhonov filter factors?
: about 1 for , about 0 for , and at .
What does the discrete Picard condition say?
The exact data coefficients must decay faster than the singular values , so that stays bounded.
For which prior is Tikhonov regularization the MAP estimate, with noise ?
with , that is, .
What residual norm does the discrepancy principle aim for?
, the expected norm of the noise vector.
Write one Gauss–Newton step for .
Solve and set .
Give the adjoint-state gradient for and , and its cost.
, then . One forward and one adjoint solve per source, for any number of parameters.
Give the FWI gradient for .
, where solves the wave equation backward from with the residual injected at the receivers.
When does FWI cycle-skip?
When the predicted arrival times are wrong by more than about half a period of the wavelet; gradient descent then fits the wrong cycle.
9. Interview angle #
- "Why not just invert the matrix?" — Small singular values amplify the noise by . Show the singular values and the Picard plot, then regularize.
- "Explain ridge regression as a Bayesian method." — It is MAP with a Gaussian prior on the weights; the posterior covariance is .
- "How do you choose the regularization weight?" — Discrepancy principle if the noise level is known; L-curve or GCV otherwise; cross-validation on held-out data in ML.
- "How do you get the gradient for millions of parameters in a PDE-constrained problem?" — The adjoint-state method: one forward and one adjoint solve; derive it with a Lagrangian; check it against finite differences.
- "What is cycle skipping and how do you avoid it?" — A local minimum when the arrival is off by more than half a period; use low frequencies first, a good starting model, or a misfit that is less oscillatory.
10. Practice #
The notebook for this lesson has five exercises, each with a check cell and a folded solution:
- Build the blur matrix, see the naive inverse fail, and fix it with truncated SVD and Tikhonov.
- Choose with the discrepancy principle and with the corner of the L-curve; compare with the best .
- Locate an earthquake with Gauss–Newton from 5 stations, and compare the uncertainty ellipses of two station geometries.
- The adjoint-state gradient for a 1D problem with receivers, checked with finite differences.
- Misfit curves of a 1D wave simulation for a low and a high frequency: count the local minima (cycle skipping).
11. Watch and read #
- A. Tarantola, Inverse Problem Theory and Methods for Model Parameter Estimation (SIAM 2005): the Bayesian view of inverse problems, with many geophysical examples.
- S. G. Johnson, Notes on adjoint methods for 18.335: the adjoint of in two pages.
- R.-E. Plessix, A review of the adjoint-state method for computing the gradient of a functional with geophysical applications, Geophysical Journal International 167 (2006).
- J. Virieux and S. Operto, An overview of full-waveform inversion in exploration geophysics, Geophysics 74 (2009).
- A. M. Stuart, Inverse problems: a Bayesian perspective, Acta Numerica 19 (2010).
- J. Tromp, C. Tape and Q. Liu, Seismic tomography, adjoint methods, time reversal and banana-doughnut kernels, Geophysical Journal International 160 (2005).
Runs in your browser with nothing to install. The notebook repeats the key demos and has exercises with checks; solutions are inside, folded.
No Google account needed: Python runs inside your browser. The first start downloads about 40 MB; your changes are saved in this browser only.