Module 3 — The Calibration Curve Explorer#
Goal: Build intuition for the parameters that define the TEXAS calibration curve, without needing to read any equations.
The core idea#
The Scaled Ring Index increases with sea surface temperature. But it does not increase linearly — it follows an S-shaped curve. Warm tropical waters give high Scaled RI values; cold polar waters give low values; and the curve bends most steeply somewhere in between.
TEXAS fits this S-curve to a global dataset of coretop, culture and mesocosm observations. The fitted curve then lets you go in the other direction: given a measured Scaled RI from a sediment core, what temperature does it imply?
Two kinds of parameter#
The shape of the S-curve is controlled by four parameters — t₀, k, b, and v. These are properties of the calibration, shared by every sample.
TEXAS also carries two non-thermal predictors, because a sample’s GDGT distribution reflects more than temperature alone: the GDGT-2/GDGT-3 ratio (a marker of archaeal community composition) and nitrate concentration. These do not get their own curve. They shift the existing curve along the temperature axis, by an amount specific to each sample:
Because the shift happens inside the S-curve rather than being added on top of it, the predicted Scaled RI stays between b and 1 no matter how large the correction gets — which matters, since the Scaled RI is a ratio that cannot leave that range by definition.
The γ coefficients are in °C per predictor unit, so they read directly: a sample with this much G₂/₃ behaves like water that is γ·G₂/₃ °C colder.
The widget below lets you move each parameter and watch the curve respond in real time.
import numpy as np
import matplotlib.pyplot as plt
from ipywidgets import interact, FloatSlider, Layout
# The very same functions TEXAS uses internally for the calibration curve.
# Importing them here means this explorer can never drift from the package.
from TEXAS.models.logistics import generalized_logistic_fixed_upper
from TEXAS.models.multivariate import generalized_logistic_fixed_upper_t0shift
# Posterior medians of the published SST calibration (case tx.GHEB.sst.sri03.G23-N1p0),
# used as the starting position of every slider so the explorer opens on the real fit.
FIT = dict(t0=34.8, k=0.275, b=0.412, v=4.0)
GAMMA_G23 = 0.634 # degC per unit G2/3
GAMMA_NO3 = 2.79 # degC per log10 unit NO3
NO3_CUTOFF = 1.0 # umol/L; above this the nitrate term switches off
The generalized logistic curve#
The curve TEXAS uses is called a generalized logistic function:
If that looks intimidating, don’t worry — the widget below lets you develop a feel for each parameter without needing to parse the equation. The short version:
Parameter |
Plain English |
|---|---|
t₀ |
Where the curve sits on the temperature axis |
k |
How steeply the curve rises |
b |
How low the curve bottoms out in cold water |
v |
Whether the rise is symmetric or lopsided |
γ |
How far a sample’s non-thermal conditions slide t₀ |
Warning
t₀ is where the curve sits, not where it bends fastest. Those two are the same point only when v = 1. In general the steepest response is at
which for the fitted v of 2–4 lands roughly 4–5 °C below t₀. The right-hand panel of the widget marks the real one, so you can watch the two separate as you drag v. This is also why there is no single “sensitivity” number for TEXAS: dRI/dT varies about sixfold across the calibrated temperature range.
T_RANGE = np.linspace(-5, 45, 800)
# Simulated coretop scatter around the published fit, for visual context only.
rng = np.random.default_rng(42)
_t_ref = rng.uniform(-2, 30, 160)
_ri_ref = generalized_logistic_fixed_upper(_t_ref, **FIT)
_ri_ref = np.clip(_ri_ref + rng.normal(0, 0.04, len(_t_ref)), 0.02, 0.98)
SLIDER_STYLE = {'description_width': '190px'}
SLIDER_LAYOUT = Layout(width='520px')
def max_slope_temp(t0, k, v):
"""Temperature of steepest response: setting d2f/dT2 = 0 gives t0 - ln(v)/k."""
return t0 - np.log(v) / k
def plot_curve(t0=FIT['t0'], k=FIT['k'], b=FIT['b'], v=FIT['v'],
g23=0.0, no3=NO3_CUTOFF):
# Thermal-only curve: what the calibration predicts before any correction.
ri_thermal = generalized_logistic_fixed_upper(T_RANGE, t0=t0, b=b, k=k, v=v)
# Corrected curve: the non-thermal predictors shift t0, and nothing else.
ri = generalized_logistic_fixed_upper_t0shift(
T_RANGE, t0=t0, b=b, k=k, v=v,
gamma_G23=GAMMA_G23, gdgt23ratio=np.full_like(T_RANGE, g23),
gamma_NO3=GAMMA_NO3, no3=np.full_like(T_RANGE, no3),
no3_cutoff=NO3_CUTOFF,
)
shift = GAMMA_G23 * g23
if 0 < no3 < NO3_CUTOFF:
shift += GAMMA_NO3 * np.log10(no3)
t0_eff = t0 + shift
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
# ── left panel: the curve ────────────────────────────────────────────────
ax = axes[0]
ax.scatter(_t_ref, _ri_ref, s=18, alpha=0.30, color='gray', zorder=1,
label='simulated coretop scatter')
ax.plot(T_RANGE, ri_thermal, color='slategray', lw=1.6, ls='--', zorder=2,
label=f'thermal only (t₀ = {t0:.1f} °C)')
ax.plot(T_RANGE, ri, color='steelblue', lw=2.5, zorder=3,
label=f'with corrections (t₀,eff = {t0_eff:.1f} °C)')
ax.axhline(b, color='seagreen', lw=1.5, ls=':', alpha=0.8,
label=f'b = {b:.2f} (lower asymptote)')
ax.axhline(1.0, color='slategray', lw=1.0, ls=':', alpha=0.5,
label='upper asymptote = 1 (fixed)')
# The bound the parameterization guarantees: the curve cannot leave this band.
ax.axhspan(b, 1.0, color='seagreen', alpha=0.05, zorder=0)
ax.annotate('', xy=(t0_eff, b - 0.015), xytext=(t0, b - 0.015),
arrowprops=dict(arrowstyle='->', color='darkorange', lw=1.8))
ax.set_xlabel('Sea Surface Temperature (°C)')
ax.set_ylabel('Scaled Ring Index')
ax.set_xlim(-5, 45)
ax.set_ylim(-0.05, 1.08)
ax.set_title('Calibration curve — predictors slide it sideways',
fontweight='bold')
ax.legend(fontsize=8.5, loc='upper left')
ax.grid(True, alpha=0.2)
# ── right panel: where 1 degC matters most ───────────────────────────────
ax2 = axes[1]
dri_dt = np.gradient(ri, T_RANGE)
ax2.plot(T_RANGE, dri_dt, color='tomato', lw=2.5)
ax2.axvline(t0_eff, color='darkorange', lw=1.5, ls='--', alpha=0.8,
label=f't₀,eff = {t0_eff:.1f} °C (location)')
t_peak = max_slope_temp(t0_eff, k, v)
ax2.axvline(t_peak, color='purple', lw=1.5, ls='-', alpha=0.8,
label=f'steepest at {t_peak:.1f} °C = t₀,eff − ln(v)/k')
peak_sens = dri_dt.max()
ax2.annotate(f'peak sensitivity\n{peak_sens:.4f} RI / °C',
xy=(T_RANGE[np.argmax(dri_dt)], peak_sens),
xytext=(T_RANGE[np.argmax(dri_dt)] - 22, peak_sens * 0.80),
arrowprops=dict(arrowstyle='->', color='tomato'),
fontsize=9, color='tomato')
ax2.set_xlabel('Sea Surface Temperature (°C)')
ax2.set_ylabel('dRI / dT (sensitivity)')
ax2.set_xlim(-5, 45)
ax2.set_ylim(bottom=0)
ax2.set_title('Proxy sensitivity — where does 1 °C matter most?',
fontweight='bold')
ax2.legend(fontsize=8.5)
ax2.grid(True, alpha=0.2)
plt.suptitle(
f't₀={t0:.1f} °C k={k:.3f} b={b:.3f} v={v:.1f} '
f'| G₂/₃={g23:.1f}, NO₃={no3:.2f} µmol/L '
f'→ shift {shift:+.1f} °C',
fontsize=10, color='dimgray', y=1.02
)
plt.tight_layout()
plt.show()
interact(
plot_curve,
t0=FloatSlider(value=FIT['t0'], min=20, max=50, step=0.5,
description='t₀ — curve location (°C)',
style=SLIDER_STYLE, layout=SLIDER_LAYOUT),
k=FloatSlider(value=FIT['k'], min=0.05, max=0.60, step=0.005,
description='k — slope / steepness',
style=SLIDER_STYLE, layout=SLIDER_LAYOUT),
b=FloatSlider(value=FIT['b'], min=0.00, max=0.60, step=0.01,
description='b — lower asymptote',
style=SLIDER_STYLE, layout=SLIDER_LAYOUT),
v=FloatSlider(value=FIT['v'], min=0.1, max=8.0, step=0.1,
description='v — shape (curve asymmetry)',
style=SLIDER_STYLE, layout=SLIDER_LAYOUT),
g23=FloatSlider(value=0.0, min=0.0, max=6.0, step=0.1,
description='G₂/₃ of this sample',
style=SLIDER_STYLE, layout=SLIDER_LAYOUT),
no3=FloatSlider(value=NO3_CUTOFF, min=0.01, max=2.0, step=0.01,
description='NO₃ (µmol/L)',
style=SLIDER_STYLE, layout=SLIDER_LAYOUT),
);
What each parameter controls#
t₀ — where the curve sits#
Drag t₀ left and right. The whole curve slides along the temperature axis, and the sensitivity peak in the right panel slides with it.
What it means physically: t₀ locates the calibration on the temperature axis. If t₀ is far outside the temperature range of your samples, the proxy will be nearly flat over your data — it won’t discriminate well between warm and cool sites.
What it is not: the temperature at which the proxy responds fastest. That is t₀ − ln(v)/k, marked separately in the right panel. At the fitted v ≈ 4 and k ≈ 0.275 the gap is about 5 °C. Drag v and watch the orange and purple lines pull apart.
Try it: Push t₀ to 50 °C and notice how the curve becomes nearly a flat line across the −2 to 30 °C range most coretops live in. The right panel tells the same story: peak sensitivity moves out of the range of your data.
k — the slope#
Drag k from low to high. A high k makes the curve steep — Scaled RI jumps rapidly over a narrow temperature window. A low k makes it gradual.
What it means physically: k controls how discriminating the proxy is. High k means a small temperature difference produces a large Scaled RI difference, which sounds desirable — but it also means measurement noise on Scaled RI translates into large temperature uncertainty in cold reconstructions.
Try it: Set k very low (≈ 0.05). The sensitivity panel flattens — the proxy barely responds to temperature at all, so it would be a poor thermometer.
b — the lower asymptote#
Drag b. The bottom of the curve rises and falls. The upper asymptote is fixed at 1 in TEXAS, since the Scaled Ring Index cannot exceed 1 by definition. The shaded band between b and 1 is the range the curve is confined to.
What it means physically: b sets the minimum Scaled RI expected even in the coldest water. Some GDGT ring production is always present regardless of temperature (basal membrane fluidity), so b > 0 is physically realistic. The fitted value is about 0.41.
Try it: Compare b = 0 and b = 0.55. The high-b curve has less range to work with, compressing all temperature information into a narrower band of Scaled RI values.
v — asymmetry#
Drag v. At v = 1 you get a classic symmetric S-curve, and only then does the steepest point coincide with t₀. Increasing v skews the steepest part toward lower temperatures; decreasing it pushes the steep part toward higher temperatures.
What it means physically: the GDGT–temperature relationship is not symmetric. v lets the model fit that, and the fitted v ≈ 4 is what places the steepest response about 5 °C below t₀ — in the middle of the range most coretops occupy, rather than at its warm edge.
Try it: Set v = 0.3, then v = 8.0, watching the purple line in the right panel. A low v means the proxy discriminates best in warm tropical water; a high v means it discriminates best in cooler water.
γ — the non-thermal shift#
Drag G₂/₃ up from 0, or NO₃ down from 1.0 µmol/L. The dashed grey curve stays put — that is the thermal-only calibration — while the blue curve slides sideways. The orange arrow at the bottom shows the shift.
What it means physically: a sample’s archaeal community composition and nutrient environment change the temperature the membrane lipids record, not the ceiling or floor of the index. So they are modelled as a translation of the curve, in °C:
γ_G₂/₃ ≈ 0.63 °C per unit. More GDGT-2 relative to GDGT-3 shifts t₀ warmer, which lowers the predicted Scaled RI at any given temperature.
γ_NO₃ ≈ 2.79 °C per log₁₀ unit, applied only below the 1.0 µmol/L cutoff. Below that threshold log₁₀(NO₃) is negative, so nutrient-depleted water shifts t₀ cooler and raises the predicted Scaled RI.
Try it: Set NO₃ to 0.02 µmol/L — a strongly depleted, oligotrophic setting. The curve shifts several °C cooler, but look at the vertical axis: it still starts at b and still tops out at 1. That is the point of shifting t₀ rather than adding an offset to the response. No matter how extreme the correction, the prediction cannot leave the range a ratio is allowed to occupy.
Also try: push NO₃ above 1.0 µmol/L. The nitrate term switches off entirely and the curve snaps back — the correction only applies where nitrate is genuinely limiting.
What TEXAS actually does#
Rather than choosing fixed values for these parameters, TEXAS estimates a probability distribution over each — the posterior. After calibration, instead of “t₀ = 34.8 °C”, TEXAS knows “t₀ is 34.8 °C give or take 0.7”. That uncertainty flows through to every paleotemperature reconstruction you run.
The slider defaults above are the posterior medians of the published SST calibration, so the widget opens on the real fitted curve rather than an illustrative one.
Module 4 explains how TEXAS draws samples from those distributions. Module 5 shows how to interpret the resulting temperature credible intervals.