22.6. Visualization
Table of Contents
Matplotlib
In this chapter you use Python to visualize GATE simulation results, focusing on Matplotlib. At this point you should already know how to read data from ROOT or other GATE output formats into NumPy or Pandas. Here you concentrate on turning those arrays into clear plots.
Matplotlib is a general plotting library for Python. For GATE analysis it is especially useful for quick inspection plots such as energy spectra, interaction position maps, timing distributions, and dose profiles.
A typical analysis script starts with imports and a basic plotting style:
import numpy as np
import matplotlib.pyplot as plt
plt.style.use("seaborn-v0_8-colorblind") # optional, but gives readable defaults
After you have loaded data from ROOT or another file, you can create histograms. For example, suppose energy is an array of deposited energies in keV from singles data.
bins = np.linspace(0, 800, 400) # 0 to 800 keV, 2 keV bin width
plt.figure()
plt.hist(energy, bins=bins, histtype="step", color="C0")
plt.xlabel("Energy [keV]")
plt.ylabel("Counts")
plt.title("Energy spectrum of singles")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()For timing analysis, you might have an array of coincidence time differences in ns:
dt = coinc_times_detector1 - coinc_times_detector2
bins = np.linspace(-10, 10, 201)
plt.figure()
plt.hist(dt, bins=bins, histtype="stepfilled", alpha=0.5)
plt.xlabel("Time difference Δt [ns]")
plt.ylabel("Coincidences")
plt.title("Coincidence timing distribution")
plt.tight_layout()
plt.show()
Two dimensional histograms are useful for maps, such as detector occupancy or dose slices. Matplotlib offers imshow for images and hist2d for binned distributions.
Assume x and y are interaction coordinates in mm on a detector plane:
plt.figure()
plt.hist2d(x, y, bins=100, cmap="viridis")
plt.xlabel("x [mm]")
plt.ylabel("y [mm]")
plt.title("Detector occupancy map")
cbar = plt.colorbar()
cbar.set_label("Counts")
plt.axis("equal")
plt.tight_layout()
plt.show()
If you instead already have a 2D array that represents an image, for example a dose map slice with shape (ny, nx) and pixel size dx, dy, you can control the physical coordinate axes using extent:
ny, nx = dose_slice.shape
dx = dy = 2.0 # mm
extent = [0, nx * dx, 0, ny * dy] # x_min, x_max, y_min, y_max
plt.figure()
plt.imshow(dose_slice, origin="lower", extent=extent, cmap="inferno")
plt.xlabel("x [mm]")
plt.ylabel("y [mm]")
plt.title("Dose map slice")
cbar = plt.colorbar()
cbar.set_label("Dose [Gy]")
plt.tight_layout()
plt.show()Both one dimensional and two dimensional plots often benefit from using logarithmic scales. This can help for wide dynamic ranges such as scatter tails in an energy spectrum or low dose regions in a dose image.
plt.figure()
plt.hist(energy, bins=bins, histtype="step", color="C0")
plt.yscale("log")
plt.xlabel("Energy [keV]")
plt.ylabel("Counts (log scale)")
plt.title("Energy spectrum (log scale)")
plt.tight_layout()
plt.show()When you explore several quantities together, subplots keep the organization clear. For example, to inspect both spectrum and occupancy from a PET simulation side by side:
fig, axes = plt.subplots(1, 2, figsize=(10, 4))
axes[0].hist(energy, bins=bins, histtype="step")
axes[0].set_xlabel("Energy [keV]")
axes[0].set_ylabel("Counts")
axes[0].set_title("Energy spectrum")
h = axes[1].hist2d(x, y, bins=100, cmap="viridis")
axes[1].set_xlabel("x [mm]")
axes[1].set_ylabel("y [mm]")
axes[1].set_title("Detector occupancy")
fig.colorbar(h[3], ax=axes[1], label="Counts")
fig.tight_layout()
plt.show()
To save your figures for later analysis or for reports, Matplotlib provides savefig. It is important to call it before show in scripts that close figures automatically.
output_path = "results/energy_spectrum.png"
plt.figure()
plt.hist(energy, bins=bins, histtype="step")
plt.xlabel("Energy [keV]")
plt.ylabel("Counts")
plt.title("Energy spectrum of singles")
plt.tight_layout()
plt.savefig(output_path, dpi=300)
plt.close()The table below summarizes which Matplotlib plot types are commonly used for different GATE outputs:
| GATE quantity | Typical array(s) | Recommended plot |
|---|---|---|
| Energy spectrum | energy | plt.hist |
| Coincidence time difference | dt | plt.hist |
| Detector occupancy | x, y | plt.hist2d or plt.imshow |
| Depth dose | z, dose_z | plt.plot |
| Angular projections (SPECT) | angle, counts | plt.plot |
| Dose slice | dose_slice 2D array | plt.imshow |
Whenever you analyze GATE data, keep units explicit in axis labels and titles to avoid confusion between keV and MeV, ns and ps, or mm and cm.
Always include units in axis labels and describe exactly what is plotted. Ambiguous or unlabeled figures are very difficult to interpret and to compare across simulations.
Scientific figures
When you move from simple diagnostic plots to scientific figures, you must care not only about the content but also about clarity, consistency, and reproducibility. In the context of GATE, scientific figures typically show dose distributions, energy spectra with fitted peaks, count rate curves, or comparison between simulation and measurements.
A first step is to control figure size and font sizes so that the figure remains readable when inserted into a report or article. You can define some global Matplotlib parameters at the start of your script:
import matplotlib.pyplot as plt
plt.rcParams.update({
"figure.figsize": (6, 4),
"font.size": 12,
"axes.labelsize": 12,
"axes.titlesize": 13,
"legend.fontsize": 11,
"xtick.labelsize": 11,
"ytick.labelsize": 11,
})
For comparison plots such as simulated versus measured depth dose, you need careful legends and markers. Suppose you have arrays z in mm, dose_sim and dose_meas in Gy, both normalized to their maximum:
plt.figure()
plt.plot(z, dose_sim, label="GATE simulation", color="C0")
plt.plot(z, dose_meas, "o", label="Measurement", color="C1", markersize=4)
plt.xlabel("Depth in water [mm]")
plt.ylabel("Relative dose")
plt.title("Depth dose curve comparison")
plt.grid(True, alpha=0.3)
plt.legend()
plt.tight_layout()
plt.savefig("results/depth_dose_comparison.pdf")
plt.close()Using vector formats such as PDF or SVG is often preferable for final figures because they scale without loss of quality. You can still use PNG for diagnostic plots or web display.
When displaying images such as dose maps or activity distributions, add color bars with physical units and adjust colormaps for interpretability. Avoid purely rainbow-like maps that can distort perception of gradients. Sequential colormaps like "viridis", "plasma", or "inferno" work well.
plt.figure()
im = plt.imshow(dose_slice, origin="lower", extent=extent, cmap="inferno")
plt.xlabel("x [mm]")
plt.ylabel("y [mm]")
plt.title("Axial dose distribution")
cbar = plt.colorbar(im)
cbar.set_label("Dose [Gy]")
plt.tight_layout()
plt.savefig("results/dose_slice_axial.png", dpi=300)
plt.close()In timing and energy spectra, it is common to overlay vertical lines or shaded regions that indicate analysis cuts, such as energy windows or coincidence time windows. This helps document exactly how events were selected.
plt.figure()
plt.hist(energy, bins=bins, histtype="step", color="C0")
e_low, e_high = 400, 650 # keV
plt.axvspan(e_low, e_high, color="C1", alpha=0.2,
label=f"Energy window [{e_low}, {e_high}] keV")
plt.xlabel("Energy [keV]")
plt.ylabel("Counts")
plt.title("PET energy spectrum and selected window")
plt.legend()
plt.tight_layout()
plt.savefig("results/pet_energy_window.png", dpi=300)
plt.close()
Error bars are important for Monte Carlo results, because each bin of a histogram or each point in a profile has statistical uncertainty. If you know both counts and their uncertainties, you can plot them with plt.errorbar. For counts dominated by Poisson statistics, a simple approximation is $\sigma = \sqrt{N}$.
bin_counts, bin_edges = np.histogram(energy, bins=bins)
bin_centers = 0.5 * (bin_edges[:-1] + bin_edges[1:])
errors = np.sqrt(bin_counts)
plt.figure()
plt.errorbar(bin_centers, bin_counts, yerr=errors,
fmt="o", markersize=3, label="Simulation")
plt.xlabel("Energy [keV]")
plt.ylabel("Counts per bin")
plt.title("Energy spectrum with statistical uncertainties")
plt.legend()
plt.tight_layout()
plt.savefig("results/energy_spectrum_errorbars.pdf")
plt.close()In many GATE applications you compare multiple configurations, for example different shielding materials, detector designs, or physics lists. Consistent colors, line styles, and labels help the reader understand what is different and what is the same.
plt.figure()
plt.plot(z, dose_lead, label="Lead", color="C0")
plt.plot(z, dose_al, label="Aluminum", color="C1")
plt.plot(z, dose_concrete, label="Concrete", color="C2")
plt.xlabel("Depth [cm]")
plt.ylabel("Relative transmitted dose")
plt.title("Effect of shielding material on transmission")
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig("results/shielding_comparison.png", dpi=300)
plt.close()To keep figures reproducible, include the exact GATE configuration that generated the data in a separate log file, and record any normalization or scaling applied to the plotted quantities. If you normalize curves to their maximum or to a reference point, state this clearly in the caption or title.
Finally, organize your output directory so that figures have meaningful, systematic file names, for example including simulation IDs, geometry versions, or parameter values.
| Aspect | Good practice for scientific figures |
|---|---|
| Units | Always in axis labels and color bars |
| Titles | Brief description of what is plotted |
| Legends | Clear identification of curves and datasets |
| Uncertainty | Error bars where appropriate |
| File format | PDF or SVG for publication, PNG for quick viewing |
| Reproducibility | Save scripts and configuration alongside figures |
For scientific figures, use clear labels, units, and legends, include statistical uncertainties when relevant, and save figures in reproducible form with scripts and parameters that can regenerate them exactly.
Views: 9
KAHIBARO