Skip to content
Open
Show file tree
Hide file tree
Changes from 63 commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
9efcb77
correct default shapes of `particle_charges` and `survival_probabilit…
jp-ga Oct 14, 2025
c4ba854
add ensemble plot functionality
jp-ga Oct 17, 2025
48b67e1
update changelog
jp-ga Oct 17, 2025
d8b5f30
revert survival probabilities and charges default shaping
jp-ga Oct 17, 2025
7d57136
move style to plot_2d_kws
jp-ga Oct 17, 2025
1a1bd25
add ensemble plotting test
jp-ga Oct 17, 2025
04f97a8
update changelog
jp-ga Oct 17, 2025
76bf864
update changelog
jp-ga Oct 17, 2025
e6ecefc
fix docstring
jp-ga Oct 17, 2025
718bc21
fix docstring
jp-ga Oct 17, 2025
3ed190b
fix docstring
jp-ga Oct 17, 2025
af21b42
Various cleanups in function signatures and docstrings
jank324 Oct 21, 2025
42df81b
Cleanup test
jank324 Oct 21, 2025
8f62df6
Cleanup test ammend
jank324 Oct 21, 2025
27aac7a
Slightly clearer line in test
jank324 Oct 21, 2025
3cd56e6
Better test name
jank324 Oct 21, 2025
a473af8
Apply suggestions from code review
jank324 Oct 21, 2025
c2fb7fc
Various small cleanups
jank324 Oct 21, 2025
a05a810
Refactor vectorised beam distribution code and make more seaborn-like
jank324 Oct 29, 2025
56bab02
Fix test warnings and failures
jank324 Oct 29, 2025
fb5e0e2
Move vectorised distribution plotting utility functions to more fitti…
jank324 Nov 5, 2025
8342187
Clean up docstrings
jank324 Nov 5, 2025
fa89183
Further docstring fixes and cleanup
jank324 Nov 5, 2025
4d655a7
Clean up docstring of `histograms_mean_and_confidence`
jank324 Nov 5, 2025
4d223b5
Add tests for vectorised beam plotting satistics utility functions
jank324 Nov 5, 2025
b54e336
Add IDs to parametrised test cases
jank324 Nov 5, 2025
8c26333
Fix hacky use of `torch.bucketize` with prober definition of `boundar…
jank324 Nov 6, 2025
c6f6205
Fix some issues with the statistics tests
jank324 Nov 6, 2025
eda2b7e
Minor fixes in 1D phase space plot method
jank324 Nov 6, 2025
dc10549
A first pass of code cleanup of 2D histogram plot method
jank324 Nov 6, 2025
081517e
Remove breaking change
jank324 Nov 6, 2025
6f39bef
Fix wrong type annotations
jank324 Nov 6, 2025
54ae47d
Remove no longer needed special vectorisation case distinction
jank324 Nov 6, 2025
3aa29b2
Further cleanup and fixes for 2D plot
jank324 Nov 6, 2025
51f65c0
Further cleanup and make more similar 1D 2D
jank324 Nov 6, 2025
9ff5226
Cleanup to end of 2D plot function
jank324 Nov 6, 2025
ae9471e
Clean up and fix docstring
jank324 Nov 6, 2025
16eddc8
Index fix for vectorisation
jank324 Nov 6, 2025
1548251
Return to original indexing
jank324 Nov 6, 2025
ffc08bb
Finish return to proper original indexing
jank324 Nov 6, 2025
8e1c3b8
Replace `dict.update`s with `|` operator because the former returns `…
jank324 Nov 6, 2025
85c42a2
Fix bin issue
jank324 Nov 6, 2025
280a313
Fix tests failing on older versions of NumPy
jank324 Nov 11, 2025
61cb370
Test increasing mimum requriement for NumPy to that in #582
jank324 Nov 11, 2025
6f4ab1e
Merge branch 'master' into 583-add-beam-ensemble-plotting
jank324 Dec 2, 2025
aa35395
Minor docstring fix
jank324 Dec 2, 2025
4f832f5
Fix docstring indentation
jank324 Dec 2, 2025
5645966
Another minor docstring fix
jank324 Dec 2, 2025
165e016
Fix 2D plot method signature
jank324 Dec 2, 2025
ea0f613
Clean up docstring
jank324 Dec 2, 2025
cf91b74
Fix argument order
jank324 Dec 2, 2025
e7ece47
Remove no longer avaialble method from docstrings and error messages
jank324 Dec 2, 2025
1e26b67
Fix slightling confusing argument names
jank324 Dec 2, 2025
8c35ef5
Clearer changelog entry
jank324 Dec 2, 2025
f1c39cf
Fix PR ID in changelog entry
jank324 Dec 2, 2025
996df93
Test some things
jank324 Dec 2, 2025
a026443
Minor code cleanup in notebook for testing
jank324 Dec 9, 2025
9580388
Fix smoothing in 1D histogram
jank324 Dec 9, 2025
0d98b8d
Fix squiggly confidence contours
jank324 Dec 9, 2025
75f4604
Make bounds clearer as such
jank324 Dec 9, 2025
3c5fb3c
Remove histogram from contour smoothing to maintain original function…
jank324 Dec 9, 2025
31e6c15
Make colour behaviour more predictable accordig to theme
jank324 Dec 9, 2025
ab0bea1
Improve overall structure of 2D plot function
jank324 Dec 9, 2025
d4190e7
Automatic levels (slightly broken)
jank324 Dec 9, 2025
032fde7
Match new code style on transpose
jank324 Dec 9, 2025
3a89304
Merge branch 'master' into 583-add-beam-ensemble-plotting
jank324 Feb 6, 2026
9312eaf
Change `.mT` for NumPy arrays back to `.T` because the former is only…
jank324 Feb 24, 2026
17e5582
Rerun ensemble plot test notebook with current state
jank324 Feb 24, 2026
c0bcf07
Revert "Change `.mT` for NumPy arrays back to `.T` because the former…
jank324 Feb 24, 2026
f719607
Increase minimum NumPy requirement to 2.0.0
jank324 Feb 24, 2026
04c7eb7
Up minimum scipy version to lowest compatible with NumPy 2
jank324 Feb 24, 2026
849138d
Increase minimum Matplotlib version for NumPy 2 compatibility
jank324 Feb 24, 2026
bedf00a
Apply suggestion from Copilot code review
jank324 Feb 24, 2026
c147f33
Merge branch 'master' into 583-add-beam-ensemble-plotting
cr-xu Mar 18, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
- `BPM` elements now support misalignments (see #533) (@roussel-ryan, @jank324)
- Speed up tracking by replacing some PyTorch operations with faster alternatives (see #538, #558, #555, #556, #563) (@jank324, @Hespe)
- New `CombinedCorrector` element that combines the functionality of `HorizontalCorrector` and `VerticalCorrector` (see #589) (@jank324, @amylizzle)
- Add mean and uncertainty plotting functionalities to `ParticleBeam` plotting over vectorised beams (see #584) (@jp-ga, @jank324)

### 🐛 Bug fixes

Expand Down
216 changes: 136 additions & 80 deletions cheetah/particles/particle_beam.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
from cheetah.particles.beam import Beam
from cheetah.particles.species import Species
from cheetah.utils import (
distribution_histogram_and_confidence_1d,
distribution_histogram_and_confidence_2d,
elementwise_linspace,
format_axis_with_prefixed_unit,
match_distribution_moments,
Expand Down Expand Up @@ -1246,46 +1248,65 @@ def plot_1d_distribution(
bins: int = 100,
bin_range: tuple[float] | None = None,
smoothing: float = 0.0,
errorbar: tuple[str, int | float] | str = ("pi", 95),
plot_kws: dict | None = None,
fill_between_kws: dict | None = None,
ax: plt.Axes | None = None,
) -> plt.Axes:
"""
Plot a 1D histogram of the given dimension of the particle distribution.
Plot a 1-dimensional histogram of the given phase-space dimension.

:param dimension: Name of the dimension to plot. Should be one of
`('x', 'px', 'y', 'py', 'tau', 'p')`.
:param bins: Number of bins to use for the histogram.
:param bin_range: Range of the bins to use for the histogram.
:param bins: Number of histogram bins.
:param bin_range: Tuple (min, max) specifying the histogram range, or `None` to
infer from the data.
:param smoothing: Standard deviation of the Gaussian kernel used to smooth the
histogram.
:param plot_kws: Additional keyword arguments to be passed to `plot` function of
matplotlib used to plot the histogram data.
:param ax: Matplotlib axes object to use for plotting.
:return: Matplotlib axes object with the plot.
:param errorbar: Method to compute uncertainty bands over vectorised beams.
Pass either a method string or a tuple `(method, level)`. Available methods
are "sd", "se" and "pi".
:param plot_kws: Additional keyword arguments forwarded to
`matplotlib.axes.Axes.plot`.
:param fill_between_kws: Additional keyword arguments forwarded to
`matplotlib.axes.Axes.fill_between`.
:param ax: Matplotlib axes object to use for plotting. If `None` a new axes is
created.
:return: Matplotlib axes containing the plotted histogram.
"""
if ax is None:
_, ax = plt.subplots()

x_array = getattr(self, dimension).cpu().detach().numpy()
histogram, edges = np.histogram(x_array, bins=bins, range=bin_range)
centers = (edges[:-1] + edges[1:]) / 2
bin_centers, histogram, lower_bound, upper_bound = (
distribution_histogram_and_confidence_1d(
a=getattr(self, dimension),
bins=bins,
bin_range=bin_range,
errorbar=errorbar,
)
)

if smoothing:
histogram = gaussian_filter(histogram, smoothing)
if upper_bound is not None and lower_bound is not None:
smoothed_lower_bound = gaussian_filter(lower_bound, smoothing)
smoothed_upper_bound = gaussian_filter(upper_bound, smoothing)
Comment on lines +1328 to +1329

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gaussian_filter function from scipy.ndimage expects NumPy arrays, but lower_bound and upper_bound are PyTorch tensors returned from distribution_histogram_and_confidence_1d. These tensors need to be converted to NumPy arrays before being passed to gaussian_filter, and the results should be converted back to tensors if needed for consistency with the rest of the code. The same issue applies to histogram on line 1299.

Copilot uses AI. Check for mistakes.
ax.fill_between(
bin_centers,
smoothed_lower_bound,
smoothed_upper_bound,
**({"color": "C1", "alpha": 0.5} | (fill_between_kws or {})),
)

ax.plot(
centers,
histogram / histogram.max(),
**{"color": "black"} | (plot_kws or {}),
)
ax.set_xlabel(f"{self.PRETTY_DIMENSION_LABELS[dimension]}")
smoothed_histogram = gaussian_filter(histogram, smoothing)

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gaussian_filter function from scipy.ndimage expects NumPy arrays, but histogram is a PyTorch tensor returned from distribution_histogram_and_confidence_1d. This tensor needs to be converted to a NumPy array before being passed to gaussian_filter.

Copilot uses AI. Check for mistakes.
ax.plot(bin_centers, smoothed_histogram, **({"color": "C0"} | (plot_kws or {})))
Comment on lines +1330 to +1338

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matplotlib's plotting functions typically expect NumPy arrays. While bin_centers is a PyTorch tensor, it should be converted to NumPy (using .numpy() or .cpu().numpy()) before being passed to ax.fill_between and ax.plot to ensure compatibility.

Copilot uses AI. Check for mistakes.

# Handle units
if dimension in ("x", "y", "tau"):
base_unit = "m"
ax.set_xlabel(f"{self.PRETTY_DIMENSION_LABELS[dimension]}")

if dimension in ("x", "y", "tau"):
format_axis_with_prefixed_unit(ax.xaxis, base_unit, centers)
base_unit = "m"
format_axis_with_prefixed_unit(
ax.xaxis, base_unit, bin_centers.numpy()
) # Take `.numpy()` because `np.max` somehow acts up on `torch.Tensor`s

return ax

Expand All @@ -1294,112 +1315,145 @@ def plot_2d_distribution(
x_dimension: Literal["x", "px", "y", "py", "tau", "p"],
y_dimension: Literal["x", "px", "y", "py", "tau", "p"],
style: Literal["histogram", "contour"] = "histogram",
bins: int = 100,
bin_ranges: tuple[tuple[float]] | None = None,
histogram_smoothing: float = 0.0,
contour_smoothing: float = 3.0,
bins: tuple[int, int] = (100, 100),
bin_ranges: tuple[tuple[float, float], tuple[float, float]] | None = None,
errorbar: tuple[str, int | float] | str = ("pi", 95),
smoothing: float = 0.0,
pcolormesh_kws: dict | None = None,
contour_kws: dict | None = None,
distribution_contour_kws: dict | None = None,
confidence_contour_kws: dict | None = None,
ax: plt.Axes | None = None,
) -> plt.Axes:
"""
Plot a 2D histogram of the given dimensions of the particle distribution.

:param x_dimension: Name of the x dimension to plot. Should be one of
`('x', 'px', 'y', 'py', 'tau', 'p')`.
:param y_dimension: Name of the y dimension to plot. Should be one of
`('x', 'px', 'y', 'py', 'tau', 'p')`.
:param style: Style of the plot. Should be one of `('histogram', 'contour')`.
:param bins: Number of bins to use for the histogram in both dimensions.
:param bin_ranges: Ranges of the bins to use for the histogram in each
dimension.
:param smoothing: Standard deviation of the Gaussian kernel used to smooth the
histogram.
:param pcolormesh_kws: Additional keyword arguments to be passed to `pcolormesh`
function of matplotlib used to plot the histogram data.
:param contour_kws: Additional keyword arguments to be passed to `contour`
function of matplotlib used to plot the histogram data.
:param ax: Matplotlib axes object to use for plotting.
:return: Matplotlib axes object with the plot.
Plot a 2-dimensional projection of the particle distribution for two phase-space
dimensions.

:param x_dimension: Name of the dimension plotted on the x-axis. Should be one
of `('x', 'px', 'y', 'py', 'tau', 'p')`.
:param y_dimension: Name of the dimension plotted on the y-axis. Should be one
of `('x', 'px', 'y', 'py', 'tau', 'p')`.
:param style: Visualisation style, either 'histogram' (coloured 2-dimensional
histogram) or 'contour' (normalised contour levels with greyscale
`pcolormesh`).
:param bins: Tuple (nx, ny) specifying the number of histogram bins for x and y.
:param bin_ranges: Tuple ((x_min, x_max), (y_min, y_max)) specifying the
histogram ranges for x and y, or `None` to infer from the data.
:param errorbar: Method to compute uncertainty bands over vectorised beams. Pass
either a method string or a tuple `(method, level)`. Available methods are
"sd", "se" and "pi".
:param smoothing: Standard deviation of the Gaussian kernel applied to smooth
the histogram.
:param pcolormesh_kws: Additional keyword arguments forwarded to
`matplotlib.pcolormesh`.
:param distribution_contour_kws: Additional keyword arguments forwarded to
`matplotlib.contour` when drawing distribution contours.
:param confidence_contour_kws: Additional keyword arguments forwarded to
`matplotlib.contour` when drawing confidence contours.
:param ax: Matplotlib axes object to use for plotting. If `None` a new axes is
created.
:return: Matplotlib axes containing the plotted histogram.
"""
if ax is None:
_, ax = plt.subplots()

histogram, x_edges, y_edges = np.histogram2d(
getattr(self, x_dimension).cpu().detach().numpy(),
getattr(self, y_dimension).cpu().detach().numpy(),
bins=bins,
range=bin_ranges,
bin_centers_x, bin_centers_y, mean_histogram, lower_bound, upper_bound = (
distribution_histogram_and_confidence_2d(
x=getattr(self, x_dimension),
y=getattr(self, y_dimension),
bins=bins,
bin_ranges=bin_ranges,
errorbar=errorbar,
)
)
x_centers = (x_edges[:-1] + x_edges[1:]) / 2
y_centers = (y_edges[:-1] + y_edges[1:]) / 2

# Post-process and plot
smoothed_histogram = gaussian_filter(histogram, histogram_smoothing)
clipped_histogram = np.where(smoothed_histogram > 1, smoothed_histogram, np.nan)
smoothed_histogram = gaussian_filter(mean_histogram, smoothing)

if style == "histogram":
ax.pcolormesh(
x_edges,
y_edges,
clipped_histogram.T / smoothed_histogram.max(),
**{"cmap": "rainbow"} | (pcolormesh_kws or {}),
bin_centers_x,
bin_centers_y,
smoothed_histogram.T,
Comment thread
jank324 marked this conversation as resolved.
Outdated
**({"cmap": "rainbow"} | (pcolormesh_kws or {})),
)
elif style == "contour":
contour_histogram = gaussian_filter(histogram, contour_smoothing)

ax.contour(
x_centers,
y_centers,
contour_histogram.T / contour_histogram.max(),
**{"levels": 3} | (contour_kws or {}),
bin_centers_x,
bin_centers_y,
smoothed_histogram.T / smoothed_histogram.max(),
Comment thread
jank324 marked this conversation as resolved.
Outdated
**({"levels": [0.1, 0.5, 0.9]} | (distribution_contour_kws or {})),
)
if lower_bound is not None and upper_bound is not None:
smoothed_lower_bound = gaussian_filter(lower_bound, smoothing)
smoothed_upper_bound = gaussian_filter(upper_bound, smoothing)
Comment on lines +1408 to +1427

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The gaussian_filter function from scipy.ndimage expects NumPy arrays, but mean_histogram, lower_bound, and upper_bound are PyTorch tensors returned from distribution_histogram_and_confidence_2d. These tensors need to be converted to NumPy arrays before being passed to gaussian_filter.

Copilot uses AI. Check for mistakes.

ax.contour(
bin_centers_x,
bin_centers_y,
smoothed_lower_bound.mT / smoothed_lower_bound.max(),
**(
{"levels": [0.1, 0.5, 0.9], "linestyles": "--"}
| (confidence_contour_kws or {})
),
)
ax.contour(
bin_centers_x,
bin_centers_y,
smoothed_upper_bound.mT / smoothed_upper_bound.max(),
**(
{"levels": [0.1, 0.5, 0.9], "linestyles": "--"}
| (confidence_contour_kws or {})
),
)
Comment on lines 1411 to +1446

Copilot AI Feb 24, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Matplotlib's pcolormesh and contour functions typically expect NumPy arrays. The bin_centers_x, bin_centers_y, and histogram tensors should be converted to NumPy arrays before being passed to these plotting functions to ensure compatibility.

Copilot uses AI. Check for mistakes.
else:
raise ValueError("style must be either 'histogram' or 'contour'.")

# Handle units
ax.set_xlabel(f"{self.PRETTY_DIMENSION_LABELS[x_dimension]}")
ax.set_ylabel(f"{self.PRETTY_DIMENSION_LABELS[y_dimension]}")

# Handle units
if x_dimension in ("x", "y", "tau"):
x_base_unit = "m"
format_axis_with_prefixed_unit(ax.xaxis, x_base_unit, bin_centers_x.numpy())

if y_dimension in ("x", "y", "tau"):
y_base_unit = "m"

if x_dimension in ("x", "y", "tau"):
format_axis_with_prefixed_unit(ax.xaxis, x_base_unit, x_centers)

if y_dimension in ("x", "y", "tau"):
format_axis_with_prefixed_unit(ax.yaxis, y_base_unit, y_centers)
format_axis_with_prefixed_unit(ax.yaxis, y_base_unit, bin_centers_y.numpy())

return ax

def plot_distribution(
self,
dimensions: tuple[str, ...] = ("x", "px", "y", "py", "tau", "p"),
bins: int = 100,
bin_ranges: Literal["same"] | tuple[float] | list[tuple[float]] | None = None,
bin_ranges: (
Literal["unit_same"] | tuple[float] | list[tuple[float]] | None
) = None,
errorbar: tuple[str, int | float] | str = ("pi", 95),
plot_1d_kws: dict | None = None,
plot_2d_kws: dict | None = None,
axs: list[plt.Axes] | None = None,
) -> tuple[plt.Figure, np.ndarray]:
"""
Plot of coordinates projected into 2D planes.
Create a matrix plot of 1-dimensional and 2-dimensional projections for the
requested phase space dimensions.

:param dimensions: Tuple of dimensions to plot. Should be a subset of
`('x', 'px', 'y', 'py', 'tau', 'p')`.
:param contour: If `True`, overlay contour lines on the 2D histogram plots.
:param bins: Number of bins to use for the histograms.
:param bin_ranges: Ranges of the bins to use for the histograms. If set to
`"unit_same"`, the same range is used for all dimensions that share the same
unit. If set to `None`, ranges are determined automatically.
:param smoothing: Standard deviation of the Gaussian kernel used to smooth the
histograms.
:param errorbar: Method to compute uncertainty bands over vectorised beams. Pass
either a method string or a tuple `(method, level)`. Available methods
are "sd", "se" and "pi".
:param plot_1d_kws: Additional keyword arguments to be passed to
`ParticleBeam.plot_1d_distribution` for plotting 1D histograms.
`ParticleBeam.plot_1d_distribution` for plotting 1-dimensional histograms.
:param plot_2d_kws: Additional keyword arguments to be passed to
`ParticleBeam.plot_2d_distribution` for plotting 2D histograms.
`ParticleBeam.plot_2d_distribution` for plotting 2-dimensional histograms.
:param axs: List of Matplotlib axes objects to use for plotting. If set to
`None`, a new figure is created. Must have the shape `(len(dimensions),
len(dimensions))`.
`None`, a new figure is created. Must have the shape
`(len(dimensions), len(dimensions))`.
:return: Matplotlib figure and axes objects with the plot.
"""
if axs is None:
Expand Down Expand Up @@ -1493,6 +1547,7 @@ def plot_distribution(
dimension=dimension,
bins=bins,
bin_range=bin_range,
errorbar=errorbar,
ax=ax,
**(plot_1d_kws or {}),
)
Expand All @@ -1502,8 +1557,9 @@ def plot_distribution(
self.plot_2d_distribution(
x_dimension=dimensions[i],
y_dimension=dimensions[j],
bins=bins,
bins=(bins, bins),
bin_ranges=(bin_ranges[i], bin_ranges[j]),
errorbar=errorbar,
ax=axs[j, i],
**(plot_2d_kws or {}),
)
Expand Down
5 changes: 5 additions & 0 deletions cheetah/utils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,16 @@
format_axis_with_prefixed_unit,
)
from .statistics import ( # noqa: F401
distribution_histogram_and_confidence_1d,
distribution_histogram_and_confidence_2d,
histograms_mean_and_confidence,
match_distribution_moments,
unbiased_weighted_covariance,
unbiased_weighted_covariance_matrix,
unbiased_weighted_std,
unbiased_weighted_variance,
vectorized_histogram_1d,
vectorized_histogram_2d,
)
from .unique_name_generator import UniqueNameGenerator # noqa: F401
from .vector import squash_index_for_unavailable_dims # noqa: F401
Expand Down
Loading
Loading