L-Curve Regularization Selection#
pycsamt.emtools.lcurve helps choose a regularization parameter from
a sweep of candidate models. Unlike most emtools pages, this module
does not require EDI files or Sites objects. It only needs three
arrays from a regularized inversion or smoothing experiment:
data misfit, for example
||Gm - d||;model roughness, for example
||Lm||;optional regularization values, usually named
lambda.
Full callable signatures live in the API reference. This page explains how to prepare the arrays, find the corner, plot the curve, compare methods, and turn the result into a reproducible report.
What The L-Curve Means#
Regularization is a trade-off. A small regularization parameter usually fits the data more closely but allows a rough model. A large regularization parameter usually makes the model smoother but increases the data misfit.
The L-curve plots these two quantities against each other:
The useful value is often near the “corner” of the curve: the point where adding more smoothness begins to cost noticeably more data fit. That value is not magic, but it is a defensible starting point when you need to choose a regularization level from a sweep.
Inputs Expected By The Module#
lcurve_table and plot_lcurve expect positive, finite arrays.
Internally, non-finite and non-positive values are filtered out before
log-scale calculations. The shortest valid array length controls the
number of rows used.
1import numpy as np
2
3lambdas = np.logspace(-3, 3, 60)
4roughness = 1.0 / (1.0 + lambdas**2)
5misfit = lambdas**2 / (1.0 + lambdas**2)
6
7# All three arrays should describe the same sweep order.
8assert misfit.shape == roughness.shape == lambdas.shape
9assert np.all(misfit > 0.0)
10assert np.all(roughness > 0.0)
The module does not compute the inversion itself. It only analyzes the numbers produced by your inversion, forward-model sweep, or smoothing experiment.
A Minimal Synthetic Example#
Start with a curve whose corner is easy to see. This is useful for checking that the mechanics are clear before using real inversion output.
1import numpy as np
2
3from pycsamt.emtools import lcurve_table, plot_lcurve
4
5lambdas = np.logspace(-3, 3, 40)
6
7# Small lambda: rough model, low misfit.
8# Large lambda: smooth model, higher misfit.
9roughness = 1.0 / (1.0 + lambdas**2)
10misfit = lambdas**2 / (1.0 + lambdas**2)
11
12table = lcurve_table(misfit, roughness, lambdas)
13corner_idx = table.attrs["corner_idx"]
14
15print(table.iloc[corner_idx])
16print("lambda* =", table["lam"].iloc[corner_idx])
17
18ax = plot_lcurve(misfit, roughness, lambdas)
19ax.figure.savefig("synthetic_lcurve.png", dpi=200)
rough 0.412354
misfit 0.587646
lam 1.193777
curv 1.333709
slope -0.711581
Name: 20, dtype: float64
lambda* = 1.1937766417144369
The returned table has columns rough, misfit, lam, curv,
and slope. The selected row is stored separately as
table.attrs["corner_idx"] so the table itself remains ordinary
pandas data.
Read The Table#
The table is the most important output because it lets you inspect the corner selection numerically.
1from pycsamt.emtools import lcurve_table
2
3table = lcurve_table(
4 misfit,
5 roughness,
6 lambdas,
7 method="curvature",
8 smooth=3,
9 skip=1,
10)
11
12corner = table.attrs["corner_idx"]
13row = table.iloc[corner]
14
15print(f"corner index: {corner}")
16print(f"lambda*: {row['lam']:.4g}")
17print(f"misfit: {row['misfit']:.4g}")
18print(f"roughness: {row['rough']:.4g}")
19print(f"corner score: {row['curv']:.4g}")
20print(f"local slope: {row['slope']:.4g}")
corner index: 20
lambda*: 1.194
misfit: 0.5876
roughness: 0.4124
corner score: 1.334
local slope: -0.7116
curv is the corner score. With method="curvature", it is the
numerical curvature of the log-log curve. With method="maxdist", it
is the perpendicular distance from the line connecting the first and
last log-log points.
Use A Dictionary Result#
For small helper functions or serialization, return_dict=True gives
arrays plus the selected corner index.
1from pycsamt.emtools import lcurve_table
2
3result = lcurve_table(
4 misfit,
5 roughness,
6 lambdas,
7 method="maxdist",
8 return_dict=True,
9)
10
11corner = result["corner"]
12lambda_star = result["lam"][corner]
13
14print(lambda_star)
0.8376776400682924
The dictionary keys are rough, misfit, lam, curv,
slope, and corner.
Sorting And Sweep Order#
The curve can be sorted by roughness, by lambda, or automatically.
The default sort="auto" sorts by lambda when the lambda array is
monotonic; otherwise it sorts by roughness.
1from pycsamt.emtools import lcurve_table
2
3by_lambda = lcurve_table(
4 misfit,
5 roughness,
6 lambdas,
7 sort="lambda",
8)
9
10by_roughness = lcurve_table(
11 misfit,
12 roughness,
13 lambdas,
14 sort="x",
15)
Use sort="lambda" when you want to preserve the physical sweep
direction. Use sort="x" when the lambda values are missing,
duplicated, or not meaningful and the curve should simply be ordered
from low to high roughness.
Corner Methods#
Two corner-picking methods are available.
Method |
How It Scores Points |
When To Prefer It |
|---|---|---|
|
Computes numerical curvature of the log-log curve. |
Smooth, well-sampled curves. |
|
Finds the point farthest from the line joining the two endpoints. |
Noisy curves, short sweeps, or uncertain smoothing. |
1from pycsamt.emtools import lcurve_table
2
3curvature = lcurve_table(
4 misfit,
5 roughness,
6 lambdas,
7 method="curvature",
8 smooth=3,
9)
10
11maxdist = lcurve_table(
12 misfit,
13 roughness,
14 lambdas,
15 method="maxdist",
16)
17
18j_curv = curvature.attrs["corner_idx"]
19j_dist = maxdist.attrs["corner_idx"]
20
21print("curvature lambda*:", curvature["lam"].iloc[j_curv])
22print("maxdist lambda*:", maxdist["lam"].iloc[j_dist])
curvature lambda*: 1.1937766417144369
maxdist lambda*: 0.8376776400682924
If the two methods choose similar values, the corner is probably stable. If they disagree strongly, inspect the curve and consider widening the lambda sweep.
Smoothing And Endpoint Skipping#
The smooth argument affects the curvature method by applying a
moving average in log-log space before differentiating. The skip
argument prevents the first and last points from being selected as the
corner.
1from pycsamt.emtools import lcurve_table
2
3for smooth in (1, 3, 5, 7):
4 table = lcurve_table(
5 misfit,
6 roughness,
7 lambdas,
8 method="curvature",
9 smooth=smooth,
10 skip=2,
11 )
12 j = table.attrs["corner_idx"]
13 print(f"smooth={smooth}: lambda*={table['lam'].iloc[j]:.4g}")
smooth=1: lambda*=1.194
smooth=3: lambda*=1.194
smooth=5: lambda*=0.8377
smooth=7: lambda*=0.8377
Be cautious with heavy smoothing. It can move the curvature maximum
away from the visual corner, especially for short curves. maxdist is
often a useful cross-check because it does not use numerical
derivatives.
Plot A Single Curve#
plot_lcurve draws roughness on the x-axis and misfit on the y-axis,
both in log scale. The selected corner is marked with a star by
default.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import plot_lcurve
4
5fig, ax = plt.subplots(figsize=(6.4, 4.8))
6
7plot_lcurve(
8 misfit,
9 roughness,
10 lambdas,
11 labels=["station 18-001A"],
12 method="curvature",
13 smooth=3,
14 show_inset=True,
15 ax=ax,
16)
17
18ax.set_title("L-curve regularization sweep")
19fig.tight_layout()
20fig.savefig("lcurve_18-001A.png", dpi=200)
21plt.close(fig)
The inset shows the corner score. For method="curvature" the inset
title is curv. For method="maxdist" it is knee.
Plot Multiple Curves#
Pass lists of misfit, roughness, and lambda arrays to compare several stations, inversion targets, or model parameterizations.
1import matplotlib.pyplot as plt
2
3from pycsamt.emtools import plot_lcurve
4
5curves_misfit = [
6 misfit,
7 misfit * 1.25 + 0.03,
8 misfit * 0.85 + 0.08,
9]
10curves_roughness = [
11 roughness,
12 roughness * 0.75 + 0.02,
13 roughness * 1.35 + 0.05,
14]
15curves_lambda = [lambdas, lambdas, lambdas]
16
17fig, ax = plt.subplots(figsize=(7.0, 5.0))
18plot_lcurve(
19 curves_misfit,
20 curves_roughness,
21 curves_lambda,
22 labels=["run A", "run B", "run C"],
23 method="maxdist",
24 show_inset=True,
25 ax=ax,
26)
27fig.savefig("lcurve_multi_run.png", dpi=200)
28plt.close(fig)
The plot uses one shared inset for all curves, so the corner scores can
be compared in the same small panel. The legend reports the selected
lambda* for each curve when lambda values are supplied.
Show Sweep Direction#
arrow_every draws arrows along the curve. This is helpful when you
want readers to see how increasing lambda moves through the trade-off.
1from pycsamt.emtools import plot_lcurve
2
3ax = plot_lcurve(
4 misfit,
5 roughness,
6 lambdas,
7 arrow_every=5,
8 show_points=False,
9 show_inset=False,
10)
11ax.figure.savefig("lcurve_with_lambda_direction.png", dpi=200)
Use arrows when teaching or reviewing a sweep. For compact reports, points plus the corner marker are usually enough.
Use L-Curve With A Real Smoothing Sweep#
The example below builds a small Tikhonov smoothing problem from one station’s apparent resistivity. It is not a full inversion; it is a transparent way to produce real misfit and roughness arrays.
1import numpy as np
2
3from pycsamt.emtools import ensure_sites, lcurve_table, plot_lcurve
4from pycsamt.emtools._core import _get_z_block, _iter_items, _name
5
6survey = ensure_sites("data/AMT/WILLY_DATA/L18PLT", strict=True)
7
8def second_difference(n):
9 operator = np.zeros((n - 2, n))
10 for i in range(n - 2):
11 operator[i, i] = 1.0
12 operator[i, i + 1] = -2.0
13 operator[i, i + 2] = 1.0
14 return operator
15
16def station_log10_rho_xy(sites, station):
17 for index, site in enumerate(_iter_items(sites)):
18 if _name(site, index) != station:
19 continue
20 _, z, freq = _get_z_block(site)
21 rho_xy = 0.2 * np.abs(z[:, 0, 1]) ** 2 / freq
22 return np.log10(rho_xy), freq
23 raise KeyError(station)
24
25def smoothing_sweep(data, lambdas):
26 n = data.size
27 identity = np.eye(n)
28 rough_operator = second_difference(n)
29 regularizer = rough_operator.T @ rough_operator
30
31 misfits = []
32 roughness = []
33 models = []
34
35 for lam in lambdas:
36 model = np.linalg.solve(
37 identity + lam**2 * regularizer,
38 data,
39 )
40 misfits.append(np.linalg.norm(model - data))
41 roughness.append(np.linalg.norm(rough_operator @ model))
42 models.append(model)
43
44 return np.array(misfits), np.array(roughness), np.array(models)
45
46lambdas = np.logspace(-3, 3, 60)
47data, freq = station_log10_rho_xy(survey, "18-001A")
48misfit, roughness, models = smoothing_sweep(data, lambdas)
49
50table = lcurve_table(misfit, roughness, lambdas)
51corner = table.attrs["corner_idx"]
52print("lambda* =", table["lam"].iloc[corner])
53
54ax = plot_lcurve(misfit, roughness, lambdas, labels=["18-001A"])
55ax.figure.savefig("lcurve_real_station.png", dpi=200)
lambda* = 0.34863652276780877
This code makes the L-curve inputs explicit. misfit measures how far
the smoothed model is from the observed log-resistivity curve.
roughness measures how curved the smoothed model remains after
applying the second-difference operator.
Inspect What Lambda Does#
After choosing a corner, plot the model at the corner against clearly under- and over-regularized choices. This is the best sanity check.
1import matplotlib.pyplot as plt
2
3period = 1.0 / freq
4corner = table.attrs["corner_idx"]
5under = 5
6over = 50
7
8fig, ax = plt.subplots(figsize=(7.0, 4.5))
9
10ax.semilogx(period, data, "o", color="0.55", label="observed")
11ax.semilogx(
12 period,
13 models[under],
14 "-",
15 label=f"under-regularized lambda={lambdas[under]:.3g}",
16)
17ax.semilogx(
18 period,
19 models[corner],
20 "-",
21 linewidth=2.2,
22 label=f"corner lambda={lambdas[corner]:.3g}",
23)
24ax.semilogx(
25 period,
26 models[over],
27 "-",
28 label=f"over-regularized lambda={lambdas[over]:.3g}",
29)
30
31ax.set_xlabel("Period (s)")
32ax.set_ylabel("log10 apparent resistivity")
33ax.legend(fontsize=8)
34fig.savefig("regularization_levels.png", dpi=200)
35plt.close(fig)
The under-regularized model should usually track too much local noise. The over-regularized model should usually be too smooth. The corner model should sit between them.
Common Failure Modes#
- Corner at the first or last lambda
The sweep probably does not bracket the useful region. Extend the lambda range and rerun the sweep. Increasing
skipcan hide the endpoint, but it cannot fix a badly chosen sweep.- Misfit or roughness contains zeros
Log-scale L-curve scoring requires positive values. The module filters non-positive values, but you should still investigate why they occurred.
- Curvature and max-distance disagree
The curve may be noisy, too short, too heavily smoothed, or not truly L-shaped. Plot both methods and inspect the model curves at both selected lambdas.
- Multiple curves have very different scales
That can be normal when stations or inversions differ strongly. Compare selected lambdas and model behavior, not only absolute misfit and roughness values.
- The curve is nearly a straight line
There may not be a meaningful trade-off in the swept range. Try a wider lambda interval or revisit the definition of misfit and roughness.
Save A Reproducible L-Curve Bundle#
This script writes the table, selected corner, plot, and a short text summary for one sweep.
1from pathlib import Path
2
3import matplotlib.pyplot as plt
4
5from pycsamt.emtools import lcurve_table, plot_lcurve
6
7out = Path("lcurve_report")
8out.mkdir(parents=True, exist_ok=True)
9
10table = lcurve_table(
11 misfit,
12 roughness,
13 lambdas,
14 method="curvature",
15 smooth=3,
16 skip=1,
17)
18corner = table.attrs["corner_idx"]
19table.to_csv(out / "lcurve_table.csv", index=False)
20
21corner_row = table.iloc[corner]
22with (out / "corner.txt").open("w", encoding="utf-8") as stream:
23 stream.write(f"corner_index: {corner}\n")
24 stream.write(f"lambda_star: {corner_row['lam']:.8g}\n")
25 stream.write(f"misfit: {corner_row['misfit']:.8g}\n")
26 stream.write(f"roughness: {corner_row['rough']:.8g}\n")
27 stream.write(f"score: {corner_row['curv']:.8g}\n")
28
29fig, ax = plt.subplots(figsize=(6.4, 4.8))
30plot_lcurve(
31 misfit,
32 roughness,
33 lambdas,
34 labels=["selected sweep"],
35 method="curvature",
36 smooth=3,
37 ax=ax,
38)
39fig.tight_layout()
40fig.savefig(out / "lcurve.png", dpi=200)
41plt.close(fig)
Worked Example#
The gallery example builds a real regularization sweep from bundled apparent-resistivity data and compares corner-picking methods.
Open the rendered gallery page here: L-curve regularization-parameter selection (pycsamt.emtools.lcurve).