Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
55 changes: 44 additions & 11 deletions examples/interstage_example.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ dependencies = [
"ax-platform == 1.1.2",
"CLICopti >= 2.2",
"RF-Track == 2.5.3",
"impactx-noacc >=25.11, <=25.12",
"impactx-noacc >=26.5",
"toml"
]
requires-python = ">=3,<3.13"
Expand Down
40 changes: 39 additions & 1 deletion src/abel/classes/interstage/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from abc import abstractmethod
from abel.classes.trackable import Trackable
from abel.classes.cost_modeled import CostModeled
from abel.CONFIG import CONFIG
from types import SimpleNamespace
import numpy as np
import scipy.constants as SI
Expand Down Expand Up @@ -339,9 +340,46 @@ def plot_evolution(self):
axs[2,2].set_xlabel(long_label)
axs[2,2].set_xlim(long_limits)


plt.show()


def plot_spin_evolution(self):
"""
Plot the spin evolution inside the interstage.
"""

from matplotlib import pyplot as plt

evol = self.evolution

# stop if no evolution calculated
if not hasattr(evol, 'location'):
print('No evolution calculated.')
return
if not hasattr(evol, 'spin_x'):
print('No spin evolution calculated.')
return

# preprate plot
fig, axs = plt.subplots(1,1)
fig.set_figwidth(CONFIG.plot_width_default)
fig.set_figheight(CONFIG.plot_width_default*0.5)
colx = "tab:blue"
coly = "tab:orange"
colz = "tab:green"
long_label = 'Location [m]'
long_limits = [min(evol.location), max(evol.location)]

# plot energy
axs.plot(evol.location, evol.spin_x, color=colx, label='x')
axs.plot(evol.location, evol.spin_y, color=coly, label='y')
axs.plot(evol.location, evol.spin_z, color=colz, label='z')
axs.set_ylabel('Spin polarization')
axs.set_xlabel(long_label)
axs.set_xlim(long_limits)
axs.set_ylim(-1.02, 1.02)
axs.legend()


## PLOTTING OPTICS

Expand Down
13 changes: 5 additions & 8 deletions src/abel/classes/source/source.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,9 +73,7 @@ class Source(Trackable, CostModeled):

spin_polarization_direction : str
Defines the orientation of the spin. Can be ``'x'``, ``'y'`` or ``'z'``.

is_polarized : bool
Whether the source produces polarized particles.

"""
#TODO: Why is accel_gradient needed??

Expand Down Expand Up @@ -113,9 +111,7 @@ def __init__(self, length=0.0, charge=None, energy=None, accel_gradient=None, wa

self.spin_polarization = 0
self.spin_polarization_direction = 'z'

self.is_polarized = False #TODO shouldn't this rather be a function that checks whether spin_polarization > 0?



@abstractmethod
def track(self, beam, savedepth=0, runnable=None, verbose=False):
Expand Down Expand Up @@ -159,7 +155,8 @@ def track(self, beam, savedepth=0, runnable=None, verbose=False):
beam.bunch_separation = self.bunch_separation

# set spin polarization
beam.set_arbitrary_spin_polarization(self.spin_polarization, direction=self.spin_polarization_direction)
if abs(self.spin_polarization) > 0:
beam.set_arbitrary_spin_polarization(self.spin_polarization, direction=self.spin_polarization_direction)

# rotate the beam according to its pointing angles
do_beam_rotation = False
Expand Down Expand Up @@ -196,7 +193,7 @@ def get_length(self):


def get_cost_breakdown(self):
if self.is_polarized:
if abs(self.spin_polarization) > 0:
if self.charge < 0:
return ('Polarized electron source', CostModeled.cost_per_source_polarized_electrons)
else:
Expand Down
62 changes: 55 additions & 7 deletions src/abel/wrappers/impactx/impactx_wrapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ def run_impactx(lattice, beam0, nom_energy=None, runnable=None, keep_data=False,
sim.isr_order = 3
else:
sim.isr_order = 1
sim.spin = beam0.spin_check()

# convert to ImpactX particle container
_, sim = beam2particle_container(beam0, sim=sim, verbose=verbose, nom_energy=nom_energy)
Expand All @@ -128,7 +129,7 @@ def run_impactx(lattice, beam0, nom_energy=None, runnable=None, keep_data=False,
eval('sim.track_particles()')

# convert back to ABEL beam
beam = particle_container2beam(sim.particle_container())
beam = particle_container2beam(sim.particle_container(), extract_spins=beam0.spin_check())

# clean shutdown
sim.finalize()
Expand Down Expand Up @@ -226,7 +227,8 @@ def run_envelope_impactx(lattice, distr, nom_energy=None, peak_current=None, spa
# reference particle
ref = sim.particle_container().ref_particle()
ref.set_charge_qe(1.0).set_mass_MeV(SI.m_e*SI.c**2/SI.e/1e6).set_kin_energy_MeV(nom_energy/1e6) # TODO: what if nom_energy is None?

ref.set_species("electron")

# initialize the envelope
sim.init_envelope(ref, distr, peak_current) # TODO: what if peak_current is None?

Expand Down Expand Up @@ -430,20 +432,27 @@ def extract_evolution(path=''):
evol.charge = diags["charge_C"]
evol.dispersion_x = diags["dispersion_x"]
evol.dispersion_y = diags["dispersion_y"]
if {'mean_sx', 'mean_sy', 'mean_sz'}.issubset(diags.columns):
evol.spin_x = diags["mean_sx"]
evol.spin_y = diags["mean_sy"]
evol.spin_z = diags["mean_sz"]

return evol


# ==================================================
# convert from ImpactX particle container to ABEL beam
def particle_container2beam(particle_container):
def particle_container2beam(particle_container, extract_spins=False):
"""
Convert from ImpactX particle container to an ABEL beam object.

Parameters
----------
particle_container : ImpactX ``ParticleContainer``
ImpactX ``ParticleContainer`` to be converted.

extract_spins : bool, optional
Whether to extract the spins or not.

Returns
-------
Expand All @@ -467,8 +476,13 @@ def particle_container2beam(particle_container):
beam.set_uys(ref.pz*(array[:,5])*SI.c)
beam.set_uzs(ref.pz*(1-array[:,6])*SI.c)

q_particle = array[0,7]*(SI.m_e*SI.c**2/SI.e)*SI.e
beam.set_qs(array[:,8]*q_particle)
q_particle = array[0,-2]*(SI.m_e*SI.c**2/SI.e)*SI.e
beam.set_qs(array[:,-1]*q_particle)

if extract_spins:
beam.set_spxs(array[:,7])
beam.set_spys(array[:,8])
beam.set_spzs(array[:,9])

beam.location = ref.s

Expand Down Expand Up @@ -522,6 +536,7 @@ def beam2particle_container(beam, nom_energy=None, sim=None, verbose=False):

# reference particle
ref = sim.particle_container().ref_particle()
ref.set_species("electron")
ref.set_charge_qe(beam.charge_sign())
ref.set_mass_MeV(0.510998950)
ref.set_kin_energy_MeV(nom_energy/1e6)
Expand All @@ -537,13 +552,15 @@ def beam2particle_container(beam, nom_energy=None, sim=None, verbose=False):
dpx_podv = amr.PODVector_real_std()
dpy_podv = amr.PODVector_real_std()
dpt_podv = amr.PODVector_real_std()
dw_podv = amr.PODVector_real_std()
else: # initialize on device using arena/gpu-based PODVectors
dx_podv = amr.PODVector_real_arena()
dy_podv = amr.PODVector_real_arena()
dt_podv = amr.PODVector_real_arena()
dpx_podv = amr.PODVector_real_arena()
dpy_podv = amr.PODVector_real_arena()
dpt_podv = amr.PODVector_real_arena()
dw_podv = amr.PODVector_real_arena()

for p_dx in dx:
dx_podv.push_back(p_dx)
Expand All @@ -557,10 +574,41 @@ def beam2particle_container(beam, nom_energy=None, sim=None, verbose=False):
dpy_podv.push_back(p_dpy)
for p_dpt in dpt:
dpt_podv.push_back(p_dpt)


for p_w in beam.weightings():
dw_podv.push_back(p_w)

particle_container = sim.particle_container()
qm_eev = -1.0 / 0.510998950e6 # electron charge/mass in e / eV
particle_container.add_n_particles(dx_podv, dy_podv, dt_podv, dpx_podv, dpy_podv, dpt_podv, qm_eev, beam.abs_charge())

# add spins if these are tracked
if beam.spin_check():

# set up vectors
if not Config.have_gpu:
dsx_podv = amr.PODVector_real_std()
dsy_podv = amr.PODVector_real_std()
dsz_podv = amr.PODVector_real_std()
else:
dsx_podv = amr.PODVector_real_arena()
dsy_podv = amr.PODVector_real_arena()
dsz_podv = amr.PODVector_real_arena()

# populate with spins
for sx in beam.spxs():
dsx_podv.push_back(sx)
for sy in beam.spys():
dsy_podv.push_back(sy)
for sx in beam.spzs():
dsz_podv.push_back(sx)

# make particle container (with spins)
particle_container.add_n_particles(dx_podv, dy_podv, dt_podv, dpx_podv, dpy_podv, dpt_podv, qm_eev, beam.abs_charge(), None, dsx_podv, dsy_podv, dsz_podv)

else:

# make particle container (no spins)
particle_container.add_n_particles(dx_podv, dy_podv, dt_podv, dpx_podv, dpy_podv, dpt_podv, qm_eev, beam.abs_charge())

return particle_container, sim

6 changes: 3 additions & 3 deletions tests/test_StageBasic_beamline.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,11 +540,11 @@ def test_ramped_jitter_linac():
assert np.isclose(final_beam.rel_energy_spread(), 9.825586100176821e-05, rtol=1e-2, atol=0.0)

nom_beam_size_x = (stages[0].nom_energy/stages[-1].nom_energy)**(1/4) * initial_beam.beam_size_x()
#nom_beam_size_y = (stages[0].nom_energy/stages[-1].nom_energy)**(1/4) * initial_beam.beam_size_y()
nom_beam_size_y = (stages[0].nom_energy/stages[-1].nom_energy)**(1/4) * initial_beam.beam_size_y()
assert np.isclose(final_beam.beam_size_x(), nom_beam_size_x, rtol=1e-2, atol=0.0)
assert np.isclose(final_beam.beam_size_y(), 1.2967329484466221e-06, rtol=1e-1, atol=0.0)
assert np.isclose(final_beam.beam_size_y(), nom_beam_size_y, rtol=1e-1, atol=0.0)
assert np.isclose(final_beam.norm_emittance_x(), main_source.emit_nx, rtol=1e-2, atol=0.0)
assert np.isclose(final_beam.norm_emittance_y(), 1.084878053041328e-06, rtol=1e-2, atol=0.0)
assert np.isclose(final_beam.norm_emittance_y(), 2.33*main_source.emit_ny, rtol=1e-1, atol=0.0)

#ref_beam = Beam.load('./tests/data/')
#ref_beam.beam_name = 'Reference beam'
Expand Down
14 changes: 7 additions & 7 deletions tests/test_StageQuasistatic2d.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,11 +166,11 @@ def test_driver_unrotation():
driver0.set_ys(ys + y_drift)

# Cannot compare the whole phase space due to driver evolution
assert np.isclose(driver.x_offset(), driver0.x_offset(), rtol=1e-4, atol=0.0)
assert np.isclose(driver.y_offset(), driver0.y_offset(), rtol=1e-4, atol=0.0)
assert np.isclose(driver.x_offset(), driver0.x_offset(), rtol=1e-3, atol=0.0)
assert np.isclose(driver.y_offset(), driver0.y_offset(), rtol=1e-3, atol=0.0)
assert np.isclose(driver.z_offset(), driver0.z_offset(), rtol=1e-9, atol=0.0)
assert np.isclose(driver.x_angle(), driver0.x_angle(), rtol=1e-4, atol=0.0)
assert np.isclose(driver.y_angle(), driver0.y_angle(), rtol=1e-4, atol=0.0)
assert np.isclose(driver.x_angle(), driver0.x_angle(), rtol=1e-3, atol=0.0)
assert np.isclose(driver.y_angle(), driver0.y_angle(), rtol=1e-3, atol=0.0)


# ========== Driver jitter, no angular offset ==========
Expand All @@ -193,10 +193,10 @@ def test_driver_unrotation():

# Cannot compare the whole phase space due to driver evolution
assert np.isclose(driver.x_offset(), driver0.x_offset(), rtol=1e-1, atol=0.0)
assert np.isclose(driver.y_offset(), driver0.y_offset(), rtol=1e-3, atol=0.0)
assert np.isclose(driver.y_offset(), driver0.y_offset(), rtol=1e-2, atol=0.0)
assert np.isclose(driver.z_offset(), driver0.z_offset(), rtol=1e-9, atol=0.0)
assert np.isclose(driver.x_angle(), driver0.x_angle(), rtol=1e-3, atol=0.0)
assert np.isclose(driver.y_angle(), driver0.y_angle(), rtol=1e-3, atol=0.0)
assert np.isclose(driver.x_angle(), driver0.x_angle(), rtol=1e-2, atol=0.0)
assert np.isclose(driver.y_angle(), driver0.y_angle(), rtol=1e-2, atol=0.0)
assert np.isclose(driver.norm_emittance_x(), driver0.norm_emittance_x(), rtol=1e-2, atol=0.0)
assert np.isclose(driver.norm_emittance_y(), driver0.norm_emittance_y(), rtol=1e-2, atol=0.0)
assert np.isclose(driver.bunch_length(), driver0.bunch_length(), rtol=1e-10, atol=0.0)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_StageReducedModels.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,7 @@ def test_driver_unrotation():
assert stage.driver_source.align_beam_axis is True
driver0 = stage.driver_incoming

Beam.comp_beams(driver, driver0, comp_location=False, rtol=1e-13, atol=0.0)
Beam.comp_beams(driver, driver0, comp_location=False, rtol=1e-7, atol=0.0)


# ========== No jitter, large angular offset ==========
Expand Down
2 changes: 1 addition & 1 deletion tests/test_StageReducedModels_beamline.py
Original file line number Diff line number Diff line change
Expand Up @@ -586,7 +586,7 @@ def test_jitter_trInstability_ramped_linac():
assert np.isclose(final_beam.beta_x(), nom_beta_x, rtol=0.1, atol=0.0)
assert np.isclose(final_beam.beta_y(), nom_beta_y, rtol=0.1, atol=0.0)
assert np.isclose(final_beam.norm_emittance_x(), main_source.emit_nx, rtol=0.1, atol=0.0)
assert np.isclose(final_beam.norm_emittance_y(), 1.0996284764402848e-07, rtol=1e-2, atol=0.0) # Expect emittance growth.
assert np.isclose(final_beam.norm_emittance_y(), 1.038123343811976e-07, rtol=1e-1, atol=0.0) # Expect emittance growth.

# Remove output directory
shutil.rmtree(linac.run_path())
Expand Down
Loading
Loading