Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add particle species in Beam classes and update tracking methods #276

Open
wants to merge 59 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 50 commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
213e7af
Add particle species for Beam classes
cr-xu Oct 7, 2024
cbcbd4c
Add particle mass in the `transfer_map` calculation
cr-xu Oct 14, 2024
b41181b
Update CI/CD pytest pipeline; Add benchmarking tests with different s…
cr-xu Oct 15, 2024
5f9c19d
Try using the 'classic' conda-solver
cr-xu Oct 15, 2024
89d1740
Try fixing conda with pip installs in github actions
cr-xu Oct 15, 2024
eb82c49
Try again conda
cr-xu Oct 15, 2024
9565b73
Separate windows tests, excluding bmad runs
cr-xu Oct 15, 2024
d49781e
Update test_tracking_different_species.py
cr-xu Oct 15, 2024
7cdb3f3
Merge branch 'master' into 231-add-particle-species-in-particlebeam
cr-xu Oct 16, 2024
489ebb2
Remove the screen reading from of the speed test
cr-xu Oct 16, 2024
30cf6a5
Update CHANGELOG and documentation
cr-xu Oct 16, 2024
a620f4e
Update docstring
cr-xu Oct 16, 2024
741232f
Merge branch 'master' into 231-add-particle-species-in-particlebeam
jank324 Oct 16, 2024
1a2976d
Update tests; Improve docstrings
cr-xu Oct 17, 2024
a3570ff
Merge branch 'master' into 231-add-particle-species-in-particlebeam
cr-xu Oct 22, 2024
691490b
Merge branch 'master' into 231-add-particle-species-in-particlebeam
jank324 Jan 16, 2025
ddcac5f
Some cleanup
jank324 Jan 16, 2025
d137d58
Clean up and merge most tests into one
jank324 Jan 16, 2025
10dd222
Fix flake8 errors because of undefined variables
jank324 Jan 16, 2025
262433f
Move `species` to `particles` module
jank324 Jan 16, 2025
de73cbd
Streamline species tests
jank324 Jan 16, 2025
e98ef0b
Merge branch 'master' into 231-add-particle-species-in-particlebeam
jank324 Jan 16, 2025
768871b
Fixes grammatical error in docstring
jank324 Jan 16, 2025
5005675
Fix issue that allowed accidental changing of the default species par…
jank324 Jan 16, 2025
12a3d89
Fix beam `__repr__`s, docstrings and other minor items
jank324 Jan 16, 2025
d74fc78
Return speed tests to original
jank324 Jan 16, 2025
09cd0d3
Fix broken references to `mass_eV`
jank324 Jan 16, 2025
a352ea0
Fix `Species` documentation render
jank324 Jan 16, 2025
c5e1ab8
Fix class location for saved incoming test beam
jank324 Jan 16, 2025
9275cd9
Properly deselect Bmad tests in Windows GitHub Action
jank324 Jan 16, 2025
7e8d653
Temporariliy deactive macOS test to see Ubuntu results
jank324 Jan 16, 2025
8ee1044
Revert "Temporariliy deactive macOS test to see Ubuntu results"
jank324 Jan 16, 2025
d54d63b
Get Tao usage in tests at least to a point where it doesn't crash all…
jank324 Jan 17, 2025
957b347
Fix Bmad particle species and element tests that don't fail because o…
jank324 Jan 17, 2025
6491a32
Fix segmentation fault failures in Bmad tests
jank324 Jan 17, 2025
6a63bb7
Finalise cleanup and unification into a single test of Bmad compare e…
jank324 Jan 17, 2025
1537d7b
Minor code formatting improvement
jank324 Jan 17, 2025
c582310
Force particle mass to be explicit in relativistic factor computation
jank324 Jan 17, 2025
992fa11
Fix failing tests
jank324 Jan 17, 2025
981f825
Fix weird random seed thing that caused broken test to succeed (since…
jank324 Jan 17, 2025
00c65a6
Remove fixed GitHub Actions random seed becasue it masked failing tests
jank324 Jan 17, 2025
5963602
Another attempt at fixing Tao import issue on Windows
jank324 Jan 17, 2025
15cc15c
Refactor `Species`
jank324 Jan 17, 2025
3ace4b3
Add missing argument entry in docstring
jank324 Jan 17, 2025
9f1adf3
Cleanup test setup excluding Bmad tests on Windows
jank324 Jan 20, 2025
1d82517
Fix conda installation on Windows
jank324 Jan 20, 2025
9b7fc4e
Attempt at fixing Windows ignoring of Bmad tests
jank324 Jan 20, 2025
7bffd7e
Remove pytest `bmad` marker that wasn't used in the end
jank324 Jan 20, 2025
4d170fe
Update changelog entry
jank324 Jan 20, 2025
d4cf841
Make changelog message slightly more concrete
jank324 Jan 20, 2025
ea6467c
Apply suggestions from code review
jank324 Jan 20, 2025
77db2be
Fix species-element docstring to refelect what the test actually does
jank324 Jan 20, 2025
4dd0484
Test TEMPORARILY reinstating GitHub Action test seed
jank324 Jan 21, 2025
eb8acd1
Merge branch 'master' into 231-add-particle-species-in-particlebeam
jank324 Jan 28, 2025
31fc324
Merge branch 'master' into 231-add-particle-species-in-particlebeam
jank324 Jan 30, 2025
5845d4c
Merge branch 'master' into 231-add-particle-species-in-particlebeam
jank324 Feb 3, 2025
f668971
Pre-holiday commit
jank324 Feb 7, 2025
dd095a1
Fix species charge type
Hespe Feb 10, 2025
e37a26e
Merge branch 'master' into 231-add-particle-species-in-particlebeam
Hespe Feb 10, 2025
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
16 changes: 12 additions & 4 deletions .github/workflows/pytest.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,26 @@ jobs:
- os: windows-latest
python-version: "3.12"
runs-on: ${{ matrix.os }}
defaults:
run:
shell: bash -el {0}

steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
- uses: conda-incubator/setup-miniconda@v3
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
miniforge-version: "latest"
- name: Install Bmad and PyTao
run: |
if [[ ! ${{ runner.os }} == 'Windows' ]]; then
conda install bmad pytao
fi
- name: Install pip dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[openpmd]
pip install -r test_requirements.txt
- name: Test with pytest
run: |
pytest --seed 42
pytest
jank324 marked this conversation as resolved.
Show resolved Hide resolved
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
### 🚀 Features

- `ParticleBeam` now supports importing from and exporting to [openPMD-beamphysics](https://github.com/ChristopherMayes/openPMD-beamphysics) HDF5 files and `ParticleGroup` objects. This allows for easy conversion to and from other file formats supported by openPMD-beamphysics. (see #305, #320) (@cr-xu, @Hespe)
- Add support for particle species through a new `Species` class (see #276) (@cr-xu, @jank324)

### 🐛 Bug fixes

Expand Down
2 changes: 1 addition & 1 deletion cheetah/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@
Undulator,
VerticalCorrector,
)
from .particles import Beam, ParameterBeam, ParticleBeam # noqa: F401
from .particles import Beam, ParameterBeam, ParticleBeam, Species # noqa: F401
4 changes: 3 additions & 1 deletion cheetah/accelerator/aperture.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,9 @@ def __init__(
def is_skippable(self) -> bool:
return not self.is_active

def transfer_map(self, energy: torch.Tensor) -> torch.Tensor:
def transfer_map(
self, energy: torch.Tensor, particle_mass_eV: torch.Tensor
) -> torch.Tensor:
device = self.x_max.device
dtype = self.x_max.dtype

Expand Down
4 changes: 3 additions & 1 deletion cheetah/accelerator/bpm.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,9 @@ def __init__(self, is_active: bool = False, name: Optional[str] = None) -> None:
def is_skippable(self) -> bool:
return not self.is_active

def transfer_map(self, energy: torch.Tensor) -> torch.Tensor:
def transfer_map(
self, energy: torch.Tensor, particle_mass_eV: torch.Tensor
) -> torch.Tensor:
return torch.eye(7, device=energy.device, dtype=energy.dtype).repeat(
(*energy.shape, 1, 1)
)
Expand Down
38 changes: 23 additions & 15 deletions cheetah/accelerator/cavity.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
import torch
from matplotlib.patches import Rectangle
from scipy import constants
from scipy.constants import physical_constants

from cheetah.accelerator.element import Element
from cheetah.particles import Beam, ParameterBeam, ParticleBeam
Expand All @@ -17,15 +16,15 @@

generate_unique_name = UniqueNameGenerator(prefix="unnamed_element")

electron_mass_eV = physical_constants["electron mass energy equivalent in MeV"][0] * 1e6


class Cavity(Element):
"""
Accelerating cavity in a particle accelerator.

:param length: Length in meters.
:param voltage: Voltage of the cavity in volts.
:param voltage: Voltage of the cavity in volts. NOTE: This assumes the effective
voltage. If the particle does not have unit charge, the voltage needs to be
scaled properly.
Comment on lines +25 to +27
Copy link
Member

Choose a reason for hiding this comment

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

This seems counterintuitive. Is this effect caused by us measuring energy in eV such that we cannot just set voltage = energy gain for non-unit charge particles?

This might especially lead to issues if we import lattices from external files. In that case the converter would have to know the intended particle species and adjust all voltages.

Copy link
Member

Choose a reason for hiding this comment

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

@cr-xu what do you think?

Copy link
Member Author

@cr-xu cr-xu Feb 5, 2025

Choose a reason for hiding this comment

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

Yes, I thought that confusing too at the beginning.
I went with this convention exactly due to the compatibility with the converters. Because Bmad also defines the voltage as the effective voltage for the ref. particle

Copy link
Member

@jank324 jank324 Feb 5, 2025

Choose a reason for hiding this comment

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

So @cr-xu you think we are fine as we are and won't run into problems when simulating other species?

Copy link
Member Author

@cr-xu cr-xu Feb 5, 2025

Choose a reason for hiding this comment

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

I think it's in the end a design choice... I see this convention (effective voltage rather than the physical one) as consistent with other normalized field strengths like $k_1$ for quadrupoles etc.
We can also just use the physical voltage,and just document/comment that extra caution is needed if they want to have non-unit charged particles.

Copy link
Member

Choose a reason for hiding this comment

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

I have no opinion on this. I just want to know if this is okay to merge 😄

Copy link
Member Author

Choose a reason for hiding this comment

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

@Hespe Actually now the voltage = total energy gain for non-unit charged particles. Setting the voltage to physical voltage would mean that total energy gain = voltage * q

Copy link
Member

Choose a reason for hiding this comment

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

Yes, that is what I meant. For non-unit charged particles, we would have to take the charge into account to calculate the energy gain.

I think your argument that the cavity is particle independent is what I would have thought before. Otherwise you would have to know the particle you are going to simulate the correctly configure the Cavity element.

:param phase: Phase of the cavity in degrees.
:param frequency: Frequency of the cavity in Hz.
:param name: Unique identifier of the element.
Expand Down Expand Up @@ -67,14 +66,17 @@ def is_active(self) -> bool:
def is_skippable(self) -> bool:
return not self.is_active

def transfer_map(self, energy: torch.Tensor) -> torch.Tensor:
def transfer_map(
self, energy: torch.Tensor, particle_mass_eV: torch.Tensor
) -> torch.Tensor:
return torch.where(
(self.voltage != 0).unsqueeze(-1).unsqueeze(-1),
self._cavity_rmatrix(energy),
self._cavity_rmatrix(energy, particle_mass_eV),
base_rmatrix(
length=self.length,
k1=torch.zeros_like(self.length),
hx=torch.zeros_like(self.length),
particle_mass_eV=particle_mass_eV,
tilt=torch.zeros_like(self.length),
energy=energy,
),
Expand All @@ -99,11 +101,13 @@ def _track_beam(self, incoming: Beam) -> Beam:
Track particles through the cavity. The input can be a `ParameterBeam` or a
`ParticleBeam`.
"""
gamma0, igamma2, beta0 = compute_relativistic_factors(incoming.energy)
gamma0, igamma2, beta0 = compute_relativistic_factors(
incoming.energy, incoming.species.mass_eV
)

phi = torch.deg2rad(self.phase)

tm = self.transfer_map(incoming.energy)
tm = self.transfer_map(incoming.energy, incoming.species.mass_eV)
if isinstance(incoming, ParameterBeam):
outgoing_mu = torch.matmul(tm, incoming._mu.unsqueeze(-1)).squeeze(-1)
outgoing_cov = torch.matmul(
Expand All @@ -120,7 +124,9 @@ def _track_beam(self, incoming: Beam) -> Beam:
if torch.any(incoming.energy + delta_energy > 0):
k = 2 * torch.pi * self.frequency / constants.speed_of_light
outgoing_energy = incoming.energy + delta_energy
gamma1, _, beta1 = compute_relativistic_factors(outgoing_energy)
gamma1, _, beta1 = compute_relativistic_factors(
outgoing_energy, incoming.species.mass_eV
)

if isinstance(incoming, ParameterBeam):
outgoing_mu[..., 5] = incoming._mu[..., 5] * incoming.energy * beta0 / (
Expand Down Expand Up @@ -151,7 +157,7 @@ def _track_beam(self, incoming: Beam) -> Beam:
- torch.cos(phi).unsqueeze(-1)
)

dgamma = self.voltage / electron_mass_eV
dgamma = self.voltage / incoming.species.mass_eV
if torch.any(delta_energy > 0):
T566 = (
self.length
Expand Down Expand Up @@ -237,16 +243,18 @@ def _track_beam(self, incoming: Beam) -> Beam:
)
return outgoing

def _cavity_rmatrix(self, energy: torch.Tensor) -> torch.Tensor:
def _cavity_rmatrix(
self, energy: torch.Tensor, particle_mass_eV: torch.Tensor
) -> torch.Tensor:
"""Produces an R-matrix for a cavity when it is on, i.e. voltage > 0.0."""
factory_kwargs = {"device": self.length.device, "dtype": self.length.dtype}

phi = torch.deg2rad(self.phase)
delta_energy = self.voltage * torch.cos(phi)
# Comment from Ocelot: Pure pi-standing-wave case
eta = torch.tensor(1.0, **factory_kwargs)
Ei = energy / electron_mass_eV
Ef = (energy + delta_energy) / electron_mass_eV
Ei = energy / particle_mass_eV
Ef = (energy + delta_energy) / particle_mass_eV
Ep = (Ef - Ei) / self.length # Derivative of the energy
assert torch.all(Ei > 0), "Initial energy must be larger than 0"

Expand Down Expand Up @@ -296,14 +304,14 @@ def _cavity_rmatrix(self, energy: torch.Tensor) -> torch.Tensor:
* self.length
* beta0
* self.voltage
/ electron_mass_eV
/ particle_mass_eV
* torch.sin(phi)
* (g0 * g1 * (beta0 * beta1 - 1) + 1)
/ (beta1 * g1 * (g0 - g1) ** 2)
)

r66 = Ei / Ef * beta0 / beta1
r65 = k * torch.sin(phi) * self.voltage / (Ef * beta1 * electron_mass_eV)
r65 = k * torch.sin(phi) * self.voltage / (Ef * beta1 * particle_mass_eV)

# Make sure that all matrix elements have the same shape
r11, r12, r21, r22, r55_cor, r56, r65, r66 = torch.broadcast_tensors(
Expand Down
23 changes: 19 additions & 4 deletions cheetah/accelerator/custom_transfer_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,14 +62,27 @@ def from_merging_elements(
" incorrect tracking results."
)

device = elements[0].transfer_map(incoming_beam.energy).device
dtype = elements[0].transfer_map(incoming_beam.energy).dtype
device = (
elements[0]
.transfer_map(incoming_beam.energy, incoming_beam.species.mass_eV)
.device
)
dtype = (
elements[0]
.transfer_map(incoming_beam.energy, incoming_beam.species.mass_eV)
.dtype
)

tm = torch.eye(7, device=device, dtype=dtype).repeat(
(*incoming_beam.energy.shape, 1, 1)
)
for element in elements:
tm = torch.matmul(element.transfer_map(incoming_beam.energy), tm)
tm = torch.matmul(
element.transfer_map(
incoming_beam.energy, incoming_beam.species.mass_eV
),
tm,
)
incoming_beam = element.track(incoming_beam)

combined_length = sum(element.length for element in elements)
Expand All @@ -80,7 +93,9 @@ def from_merging_elements(
tm, length=combined_length, device=device, dtype=dtype, name=combined_name
)

def transfer_map(self, energy: torch.Tensor) -> torch.Tensor:
def transfer_map(
self, energy: torch.Tensor, particle_mass_eV: torch.Tensor
) -> torch.Tensor:
return self.predefined_transfer_map

@property
Expand Down
22 changes: 9 additions & 13 deletions cheetah/accelerator/dipole.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import matplotlib.pyplot as plt
import torch
from matplotlib.patches import Rectangle
from scipy.constants import physical_constants

from cheetah.accelerator.element import Element
from cheetah.particles import Beam, ParticleBeam
Expand All @@ -12,8 +11,6 @@

generate_unique_name = UniqueNameGenerator(prefix="unnamed_element")

electron_mass_eV = physical_constants["electron mass energy equivalent in MeV"][0] * 1e6


class Dipole(Element):
"""
Expand Down Expand Up @@ -189,10 +186,9 @@ def _track_bmadx(self, incoming: ParticleBeam) -> ParticleBeam:
py = incoming.py
tau = incoming.tau
delta = incoming.p
mc2 = incoming.species.mass_eV

z, pz, p0c = bmadx.cheetah_to_bmad_z_pz(
tau, delta, incoming.energy, electron_mass_eV
)
z, pz, p0c = bmadx.cheetah_to_bmad_z_pz(tau, delta, incoming.energy, mc2)

# Begin Bmad-X tracking
x, px, y, py = bmadx.offset_particle_set(
Expand All @@ -207,9 +203,7 @@ def _track_bmadx(self, incoming: ParticleBeam) -> ParticleBeam:

if self.fringe_at == "entrance" or self.fringe_at == "both":
px, py = self._bmadx_fringe_linear("entrance", x, px, y, py)
x, px, y, py, z, pz = self._bmadx_body(
x, px, y, py, z, pz, p0c, electron_mass_eV
)
x, px, y, py, z, pz = self._bmadx_body(x, px, y, py, z, pz, p0c, mc2)
if self.fringe_at == "exit" or self.fringe_at == "both":
px, py = self._bmadx_fringe_linear("exit", x, px, y, py)

Expand All @@ -225,9 +219,7 @@ def _track_bmadx(self, incoming: ParticleBeam) -> ParticleBeam:
# End of Bmad-X tracking

# Convert back to Cheetah coordinates
tau, delta, ref_energy = bmadx.bmad_to_cheetah_z_pz(
z, pz, p0c, electron_mass_eV
)
tau, delta, ref_energy = bmadx.bmad_to_cheetah_z_pz(z, pz, p0c, mc2)

# Broadcast to align their shapes so that they can be stacked
x, px, y, py, tau, delta = torch.broadcast_tensors(x, px, y, py, tau, delta)
Expand All @@ -241,6 +233,7 @@ def _track_bmadx(self, incoming: ParticleBeam) -> ParticleBeam:
survival_probabilities=incoming.survival_probabilities,
device=incoming.particles.device,
dtype=incoming.particles.dtype,
species=incoming.species,
)
return outgoing_beam

Expand Down Expand Up @@ -374,7 +367,9 @@ def _bmadx_fringe_linear(

return px_f, py_f

def transfer_map(self, energy: torch.Tensor) -> torch.Tensor:
def transfer_map(
self, energy: torch.Tensor, particle_mass_eV: torch.Tensor
) -> torch.Tensor:
device = self.length.device
dtype = self.length.dtype

Expand All @@ -386,6 +381,7 @@ def transfer_map(self, energy: torch.Tensor) -> torch.Tensor:
length=self.length,
k1=self.k1,
hx=self.hx,
particle_mass_eV=particle_mass_eV,
tilt=torch.zeros_like(self.length),
energy=energy,
) # Tilt is applied after adding edges
Expand Down
16 changes: 8 additions & 8 deletions cheetah/accelerator/drift.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,13 @@

import matplotlib.pyplot as plt
import torch
from scipy.constants import physical_constants

from cheetah.accelerator.element import Element
from cheetah.particles import Beam, ParticleBeam
from cheetah.utils import UniqueNameGenerator, bmadx, compute_relativistic_factors

generate_unique_name = UniqueNameGenerator(prefix="unnamed_element")

electron_mass_eV = physical_constants["electron mass energy equivalent in MeV"][0] * 1e6


class Drift(Element):
"""
Expand Down Expand Up @@ -39,11 +36,13 @@ def __init__(
self.length = torch.as_tensor(length, **factory_kwargs)
self.tracking_method = tracking_method

def transfer_map(self, energy: torch.Tensor) -> torch.Tensor:
def transfer_map(
self, energy: torch.Tensor, particle_mass_eV: torch.Tensor
) -> torch.Tensor:
device = self.length.device
dtype = self.length.dtype

_, igamma2, beta = compute_relativistic_factors(energy)
_, igamma2, beta = compute_relativistic_factors(energy, particle_mass_eV)

vector_shape = torch.broadcast_shapes(self.length.shape, igamma2.shape)

Expand Down Expand Up @@ -91,18 +90,18 @@ def _track_bmadx(self, incoming: ParticleBeam) -> ParticleBeam:
delta = incoming.p

z, pz, p0c = bmadx.cheetah_to_bmad_z_pz(
tau, delta, incoming.energy, electron_mass_eV
tau, delta, incoming.energy, incoming.species.mass_eV
)

# Begin Bmad-X tracking
x, y, z = bmadx.track_a_drift(
self.length, x, px, y, py, z, pz, p0c, electron_mass_eV
self.length, x, px, y, py, z, pz, p0c, incoming.species.mass_eV
)
# End of Bmad-X tracking

# Convert back to Cheetah coordinates
tau, delta, ref_energy = bmadx.bmad_to_cheetah_z_pz(
z, pz, p0c, electron_mass_eV
z, pz, p0c, incoming.species.mass_eV
)

# Broadcast to align their shapes so that they can be stacked
Expand All @@ -117,6 +116,7 @@ def _track_bmadx(self, incoming: ParticleBeam) -> ParticleBeam:
survival_probabilities=incoming.survival_probabilities,
device=incoming.particles.device,
dtype=incoming.particles.dtype,
species=incoming.species,
)
return outgoing_beam

Expand Down
Loading
Loading