MR Thermometry¶
This guide covers MR thermometry methods using the qmri.thermometry module.
Overview¶
MR thermometry enables non-invasive temperature measurement. The qmri.thermometry module provides two methods:
- Multi-echo dual-resonance: Absolute temperature from ethylene glycol phantoms
- PRF shift: Relative temperature changes for in-vivo thermal therapy monitoring
Multi-Echo Dual-Resonance Thermometry¶
The multi-echo method is designed for ethylene glycol phantom thermometry, providing absolute temperature measurements from multi-echo magnitude data.
Overview¶
Unlike PRF thermometry which measures relative temperature changes from phase, the dual-resonance method:
- Uses magnitude data (not phase)
- Provides absolute temperature (not relative change)
- Is calibrated for ethylene glycol phantoms
- Requires multiple echoes to fit the signal model
This method is ideal for phantom calibration and validation studies.
Physical Principle¶
Ethylene glycol contains two distinct chemical species (CH₂ and OH groups) that produce a characteristic beat pattern in the multi-echo signal. The frequency of this beat pattern depends on temperature, allowing absolute temperature measurement.
Basic Usage¶
import numpy as np
from qmri.thermometry import multiecho
# Example: Fit synthetic data at known temperature
magnetic_field = 3.0 # Tesla
temperature_true = 25.0 # Celsius
# Generate echo times (24 echoes from 1-24 ms)
echo_times = np.linspace(0.001, 0.024, 24)
# Calculate expected frequency difference for this temperature
df = multiecho.calculate_df_from_temperature(temperature_true, magnetic_field)
# Generate synthetic signal
signal = multiecho.thermometry_signal_model(
t=echo_times,
amplitude_1=1.0,
amplitude_2=0.5,
r2star_1=30.0,
r2star_2=40.0,
df=df,
dphi_deg=0.0,
)
# Fit the model to recover temperature
result = multiecho.fit_multiecho_thermometry(
signal, echo_times, magnetic_field, method="single"
)
print(f"Fitted temperature: {result.temperature:.1f} °C")
print(f"True temperature: {temperature_true:.1f} °C")
print(f"R²: {result.r_squared:.4f}")
Signal Model Visualisation¶
The dual-resonance signal shows a characteristic oscillating decay:
import numpy as np
import matplotlib.pyplot as plt
from qmri.thermometry import multiecho
# Parameters
echo_times = np.linspace(0.001, 0.050, 200)
magnetic_field = 3.0
# Generate signals at different temperatures
temperatures = [20, 25, 30, 35]
plt.figure(figsize=(10, 6))
for temp in temperatures:
df = multiecho.calculate_df_from_temperature(temp, magnetic_field)
signal = multiecho.thermometry_signal_model(
echo_times, 1.0, 0.5, 30.0, 40.0, df, 0.0
)
plt.plot(echo_times * 1000, signal, label=f"{temp}°C")
plt.xlabel("Echo Time (ms)")
plt.ylabel("Signal")
plt.title("Dual-Resonance Signal at Different Temperatures")
plt.legend()
plt.grid(True)
plt.show()
Temperature-Frequency Conversion¶
The calibration is specific to ethylene glycol:
from qmri.thermometry import multiecho
# Convert temperature to frequency difference
temperature = 25.0
b0 = 3.0 # Tesla
df = multiecho.calculate_df_from_temperature(temperature, b0)
print(f"At {temperature}°C and {b0}T: Δf = {df:.1f} Hz")
# Convert back
recovered_temp = multiecho.calculate_temperature_from_df(df, b0)
print(f"Recovered temperature: {recovered_temp:.1f}°C")
MultiEchoResult Dataclass¶
@dataclass(frozen=True)
class MultiEchoResult:
temperature: float # Fitted temperature in °C
temperature_uncertainty: float # Uncertainty in °C
df: float # Fitted frequency difference in Hz
r_squared: float # Coefficient of determination
fitted_params: NDArray # All fitted parameters
n_bootstrap: int | None # Number of bootstrap samples (if used)
Fit Quality Assessment¶
Always check the R² value to assess fit quality:
result = multiecho.fit_multiecho_thermometry(signal, echo_times, magnetic_field)
if result.r_squared > 0.95:
print(f"Good fit: Temperature = {result.temperature:.1f}°C")
elif result.r_squared > 0.9:
print(f"Acceptable fit: Temperature = {result.temperature:.1f}°C")
else:
print("Poor fit - check data quality")
Segmentation-Driven Image Fitting¶
For whole-image phantom data, fit_multiecho_thermometry_image fits the
dual-resonance model over a segmented volume. The segmentation defines discrete
regions by integer label (label 0 is background and is ignored), and each
non-zero region is fitted and converted to temperature.
import numpy as np
from qmri.thermometry import multiecho
# 4D multi-echo magnitude volume (nx, ny, nz, n_echoes) and a 3D label map
signal = ... # shape (nx, ny, nz, n_echoes)
segmentation = ... # shape (nx, ny, nz), integer labels
echo_times = np.linspace(0.001, 0.024, 24) # seconds
temperature_map, regions = multiecho.fit_multiecho_thermometry_image(
signal,
segmentation,
echo_times,
magnetic_field_tesla=3.0,
method="regionwise",
)
for region in regions:
print(
f"region {region.region_id} "
f"({region.region_size} voxels): "
f"{region.temperature:.2f} ± {region.temperature_uncertainty:.2f} °C"
)
The method argument selects how each region is summarised:
| Method | Behaviour |
|---|---|
regionwise |
Fit the mean signal of each region once; uncertainty from the fitted Δf covariance. Fastest. |
voxelwise |
Fit every voxel independently; region summary is the inverse-variance weighted mean of voxel temperatures. |
regionwise_bootstrap |
Resample region voxels with replacement, fit each sample's mean signal, and summarise with the mean and standard deviation over samples passing the R² threshold. |
Each entry in the returned list is a RegionThermometryResult with per-fit
temperatures, uncertainties, R² values and fitted parameters, plus a
to_dict() method for JSON serialisation.
Robust starting values (df_init)
The non-linear fit must be seeded with a starting frequency, and the
dual-resonance magnitude signal has local minima spaced roughly one over the
echo-train span apart — so a poor seed can converge to the wrong frequency
alias (e.g. a 10 °C phantom mis-fitted as ~185 °C on aliasing-prone grids).
The df_init argument (on fit_multiecho_thermometry,
fit_multiecho_thermometry_image and the pipeline) selects the strategy:
"multistart"(default): fit from both a fixed default and a data-driven Lomb-Scargle estimate of the beat frequency (from the envelope-removed signal), keeping the higher-R² result. Most robust."fixed": a single fit from the fixed default — cheapest, adequate for well-conditioned acquisitions but can alias on cold phantoms."lombscargle": a single fit seeded from the Lomb-Scargle estimate.
On well-sampled real phantom data all three typically agree to a few tenths
of a degree; multistart is the default as cheap insurance against aliasing
on other echo-time schemes.
End-to-End Pipeline¶
The qmri-pipelines package wraps the segmentation-driven fit into a
file-in / file-out workflow. It loads multi-echo NIfTIs, concatenates and sorts
echoes by echo time, detects the field strength from JSON sidecars, fits, and
writes a temperature-map NIfTI plus a JSON report.
from qmri.pipelines.thermometry import run_multiecho_thermometry
temperature_map, report = run_multiecho_thermometry(
multiecho_files=["echo_block_1.nii.gz", "echo_block_2.nii.gz"],
segmentation_file="labels.nii.gz",
echo_times_files=["te_block_1.txt", "te_block_2.txt"],
method="regionwise",
output_dir="results/",
)
for region in report.regions:
print(f"region {region.region_id}: {region.temperature:.2f} °C")
The magnetic field strength is read from a JSON sidecar (ImagingFrequency or
MagneticFieldStrength) next to the first image unless magnetic_field_tesla
is supplied explicitly.
The same pipeline is available from the command line — see the CLI Reference:
qmri thermometry multiecho echo_block_1.nii.gz echo_block_2.nii.gz \
-e te_block_1.txt -e te_block_2.txt \
-s labels.nii.gz --method regionwise -o results/
When to Use Multi-Echo vs PRF¶
| Use Case | Method |
|---|---|
| In-vivo thermal therapy monitoring | PRF |
| Phantom temperature calibration | Multi-echo |
| Relative temperature change | PRF |
| Absolute temperature measurement | Multi-echo |
| Phase-sensitive sequences | PRF |
| Magnitude-only data | Multi-echo |
Best Practices¶
- Use sufficient echo times: At least 20+ echoes spanning multiple beat periods
- Check R² values: Values > 0.95 indicate good fits
- Verify field strength: Ensure correct B0 value is used
- Temperature range: The ethylene glycol calibration is valid from ~10°C to ~60°C
- SNR requirements: Higher SNR improves fitting accuracy
References¶
-
Sprinkhuizen, S.M., Bakker, C.J.G. and Bartels, L.W. "Absolute MR thermometry using time-domain analysis of multi-gradient-echo magnitude images." MRM 64:239-248, 2010.
-
Raiford, D.S., Fisk, C.L. and Becker, E.D. "Calibration of methanol and ethylene glycol nuclear magnetic resonance thermometers." Anal. Chem. 51(12):2050-2051, 1979.
PRF Thermometry¶
The PRF shift method is the most widely used technique for MR-guided thermal interventions.
Physical Principle¶
The PRF method exploits the temperature dependence of the water proton resonance frequency. As temperature increases:
- Hydrogen bonds between water molecules break
- Electron screening of water protons decreases
- The local magnetic field experienced by protons changes
- The resonance frequency shifts (decreases with increasing temperature)
This frequency shift manifests as a phase change in gradient echo images.
Key Equation¶
The temperature-induced phase shift is:
And the temperature change can be calculated from the phase difference:
where:
- \(\Delta\phi\) is the phase difference (radians)
- \(\gamma\) is the gyromagnetic ratio (42.58 MHz/T for protons)
- \(\alpha\) is the PRF thermal coefficient (~-0.01 ppm/degC)
- \(B_0\) is the magnetic field strength (T)
- \(TE\) is the echo time (s)
- \(\Delta T\) is the temperature change (degC)
PRF Thermal Coefficient¶
The PRF thermal coefficient for water is approximately -0.01 ppm/degC. The negative sign indicates that:
- Increasing temperature causes a negative phase shift
- The resonance frequency decreases with increasing temperature
from qmri.thermometry.prf import PRF_THERMAL_COEFFICIENT
print(f"PRF coefficient: {PRF_THERMAL_COEFFICIENT * 1e6:.3f} ppm/°C")
# PRF coefficient: -0.010 ppm/°C
Calculating Temperature from Phase¶
Basic Usage¶
import numpy as np
from qmri.thermometry import prf
# Measured phase difference (radians)
# Negative phase = temperature increase
phase_difference = -0.16 # radians
# Calculate temperature change
result = prf.calculate_temperature(
phase_difference=phase_difference,
echo_time=0.020, # 20 ms
magnetic_field=3.0, # 3 Tesla
)
print(f"Temperature change: {result.temperature_change:.1f} °C")
With Baseline Temperature¶
By default, the calculation assumes a baseline temperature of 37degC (body temperature). You can specify a different baseline:
result = prf.calculate_temperature(
phase_difference=-0.16,
echo_time=0.020,
magnetic_field=3.0,
baseline_temperature=25.0 # Room temperature baseline
)
absolute_temperature = 25.0 + result.temperature_change
print(f"Absolute temperature: {absolute_temperature:.1f} °C")
Volume Processing¶
The functions handle multi-dimensional data:
import numpy as np
from qmri.thermometry import prf
# Phase maps from baseline and heated states
phase_baseline = np.random.rand(128, 128) * 2 * np.pi - np.pi
phase_heated = phase_baseline - 0.1 # Simulated heating
# Calculate phase difference
phase_diff = phase_heated - phase_baseline
# Calculate temperature map
result = prf.calculate_temperature(
phase_difference=phase_diff,
echo_time=0.015,
magnetic_field=1.5
)
print(f"Temperature map shape: {result.temperature_change.shape}")
print(f"Mean temperature change: {np.nanmean(result.temperature_change):.1f} °C")
Simulating Phase Shifts¶
Use signal_phase_shift() to calculate the expected phase shift for a given temperature change:
import numpy as np
from qmri.thermometry import prf
# Calculate phase shift for various temperature changes
temperatures = np.array([1, 5, 10, 20, 30]) # °C
phase_shifts = prf.signal_phase_shift(
temperature_change=temperatures,
echo_time=0.020, # 20 ms
magnetic_field=3.0 # 3T
)
for t, phi in zip(temperatures, phase_shifts):
print(f"ΔT = {t:2d} °C → Δφ = {np.degrees(phi):6.2f}° ({phi:.3f} rad)")
Expected output at 3T with TE=20ms:
| Temperature Change | Phase Shift |
|---|---|
| 1 degC | ~0.8 deg |
| 5 degC | ~4 deg |
| 10 degC | ~8 deg |
| 20 degC | ~16 deg |
PRFResult Dataclass¶
@dataclass(frozen=True)
class PRFResult:
temperature_change: NDArray[np.floating] # Temperature change in °C
phase_difference: NDArray[np.floating] # Phase difference in radians
Considerations for Accuracy¶
1. Phase Wrapping¶
Phase values are typically wrapped to [-pi, pi]. For large temperature changes (>20-30degC at typical parameters), phase unwrapping may be required:
import numpy as np
def unwrap_phase_2d(wrapped_phase):
"""Simple 2D phase unwrapping."""
return np.unwrap(np.unwrap(wrapped_phase, axis=0), axis=1)
# Apply unwrapping before temperature calculation
unwrapped_diff = unwrap_phase_2d(phase_heated - phase_baseline)
result = prf.calculate_temperature(unwrapped_diff, echo_time=0.02, magnetic_field=3.0)
2. Fat Tissue¶
Important: Fat does not exhibit PRF temperature sensitivity because:
- Fat protons have different chemical environment
- Hydrogen bonding structure differs from water
This means:
- Fat regions show no temperature change (useful as reference)
- Fat suppression or water-fat separation is often used
- Fat can be used for drift correction
3. Field Drift¶
B0 field drift over time causes apparent temperature changes:
# Example: Using a reference region for drift correction
def correct_drift(temp_map, reference_mask):
"""Correct field drift using a reference region."""
drift = np.nanmean(temp_map[reference_mask])
return temp_map - drift
# Use subcutaneous fat or an unheated region as reference
corrected_temp = correct_drift(result.temperature_change, fat_mask)
4. Motion¶
Motion between baseline and heated acquisitions causes artefacts. Strategies include:
- Registration: Align images before subtraction
- Multi-baseline: Acquire multiple baselines and select best match
- Referenceless methods: Estimate background phase from heated image
5. Susceptibility Effects¶
Magnetic susceptibility changes with temperature, causing additional phase shifts. This is typically a small effect but may be relevant for:
- High-precision measurements
- Interfaces between tissues
- Regions near air-tissue boundaries
6. Echo Time Selection¶
The choice of TE involves a trade-off:
| Longer TE | Shorter TE |
|---|---|
| Larger phase shift | Smaller phase shift |
| Lower SNR | Higher SNR |
| More T2* decay | Less T2* decay |
| Better temperature sensitivity | More robust signal |
Recommendation: TE ~ 10-20 ms at 1.5T-3T provides a good balance.
Complete Example: Thermal Ablation Monitoring¶
import numpy as np
from qmri.thermometry import prf
# Acquisition parameters
te = 0.012 # 12 ms
b0 = 3.0 # 3 Tesla
# Simulate a focal heating pattern
x = np.linspace(-32, 32, 128)
y = np.linspace(-32, 32, 128)
X, Y = np.meshgrid(x, y)
# Gaussian heating pattern (focal ablation)
sigma = 5 # mm
peak_temp_rise = 30 # °C
temp_true = peak_temp_rise * np.exp(-(X**2 + Y**2) / (2 * sigma**2))
# Calculate expected phase shift
phase_shift_true = prf.signal_phase_shift(
temperature_change=temp_true,
echo_time=te,
magnetic_field=b0
)
# Add noise (typical phase noise ~0.05 rad)
noise_std = 0.05
phase_shift_noisy = phase_shift_true + np.random.normal(0, noise_std, phase_shift_true.shape)
# Recover temperature
result = prf.calculate_temperature(
phase_difference=phase_shift_noisy,
echo_time=te,
magnetic_field=b0,
baseline_temperature=37.0
)
# Calculate statistics
temp_recovered = result.temperature_change
max_temp = np.max(temp_recovered)
max_true = np.max(temp_true)
print(f"Peak temperature rise:")
print(f" True: {max_true:.1f} °C")
print(f" Measured: {max_temp:.1f} °C")
print(f" Error: {max_temp - max_true:+.1f} °C")
# Calculate temperature uncertainty from phase noise
# dT/dphi = 1 / (gamma * alpha * B0 * TE)
temp_uncertainty = noise_std / (2 * np.pi * 42.58e6 * (-0.01e-6) * b0 * te)
print(f"Temperature uncertainty (1 SD): ~{abs(temp_uncertainty):.1f} °C")
Temperature Sensitivity Analysis¶
import numpy as np
from qmri.thermometry import prf
# Compare sensitivity at different field strengths and echo times
field_strengths = [1.5, 3.0, 7.0] # Tesla
echo_times = [0.010, 0.015, 0.020] # seconds
print("Phase shift (degrees) for 10°C temperature change:")
print("-" * 50)
print(f"{'TE (ms)':<10}", end="")
for b0 in field_strengths:
print(f"{b0:.1f}T{'':<8}", end="")
print()
for te in echo_times:
print(f"{te*1000:<10.0f}", end="")
for b0 in field_strengths:
phi = prf.signal_phase_shift(
temperature_change=10.0,
echo_time=te,
magnetic_field=b0
)
print(f"{np.degrees(phi):<12.1f}", end="")
print()
Best Practices¶
Acquisition¶
-
Use gradient echo sequences (spoiled GRE recommended).
-
Select appropriate TE: 10-20 ms provides good sensitivity.
-
Acquire fat-suppressed or water-selective images if possible.
-
Include reference regions (unheated tissue or fat) for drift correction.
-
Use fast imaging for real-time monitoring (EPI, spiral).
Processing¶
-
Always correct for field drift using reference regions.
-
Apply spatial filtering to reduce noise (but preserves edges).
-
Use phase unwrapping for large temperature changes.
-
Mask regions with low SNR (typically where magnitude < threshold).
-
Monitor for motion and re-register if necessary.
Quality Control¶
-
Verify baseline stability before heating.
-
Check reference region temperatures remain constant.
-
Monitor for phase wrapping in regions of rapid temperature change.
-
Validate with fibre optic thermometry when possible.
Typical Temperature Sensitivity¶
At 3T with TE = 15 ms:
- Phase shift: ~1.2 deg/degC
- With phase noise of 5 deg: Temperature uncertainty ~4degC
- With phase noise of 1 deg: Temperature uncertainty ~0.8degC
References¶
-
Ishihara, Y., et al. "A precise and fast temperature mapping using water proton chemical shift." MRM 34(6):814-823, 1995.
-
Rieke, V. and Butts Pauly, K. "MR thermometry." J Magn Reson Imaging 27(2):376-390, 2008.
-
De Poorter, J., et al. "Noninvasive MRI thermometry with the proton resonance frequency method: study of susceptibility effects." MRM 34(3):359-367, 1995.
-
Quesson, B., et al. "Magnetic resonance temperature imaging for guidance of thermotherapy." J Magn Reson Imaging 12(4):525-533, 2000.