KAHIBARO
Discord Login Register

22.6. Visualization

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:

python
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.

python
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:

python
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:

python
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:

python
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.

python
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:

python
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.

python
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 quantityTypical array(s)Recommended plot
Energy spectrumenergyplt.hist
Coincidence time differencedtplt.hist
Detector occupancyx, yplt.hist2d or plt.imshow
Depth dosez, dose_zplt.plot
Angular projections (SPECT)angle, countsplt.plot
Dose slicedose_slice 2D arrayplt.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:

python
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:

python
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.

python
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.

python
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}$.

python
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.

python
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.

AspectGood practice for scientific figures
UnitsAlways in axis labels and color bars
TitlesBrief description of what is plotted
LegendsClear identification of curves and datasets
UncertaintyError bars where appropriate
File formatPDF or SVG for publication, PNG for quick viewing
ReproducibilitySave 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

Comments

Please login to add a comment.

Don't have an account? Register now!