From 2afb94e6365d828431dc184582c0d98c8d32f6c3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Birk=20Karlsen-B=C3=A6ck?= <birk.karlsen.baeck@gmail.com>
Date: Thu, 31 Oct 2024 20:20:17 +0100
Subject: [PATCH 1/9] Added BLonD-Xsuite interface to BLonD along with
 benchmarking examples

---
 ..._single_particle_with_oneturnmatrix_lhc.py | 226 ++++++++++
 .../EX_24_single_particle_ramp_lhc.py         | 236 +++++++++++
 ...X_25_single_particle_two_rfstations_lhc.py | 256 +++++++++++
 ...EX_26_single_particle_oneturnmatrix_psb.py | 232 ++++++++++
 .../EX_27_single_particle_ramp_psb.py         | 238 +++++++++++
 __EXAMPLES/main_files/EX_28_phase_loop_lhc.py | 246 +++++++++++
 blond/interfaces/__init__.py                  |   0
 blond/interfaces/xsuite.py                    | 401 ++++++++++++++++++
 8 files changed, 1835 insertions(+)
 create mode 100644 __EXAMPLES/main_files/EX_23_single_particle_with_oneturnmatrix_lhc.py
 create mode 100644 __EXAMPLES/main_files/EX_24_single_particle_ramp_lhc.py
 create mode 100644 __EXAMPLES/main_files/EX_25_single_particle_two_rfstations_lhc.py
 create mode 100644 __EXAMPLES/main_files/EX_26_single_particle_oneturnmatrix_psb.py
 create mode 100644 __EXAMPLES/main_files/EX_27_single_particle_ramp_psb.py
 create mode 100644 __EXAMPLES/main_files/EX_28_phase_loop_lhc.py
 create mode 100644 blond/interfaces/__init__.py
 create mode 100644 blond/interfaces/xsuite.py

diff --git a/__EXAMPLES/main_files/EX_23_single_particle_with_oneturnmatrix_lhc.py b/__EXAMPLES/main_files/EX_23_single_particle_with_oneturnmatrix_lhc.py
new file mode 100644
index 00000000..988d40c9
--- /dev/null
+++ b/__EXAMPLES/main_files/EX_23_single_particle_with_oneturnmatrix_lhc.py
@@ -0,0 +1,226 @@
+'''
+LHC simulation of a single particle.
+
+:Author: **Thom Arnoldus van Rijswijk**
+'''
+
+# Imports
+import os
+import numpy as np
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+#mpl.use('Agg')
+
+# Xsuite imports
+import xpart as xp
+import xtrack as xt
+import xplt
+
+# BLonD objects
+from blond.beam.beam import Beam, Proton
+from blond.input_parameters.ring import Ring
+from blond.input_parameters.rf_parameters import RFStation
+from blond.trackers.tracker import RingAndRFTracker
+
+# Interface
+from xsuite_blond_interface import (BlondElement, BlondObserver,
+                                    blond_beam_to_xsuite_coords,
+                                    xsuite_coords_to_blond_coords)
+
+# Monitor objects
+from blond.monitors.monitors import BunchMonitor
+from blond.plots.plot import Plot
+
+# Directory to save files
+this_directory = os.path.dirname(os.path.realpath(__file__)) + '/'
+results_directory = this_directory + '../output_files/EX_01'
+os.makedirs(results_directory, exist_ok=True)
+
+# Parameters ----------------------------------------------------------------------------------------------------------
+# Accelerator parameters
+C = 26658.8832                                  # Machine circumference [m]
+p_s = 450e9                                     # Synchronous momentum [eV/c]
+p_f = 450.0e9                                   # Synchronous momentum, final
+h = 35640                                       # Harmonic number [-]
+alpha = 0.00034849575112251314                  # First order mom. comp. factor [-]
+V = 5e6                                         # RF voltage [V]
+dphi = 0                                        # Phase modulation/offset [rad]
+
+# Bunch parameters
+N_bunches = 1                                   # Number of bunches [-]
+N_m = 1                                         # Number of macroparticles [-]
+N_p = 1.15e11                                   # Intensity 
+blen = 1.25e-9                                  # Bunch length [s]
+energy_err = 100e6                              # Beamenergy error [eV/c]
+
+# Simulation parameters
+N_t = 481                                       # Number of (tracked) turns [-]
+N_buckets = 2                                   # Number of buckets [-]
+dt_plt = 50                                     # Timestep between plots [-]
+input_dt = 2 * blen - 0.1e-9                    # Input particles dt [s]
+input_dE = 0.0                                  # Input particles dE [eV]
+
+# Preparing BLonD items -----------------------------------------------------------------------------------------------
+print(f"\nPreparing BLonD items...")
+# --- BLonD objects ---
+ring = Ring(C, alpha, np.linspace(p_s, p_f, N_t + 1), Proton(), n_turns=N_t)
+rfstation = RFStation(ring, [h], [V], [dphi]) 
+beam = Beam(ring, N_m, N_p)
+
+# --- Insert particle ---
+beam.dt = np.array([input_dt])
+beam.dE = np.array([input_dE])
+
+# --- RF tracker object ---
+blond_tracker = RingAndRFTracker(rfstation, beam, Profile=None, interpolation=False,
+                                 CavityFeedback=None, BeamFeedback=None,
+                                 TotalInducedVoltage=None, 
+                                 with_xsuite=True)
+
+# --- Bunch monitor ---
+# Print parameters for plotting
+print(f"\n Calculating plotting parameters...")
+
+t_u = np.pi / rfstation.omega_rf[0, 0]
+Y_phi_s = np.sqrt(abs(-np.cos(rfstation.phi_s[0]) + (np.pi - 2 * rfstation.phi_s[0]) / 2 * np.sin(rfstation.phi_s[0])))
+dE_sep = np.sqrt(2 * abs(rfstation.charge) * rfstation.voltage[0, 0] * rfstation.beta[0] * rfstation.beta[0]
+                 * rfstation.energy[0] / (np.pi * rfstation.harmonic[0, 0] * abs(ring.eta_0[0, 0]))) * Y_phi_s
+
+print('t_u: ' + str(2 * t_u))
+print('dE_sep,m: ' + str(dE_sep))
+
+# Make bunchmonitor
+bunchmonitor = BunchMonitor(ring, rfstation, beam,
+                            results_directory + '/EX_01_output_data', Profile=None)
+
+format_options = {'dirname': results_directory + '/EX_01_fig'}
+plots = Plot(ring, rfstation, beam, dt_plt, N_t, 0, 2 * t_u,
+             -1.05 * dE_sep, 1.05 * dE_sep, xunit='s', separatrix_plot=True,
+             Profile=None, h5file=results_directory + '/EX_01_output_data',
+             format_options=format_options)
+
+# --- Creating interface elements ---
+# Blond tracker
+cavity = BlondElement(blond_tracker, beam)
+# BLonD monitor
+beam_monitor = BlondElement(bunchmonitor, beam)
+beam_plots = BlondElement(plots, beam)
+
+# Preparing Xsuite items -----------------------------------------------------------------------------------------------
+print(f"\nPreparing Xsuite items...")
+# --- Setup matrix ---
+# Make First order matrix map (takes care of drift in Xsuite)
+matrix = xt.LineSegmentMap(
+    longitudinal_mode='nonlinear',
+    qx=1.1, qy=1.2,
+    betx=1., 
+    bety=1., 
+    voltage_rf=0,
+    frequency_rf=0,
+    lag_rf=0,
+    momentum_compaction_factor=alpha,
+    length=C
+)
+
+# Create line
+line = xt.Line(elements=[matrix], element_names={'matrix'} )
+line['matrix'].length = C
+
+# Insert the BLonD elements
+line.insert_element(index=0, element=cavity, name='blond_cavity')
+line.append_element(element=beam_monitor, name='blond_monitor')
+line.append_element(element=beam_plots, name='blond_plots')
+
+# Add particles to line and build tracker 
+line.particle_ref = xp.Particles(p0c=p_s, mass0=xp.PROTON_MASS_EV, q0=1.)
+line.build_tracker()
+
+# Show table
+line.get_table().show()
+# prints:
+
+# name                s element_type   isthick isreplica parent_name iscollective
+# blond_cavity        0 BlondElement     False     False None                True
+# matrix              0 LineSegmentMap    True     False None               False
+# blond_monitor 26658.9 BlondObserver    False     False None                True
+# blond_plots   26658.9 BlondElement     False     False None                True
+# _end_point    26658.9                  False     False None               False
+
+# Simulating ----------------------------------------------------------------------------------------------------------
+print(f"\nSetting up simulation...")
+
+# --- Convert the initial BLonD distribution to xsuite coordinates ---
+zeta, ptau = blond_beam_to_xsuite_coords(beam, 
+                                         line.particle_ref.beta0[0],
+                                         line.particle_ref.energy0[0],
+                                         phi_s=rfstation.phi_s[0], 
+                                         omega_rf=rfstation.omega_rf[0, 0])
+
+# --- Track matrix ---
+particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
+line.track(particles, num_turns=N_t, turn_by_turn_monitor=True, with_progress=True)
+mon = line.record_last_track
+
+# Saving turn-by-turn particle coordinates
+test_string = ''
+test_string += '{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\n'.format(
+    'dE', 'dt', 'momentum', 'gamma', 'beta', 'energy', 'x', 'px', 'y', 'py')
+test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
+                '\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n').format(
+                beam.dE[0], beam.dt[0], rfstation.momentum[0], rfstation.gamma[0], rfstation.beta[0],
+                rfstation.energy[0], mon.x[0, 0].T, mon.px[0, 0].T, mon.y[0, 0].T, mon.py[0, 0].T)
+
+# Saving turn-by-turn Xsuite particle coordinates
+xsuite_string = ''
+xsuite_string += '{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\n'.format(
+    'ptau', 'zeta', 'momentum', 'gamma', 'beta')
+xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.format(
+                mon.ptau[0, 0].T, mon.zeta[0, 0].T, np.float64(mon.p0c[0, 0]), np.float64(mon.gamma0[0, 0]),
+                np.float64(mon.beta0[0, 0]))
+
+# Convert the xsuite particle coordinates back to BLonD
+for i in range(N_t):
+    dt, dE = xsuite_coords_to_blond_coords(mon.zeta[:, i].T, mon.ptau[:, i].T,
+                                           rfstation.beta[i],
+                                           rfstation.energy[i],
+                                           phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
+                                           omega_rf=rfstation.omega_rf[0, i])
+
+    # Statistics
+    test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
+                    '\t{:+10.10e}\t{:+10.10e}\n').format(
+                    dE[0], dt[0], rfstation.momentum[i], rfstation.gamma[i], rfstation.beta[i],
+                    rfstation.energy[i], mon.x[0, i].T, mon.px[0, i].T, mon.y[0, i].T, mon.py[0, i].T)
+    xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.format(
+                mon.ptau[0, i].T, mon.zeta[0, i].T, np.float64(mon.p0c[0, i]), np.float64(mon.gamma0[0, i]),
+                np.float64(mon.beta0[0, i]))
+
+with open(results_directory + '/EX_01_output_data.txt', 'w') as f:
+    f.write(test_string)
+with open(results_directory + '/EX_01_output_data_xsuite.txt', 'w') as f:
+    f.write(xsuite_string)
+
+# Results --------------------------------------------------------------------------------------------------------------
+print(f"\nPlotting result...")
+
+# Plot Phasespace
+
+plt.figure()
+plt.plot(mon.zeta.T, mon.ptau.T)
+plt.scatter(np.mean(mon.zeta.T), np.mean(mon.ptau.T), label='mon')
+plt.grid()
+plt.title('Phasespace')
+plt.xlabel(r'$\zeta$')
+plt.ylabel(r'$p_{\tau}$')
+plt.savefig(results_directory + '/figure_Phasespace.png')
+
+# Use Xplt to plot Phasespace
+
+plot = xplt.PhaseSpacePlot(
+    mon
+)
+plot.fig.suptitle("Particle distribution for a single turn")
+plot.fig.savefig(results_directory + '/figure_ps_mon.png')
+
+# Show plots
+plt.show()
diff --git a/__EXAMPLES/main_files/EX_24_single_particle_ramp_lhc.py b/__EXAMPLES/main_files/EX_24_single_particle_ramp_lhc.py
new file mode 100644
index 00000000..91e88d32
--- /dev/null
+++ b/__EXAMPLES/main_files/EX_24_single_particle_ramp_lhc.py
@@ -0,0 +1,236 @@
+'''
+LHC simulation of a single particle with a ramp.
+
+: Author: **Birk Emil Karlsen-Baeck**, **Thom Arnoldus van Rijswijk**
+'''
+
+# Imports
+import os
+import numpy as np
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+#mpl.use('Agg')
+
+# Xsuite imports
+import xpart as xp
+import xtrack as xt
+import xplt
+
+# BLonD objects
+from blond.beam.beam import Beam, Proton
+from blond.input_parameters.ring import Ring
+from blond.input_parameters.rf_parameters import RFStation
+from blond.trackers.tracker import RingAndRFTracker
+
+# Interface
+from xsuite_blond_interface import (BlondElement, BlondObserver, EnergyUpdate,
+                                    blond_beam_to_xsuite_coords,
+                                    xsuite_coords_to_blond_coords)
+
+# Monitor objects
+from blond.monitors.monitors import BunchMonitor
+from blond.plots.plot import Plot
+
+# Directory to save files
+this_directory = os.path.dirname(os.path.realpath(__file__)) + '/'
+results_directory = this_directory + '../output_files/EX_02'
+os.makedirs(results_directory, exist_ok=True)
+
+# Parameters ----------------------------------------------------------------------------------------------------------
+# Accelerator parameters
+C = 26658.8832                                  # Machine circumference [m]
+p_s = 450e9                                     # Synchronous momentum [eV/c]
+p_f = 450.1e9                                   # Synchronous momentum, final
+h = 35640                                       # Harmonic number [-]
+alpha = 0.00034849575112251314                  # First order mom. comp. factor [-]
+V = 5e6                                         # RF voltage [V]
+dphi = 0                                        # Phase modulation/offset [rad]
+
+# Bunch parameters
+N_bunches = 1                                   # Number of bunches [-]
+N_m = 1                                         # Number of macroparticles [-]
+N_p = 1.15e11                                   # Intensity 
+blen = 1.25e-9                                  # Bunch length [s]
+energy_err = 100e6                              # Beamenergy error [eV/c]
+
+# Simulation parameters
+N_t = 330                                       # Number of (tracked) turns [-]
+N_buckets = 2                                   # Number of buckets [-]
+dt_plt = 30                                     # Timestep between plots [-]
+input_dt = 2 * blen - 0.4e-9                    # Input particles dt [s]
+input_dE = 0.0                                  # Input particles dE [eV]
+
+# Xsuite parameters
+cavity_name = 'acsca.d5l4.b1'
+
+# Preparing BLonD items -----------------------------------------------------------------------------------------------
+print(f"\nPreparing BLonD items...")
+# --- BLonD objects ---
+ring = Ring(C, alpha, np.linspace(p_s, p_f, N_t + 1), Proton(), n_turns=N_t)
+rfstation = RFStation(ring, [h], [V], [dphi]) 
+beam = Beam(ring, N_m, N_p)
+
+# --- Insert particle ---
+beam.dt = np.array([input_dt])
+beam.dE = np.array([input_dE])
+
+# --- RF tracker object ---
+blond_tracker = RingAndRFTracker(rfstation, beam, Profile=None, interpolation=False,
+                                 CavityFeedback=None, BeamFeedback=None,
+                                 TotalInducedVoltage=None, 
+                                 with_xsuite=True)
+
+# --- Bunch monitor ---
+# Print parameters for plotting
+print(f"\n Calculating plotting parameters...")
+
+t_u = np.pi / rfstation.omega_rf[0, 0]
+Y_phi_s = np.sqrt(abs(-np.cos(rfstation.phi_s[0]) + (np.pi - 2 * rfstation.phi_s[0]) / 2 * np.sin(rfstation.phi_s[0])))
+dE_sep = np.sqrt(2 * abs(rfstation.charge) * rfstation.voltage[0, 0] * rfstation.beta[0] * rfstation.beta[0]
+                 * rfstation.energy[0] / (np.pi * rfstation.harmonic[0, 0] * abs(ring.eta_0[0, 0]))) * Y_phi_s
+
+print('t_u: ' + str(2 * t_u))
+print('dE_sep,m: ' + str(dE_sep))
+
+# Make bunchmonitor
+bunchmonitor = BunchMonitor(ring, rfstation, beam,
+                            results_directory + '/EX_02_output_data', Profile=None)
+
+format_options = {'dirname': results_directory + '/EX_02_fig'}
+plots = Plot(ring, rfstation, beam, dt_plt, N_t, 0, 2*t_u,
+             -1.05 * dE_sep, 1.05 * dE_sep, xunit='s', separatrix_plot=True,
+             Profile=None, h5file=results_directory + '/EX_02_output_data',
+             format_options=format_options)
+
+# --- Creating interface elements ---
+# Blond tracker
+cavity = BlondElement(blond_tracker, beam)
+# BLonD monitor
+beam_monitor = BlondElement(bunchmonitor, beam)
+beam_plots = BlondElement(plots, beam)
+
+# Preparing Xsuite items -----------------------------------------------------------------------------------------------
+print(f"\nPreparing Xsuite items...")
+# --- Setup matrix ---
+# Make First order matrix map (takes care of drift in Xsuite)
+matrix = xt.LineSegmentMap(
+    longitudinal_mode='nonlinear',
+    qx=1.1, qy=1.2,
+    betx=1., 
+    bety=1., 
+    voltage_rf=0,
+    frequency_rf=0,
+    lag_rf=0,
+    momentum_compaction_factor=alpha,
+    length=C)
+
+# Create line
+line = xt.Line(elements=[matrix], element_names={'matrix'})
+line['matrix'].length = C
+
+# Insert the BLonD elements
+line.insert_element(index=0, element=cavity, name='blond_cavity')
+line.append_element(element=beam_monitor, name='blond_monitor')
+line.append_element(element=beam_plots, name='blond_plots')
+
+# Insert energy ramp
+energy_update = EnergyUpdate(ring.momentum[0, :])
+line.insert_element(index='matrix', element=energy_update, name='energy_update')
+
+# Add particles to line and build tracker 
+line.particle_ref = xp.Particles(p0c=p_s, mass0=xp.PROTON_MASS_EV, q0=1.)
+line.build_tracker()
+
+# Show table
+line.get_table().show()
+# prints:
+
+# name                s element_type   isthick isreplica parent_name iscollective
+# energy_update       0 EnergyUpdate     False     False None                True
+# blond_cavity        0 BlondElement     False     False None                True
+# matrix              0 LineSegmentMap    True     False None               False
+# blond_monitor 26658.9 BlondObserver    False     False None                True
+# blond_plots   26658.9 BlondElement     False     False None                True
+# _end_point    26658.9                  False     False None               False
+
+# Simulating ----------------------------------------------------------------------------------------------------------
+print(f"\nSetting up simulation...")
+
+# --- Convert the initial BLonD distribution to xsuite coordinates ---
+zeta, ptau = blond_beam_to_xsuite_coords(beam, 
+                                         line.particle_ref.beta0[0],
+                                         line.particle_ref.energy0[0],
+                                         phi_s=rfstation.phi_s[0], 
+                                         omega_rf=rfstation.omega_rf[0, 0])
+
+# --- Track matrix ---
+particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
+line.track(particles, num_turns=N_t, turn_by_turn_monitor=True, with_progress=True)
+mon = line.record_last_track
+
+# Saving turn-by-turn particle coordinates
+test_string = ''
+test_string += '{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\n'.format(
+    'dE', 'dt', 'momentum', 'gamma', 'beta', 'energy', 'x', 'px', 'y', 'py')
+test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
+                '\t{:+10.10e}\t{:+10.10e}\n').format(beam.dE[0], beam.dt[0], rfstation.momentum[0],
+                                                     rfstation.gamma[0], rfstation.beta[0], rfstation.energy[0],
+                                                     mon.x[0, 0].T, mon.px[0, 0].T, mon.y[0, 0].T, mon.py[0, 0].T)
+
+# Saving turn-by-turn Xsuite particle coordinates
+xsuite_string = ''
+xsuite_string += '{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\n'.format(
+    'ptau', 'zeta', 'momentum', 'gamma', 'beta')
+xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.format(
+    mon.ptau[0, 0].T, mon.zeta[0, 0].T, np.float64(mon.p0c[0, 0]), np.float64(mon.gamma0[0, 0]),
+    np.float64(mon.beta0[0, 0])
+)
+
+# Convert the xsuite particle coordinates back to BLonD
+for i in range(N_t):
+    dt, dE = xsuite_coords_to_blond_coords(mon.zeta[:, i].T, mon.ptau[:, i].T,
+                                           rfstation.beta[i],
+                                           rfstation.energy[i],
+                                           phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
+                                           omega_rf=rfstation.omega_rf[0, i])
+
+    # Statistics
+    test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
+                    '\t{:+10.10e}\t{:+10.10e}\n').format(dE[0], dt[0], rfstation.momentum[i], rfstation.gamma[i],
+                                                         rfstation.beta[i], rfstation.energy[i], mon.x[0, i].T,
+                                                         mon.px[0, i].T, mon.y[0, i].T, mon.py[0, i].T)
+
+    xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.format(
+        mon.ptau[0, i].T, mon.zeta[0, i].T, np.float64(mon.p0c[0, i]), np.float64(mon.gamma0[0, i]),
+        np.float64(mon.beta0[0, i])
+    )
+
+with open(results_directory + '/EX_02_output_data.txt', 'w') as f:
+    f.write(test_string)
+with open(results_directory + '/EX_02_output_data_xsuite.txt', 'w') as f:
+    f.write(xsuite_string)
+
+# Results --------------------------------------------------------------------------------------------------------------
+print(f"\nPlotting result...")
+
+# Plot Phasespace
+
+plt.figure()
+plt.plot(mon.zeta.T,mon.ptau.T)
+plt.scatter(np.mean(mon.zeta.T), np.mean(mon.ptau.T), label='mon')
+plt.grid()
+plt.title('Phasespace')
+plt.xlabel(r'$\zeta$')
+plt.ylabel(r'$p_{\tau}$')
+plt.savefig(results_directory + '/figure_Phasespace.png')
+
+# Use Xplt to plot Phasespace
+
+plot = xplt.PhaseSpacePlot(
+    mon
+)
+plot.fig.suptitle("Particle distribution for a single turn")
+plot.fig.savefig(results_directory + '/figure_ps_mon.png')
+
+# Show plots
+plt.show()
diff --git a/__EXAMPLES/main_files/EX_25_single_particle_two_rfstations_lhc.py b/__EXAMPLES/main_files/EX_25_single_particle_two_rfstations_lhc.py
new file mode 100644
index 00000000..45c325e3
--- /dev/null
+++ b/__EXAMPLES/main_files/EX_25_single_particle_two_rfstations_lhc.py
@@ -0,0 +1,256 @@
+'''
+LHC simulation of a single particle with two RF stations.
+
+: Author: **Thom Arnoldus van Rijswijk**
+'''
+
+# Imports
+import os
+import numpy as np
+import matplotlib.pyplot as plt
+
+# Xsuite imports
+import xpart as xp
+import xtrack as xt
+import xplt
+
+# BLonD objects
+from blond.beam.beam import Beam, Proton
+from blond.input_parameters.ring import Ring
+from blond.input_parameters.rf_parameters import RFStation
+from blond.trackers.tracker import RingAndRFTracker
+
+# Interface
+from xsuite_blond_interface import (BlondElement,
+                                    blond_beam_to_xsuite_coords,
+                                    xsuite_coords_to_blond_coords)
+
+# Monitor objects
+from blond.monitors.monitors import BunchMonitor
+from blond.plots.plot import Plot
+
+# Directory to save files
+this_directory = os.path.dirname(os.path.realpath(__file__)) + '/'
+results_directory = this_directory + '../output_files/EX_03'
+os.makedirs(results_directory, exist_ok=True)
+
+# Parameters ----------------------------------------------------------------------------------------------------------
+# Accelerator parameters
+C = 26658.8832                                  # Machine circumference [m]
+p_s = 450e9                                     # Synchronous momentum [eV/c]
+p_f = 450.0e9                                   # Synchronous momentum, final
+h = 35640                                       # Harmonic number [-]
+alpha = 0.00034849575112251314                  # First order mom. comp. factor [-]
+V = 5e6                                         # RF voltage [V]
+dphi = 0                                        # Phase modulation/offset [rad]
+
+# Bunch parameters
+N_bunches = 1                                   # Number of bunches [-]
+N_m = 1                                         # Number of macroparticles [-]
+N_p = 1.15e11                                   # Intensity 
+blen = 1.25e-9                                  # Bunch length [s]
+energy_err = 100e6                              # Beamenergy error [eV/c]
+
+# Simulation parameters
+N_t = 481                                       # Number of (tracked) turns [-]
+N_buckets = 2                                   # Number of buckets [-]
+dt_plt = 50                                     # Timestep between plots [-]
+input_dt = 2 * blen - 0.1e-9                    # Input particles dt [s]
+input_dE = 0.0                                  # Input particles dE [eV]
+
+# Additional input
+C_part = 0.3
+V_part = 0.5
+
+# Preparing BLonD items -----------------------------------------------------------------------------------------------
+print(f"\nPreparing BLonD items...")
+# --- BLonD objects ---
+ring = Ring([C_part * C, (1 - C_part) * C],
+            [[alpha], [alpha]],
+            [p_s, p_s],
+            Proton(), n_turns=N_t, n_sections=2)
+rfstation_1 = RFStation(ring, [h], [V * V_part], [dphi], section_index=1)
+rfstation_2 = RFStation(ring, [h], [V * (1 - V_part)], [dphi], section_index=2)
+beam = Beam(ring, N_m, N_p)
+
+# --- Insert particle ---
+beam.dt = np.array([input_dt])
+beam.dE = np.array([input_dE])
+
+# --- RF tracker object ---
+rftracker_1 = RingAndRFTracker(rfstation_1, beam, Profile=None, interpolation=False,
+                               CavityFeedback=None, BeamFeedback=None,
+                               TotalInducedVoltage=None,
+                               with_xsuite=True)
+rftracker_2 = RingAndRFTracker(rfstation_2, beam, Profile=None, interpolation=False,
+                               CavityFeedback=None, BeamFeedback=None,
+                               TotalInducedVoltage=None,
+                               with_xsuite=True)
+
+# --- Bunch monitor ---
+# Print parameters for plotting
+print(f"\n Calculating plotting parameters...")
+
+t_u = np.pi / rfstation_1.omega_rf[0,0]
+Y_phi_s = np.sqrt(abs(-np.cos(rfstation_1.phi_s[0]) + (np.pi - 2*rfstation_1.phi_s[0])
+                      / 2 * np.sin(rfstation_1.phi_s[0])))
+dE_sep = np.sqrt(2 * abs(rfstation_1.charge) * V * rfstation_1.beta[0] * rfstation_1.beta[0] * rfstation_1.energy[0]
+                 / (np.pi * rfstation_1.harmonic[0, 0] * abs(ring.eta_0[0, 0]))) * Y_phi_s
+
+print('t_u: ' + str(2 * t_u))
+print('dE_sep,m: ' + str(dE_sep))
+
+# Make bunchmonitor
+bunchmonitor = BunchMonitor(ring, rfstation_1, beam,
+                            results_directory + '/EX_03_output_data', Profile=None)
+
+format_options = {'dirname': results_directory + '/EX_03_fig'}
+plots = Plot(ring, rfstation_1, beam, dt_plt, N_t, 0, 2 * t_u,
+             -1.05 * dE_sep, 1.05 * dE_sep, xunit='s', separatrix_plot=True,
+             Profile=None, h5file=results_directory + '/EX_03_output_data',
+             format_options=format_options)
+
+# --- Creating interface elements ---
+# Blond tracker
+cavity_1 = BlondElement(rftracker_1, beam)
+cavity_2 = BlondElement(rftracker_2, beam)
+# BLonD monitor
+beam_monitor = BlondElement(bunchmonitor, beam)
+beam_plots = BlondElement(plots, beam)
+
+# Xsuite initialization -----------------------------------------------------------------------------------------------
+print(f"\nXsuite initialization...")
+# --- Setup matrices ---
+# Make First order matrix maps (takes care of drift in Xsuite)
+matrix_1 = xt.LineSegmentMap(
+    longitudinal_mode='nonlinear',
+    qx=1.1, qy=1.2,
+    betx=1., 
+    bety=1., 
+    voltage_rf=0,
+    frequency_rf=0,
+    lag_rf=0,
+    momentum_compaction_factor=alpha,
+    length=C * C_part)
+matrix_2 = xt.LineSegmentMap(
+    longitudinal_mode='nonlinear',
+    qx=1.1, qy=1.2,
+    betx=1., 
+    bety=1., 
+    voltage_rf=0,
+    frequency_rf=0,
+    lag_rf=0,
+    momentum_compaction_factor=alpha,
+    length=C * (1 - C_part))
+
+# Create new line
+line = xt.Line(elements=[matrix_1], element_names={'matrix_1'} )
+line['matrix_1'].length = C_part * C
+line.append_element(element=matrix_2, name='matrix_2')
+line['matrix_2'].length = (1 - C_part) * C
+
+# Insert the BLonD elements
+line.insert_element(index='matrix_1', element=cavity_1, name='blond_cavity_1')
+line.insert_element(index='matrix_2', element=cavity_2, name='blond_cavity_2')
+line.append_element(element=beam_monitor, name='blond_monitor')
+line.append_element(element=beam_plots, name='blond_plots')
+
+# Add particles to line and build tracker 
+line.particle_ref = xp.Particles(p0c=p_s, mass0=xp.PROTON_MASS_EV, q0=1.)
+line.build_tracker()
+
+# Show table
+line.get_table().show()
+# prints:
+
+# name                 s element_type   isthick isreplica parent_name iscollective
+# blond_cavity_1       0 BlondElement     False     False None                True
+# matrix_1             0 LineSegmentMap    True     False None               False
+# blond_cavity_2 7997.66 BlondElement     False     False None                True
+# matrix_2       7997.66 LineSegmentMap    True     False None               False
+# blond_monitor  26658.9 BlondObserver    False     False None                True
+# blond_plots    26658.9 BlondElement     False     False None                True
+# _end_point     26658.9                  False     False None               False
+
+# Simulating ----------------------------------------------------------------------------------------------------------
+print(f"\nSetting up simulation...")
+
+# --- Convert the initial BLonD distribution to xsuite coordinates ---
+zeta, ptau = blond_beam_to_xsuite_coords(beam, 
+                                         line.particle_ref.beta0[0],
+                                         line.particle_ref.energy0[0],
+                                         phi_s=rfstation_1.phi_s[0], 
+                                         omega_rf=rfstation_1.omega_rf[0, 0])
+
+# --- Track matrix ---
+particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
+line.track(particles, num_turns=N_t, turn_by_turn_monitor=True, with_progress=True)
+mon = line.record_last_track
+
+# Saving turn-by-turn particle coordinates
+test_string = ''
+test_string += '{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\n'.format(
+    'dE', 'dt', 'momentum', 'gamma', 'beta', 'energy', 'x', 'px', 'y', 'py')
+test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
+                '\t{:+10.10e}\t{:+10.10e}\n').format(beam.dE[0], beam.dt[0], rfstation_1.momentum[0],
+                                                     rfstation_1.gamma[0], rfstation_1.beta[0], rfstation_1.energy[0],
+                                                     mon.x[0, 0].T, mon.px[0, 0].T, mon.y[0, 0].T, mon.py[0, 0].T)
+
+# Saving turn-by-turn Xsuite particle coordinates
+xsuite_string = ''
+xsuite_string += '{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\n'.format(
+    'ptau', 'zeta', 'momentum', 'gamma', 'beta')
+xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.format(
+    mon.ptau[0, 0].T, mon.zeta[0, 0].T, np.float64(mon.p0c[0, 0]), np.float64(mon.gamma0[0, 0]),
+    np.float64(mon.beta0[0, 0])
+)
+
+# Convert the xsuite particle coordinates back to BLonD
+for i in range(N_t):
+    dt, dE = xsuite_coords_to_blond_coords(mon.zeta[:,i].T, mon.ptau[:,i].T,
+                                           rfstation_1.beta[i],
+                                           rfstation_1.energy[i],
+                                           phi_s=rfstation_1.phi_s[i] - rfstation_1.phi_rf[0, 0],
+                                           omega_rf=rfstation_1.omega_rf[0, i])
+
+    # Statistics
+    test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
+                    '\t{:+10.10e}\t{:+10.10e}\n').format(dE[0], dt[0], rfstation_1.momentum[i],
+                                                         rfstation_1.gamma[i], rfstation_1.beta[i],
+                                                         rfstation_1.energy[i], mon.x[0, i].T, mon.px[0, i].T,
+                                                         mon.y[0, i].T, mon.py[0, i].T)
+
+    xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.format(
+        mon.ptau[0, i].T, mon.zeta[0, i].T, np.float64(mon.p0c[0, i]), np.float64(mon.gamma0[0, i]),
+        np.float64(mon.beta0[0, i])
+    )
+
+with open(results_directory + '/EX_03_output_data.txt', 'w') as f:
+    f.write(test_string)
+with open(results_directory + '/EX_03_output_data_xsuite.txt', 'w') as f:
+    f.write(xsuite_string)
+
+# Results --------------------------------------------------------------------------------------------------------------
+print(f"\nPlotting result...")
+
+# Plot Phasespace
+
+plt.figure()
+plt.plot(mon.zeta.T,mon.ptau.T)
+plt.scatter(np.mean(mon.zeta.T), np.mean(mon.ptau.T), label='mon')
+plt.grid()
+plt.title('Phasespace')
+plt.xlabel(r'$\zeta$')
+plt.ylabel(r'$p_{\tau}$')
+plt.savefig(results_directory + '/figure_Phasespace.png')
+
+# Use Xplt to plot Phasespace
+
+plot = xplt.PhaseSpacePlot(
+    mon
+)
+plot.fig.suptitle("Particle distribution for a single turn")
+plot.fig.savefig(results_directory + '/figure_ps_mon.png')
+
+# Show plots
+plt.show()
diff --git a/__EXAMPLES/main_files/EX_26_single_particle_oneturnmatrix_psb.py b/__EXAMPLES/main_files/EX_26_single_particle_oneturnmatrix_psb.py
new file mode 100644
index 00000000..aac0e0e4
--- /dev/null
+++ b/__EXAMPLES/main_files/EX_26_single_particle_oneturnmatrix_psb.py
@@ -0,0 +1,232 @@
+'''
+PSB simulation of a single particle.
+
+:Author: **Thom Arnoldus van Rijswijk**
+'''
+
+# Imports
+import os
+import numpy as np
+from scipy.constants import c, e, m_p
+import matplotlib.pyplot as plt
+
+# Xsuite imports
+import xpart as xp
+import xtrack as xt
+import xplt
+
+# BLonD objects
+from blond.beam.beam import Beam, Proton
+from blond.input_parameters.ring import Ring
+from blond.input_parameters.rf_parameters import RFStation
+from blond.trackers.tracker import RingAndRFTracker
+
+# Interface
+from xsuite_blond_interface import (BlondElement,
+                                    blond_beam_to_xsuite_coords,
+                                    xsuite_coords_to_blond_coords)
+
+# Monitor objects
+from blond.monitors.monitors import BunchMonitor
+from blond.plots.plot import Plot
+
+# Directory to save files
+this_directory = os.path.dirname(os.path.realpath(__file__)) + '/'
+results_directory = this_directory + '../output_files/EX_04'
+os.makedirs(results_directory, exist_ok=True)
+
+# Parameters ----------------------------------------------------------------------------------------------------------
+# Accelerator parameters
+C = 2 * np.pi * 25.000057945260654              # Machine circumference, radius 25 [m]
+gamma_t = 4.11635447373496                      # Transition gamma [-]
+alpha = 1./gamma_t/gamma_t                      # First order mom. comp. factor [-]
+h = 1                                           # Harmonic number [-]
+V = 8e3                                         # RF voltage [V]
+dphi = np.pi                                    # Phase modulation/offset [rad]
+
+# Beam parameters
+N_m = 1                                         # Number of macroparticles [-]
+N_p = 1e11                                      # Intensity / number of particles
+sigma_dt = 180e-9 / 4                           # [s]        
+blen = sigma_dt * 4                             # Initial bunch length, 4 sigma [s]
+kin_beam_energy = 1.4e9                         # [eV]
+
+
+# Simulation parameters
+N_t = 6800                                      # Number of (tracked) turns [-]
+N_buckets = 1                                   # Number of buckets [-]
+dt_plt = 500                                    # Timestep between plots [-]
+input_dt = 5.720357923415153e-07 - sigma_dt     # Input particles dt [s]
+input_dE = 0.0                                  # Input particles dE [eV]
+
+# Derived parameters
+E_0 = m_p * c**2 / e                                  # [eV]
+tot_beam_energy = E_0 + kin_beam_energy               # [eV]
+sync_momentum = np.sqrt(tot_beam_energy**2 - E_0**2)  # [eV / c]
+
+# Preparing BLonD items -----------------------------------------------------------------------------------------------
+print(f"\nPreparing BLonD items...")
+# --- BLonD objects ---
+ring = Ring(C, alpha, np.linspace(sync_momentum, sync_momentum, N_t + 1), Proton(), n_turns=N_t)
+rfstation = RFStation(ring, [h], [V], [dphi]) 
+beam = Beam(ring, N_m, N_p)
+
+# --- Insert particle ---
+beam.dt = np.array([input_dt])
+beam.dE = np.array([input_dE])
+
+# --- RF tracker object ---
+blond_tracker = RingAndRFTracker(rfstation, beam, Profile=None, interpolation=False,
+                                 CavityFeedback=None, BeamFeedback=None,
+                                 TotalInducedVoltage=None, 
+                                 with_xsuite=True)
+
+# --- Bunch monitor ---
+# Print parameters for plotting
+print(f"\n Calculating plotting parameters...")
+
+t_u = np.pi / rfstation.omega_rf[0, 0]
+Y_phi_s = np.sqrt(abs(-np.cos(rfstation.phi_s[0]) + (np.pi - 2 * rfstation.phi_s[0]) / 2 * np.sin(rfstation.phi_s[0])))
+dE_sep = np.sqrt(2 * abs(rfstation.charge) * rfstation.voltage[0, 0] * rfstation.beta[0] * rfstation.beta[0]
+                 * rfstation.energy[0] / (np.pi * rfstation.harmonic[0, 0] * abs(ring.eta_0[0, 0]))) * Y_phi_s
+
+print('t_u: ' + str(2 * t_u))
+print('dE_sep,m: ' + str(dE_sep))
+
+# Make bunchmonitor
+bunchmonitor = BunchMonitor(ring, rfstation, beam,
+                            results_directory + '/EX_04_output_data', Profile=None)
+
+format_options = {'dirname': results_directory + '/EX_04_fig'}
+plots = Plot(ring, rfstation, beam, dt_plt, N_t, 0, 2 * t_u,
+             -1.05 * dE_sep, 1.05 * dE_sep, xunit='s', separatrix_plot=True,
+             Profile=None, h5file=results_directory + '/EX_04_output_data',
+             format_options=format_options)
+
+# --- Creating interface elements ---
+# Blond tracker
+cavity = BlondElement(blond_tracker, beam)
+# BLonD monitor
+beam_monitor = BlondElement(bunchmonitor, beam)
+beam_plots = BlondElement(plots, beam)
+
+# Preparing Xsuite items -----------------------------------------------------------------------------------------------
+print(f"\nPreparing Xsuite items...")
+# --- Setup matrix ---
+# Make First order matrix map (takes care of drift in Xsuite)
+matrix = xt.LineSegmentMap(
+    longitudinal_mode='nonlinear',
+    qx=1.1, qy=1.2,
+    betx=1., 
+    bety=1., 
+    voltage_rf=0,
+    frequency_rf=0,
+    lag_rf=0,
+    momentum_compaction_factor=alpha,
+    length=C)
+
+# Create line
+line = xt.Line(elements=[matrix], element_names={'matrix'})
+line['matrix'].length = C
+
+# Insert the BLonD elements
+line.insert_element(index=0, element=cavity, name='blond_cavity')
+line.append_element(element=beam_monitor, name='blond_monitor')
+line.append_element(element=beam_plots, name='blond_plots')
+
+# Add particles to line and build tracker 
+line.particle_ref = xp.Particles(p0c=sync_momentum, mass0=xp.PROTON_MASS_EV, q0=1.)
+line.build_tracker()
+
+# Show table
+line.get_table().show()
+# prints:
+
+# name               s element_type   isthick isreplica parent_name iscollective
+# blond_cavity       0 BlondElement     False     False None                True
+# matrix             0 LineSegmentMap    True     False None               False
+# blond_monitor 157.08 BlondObserver    False     False None                True
+# blond_plots   157.08 BlondElement     False     False None                True
+# _end_point    157.08                  False     False None               False
+
+# Simulating ----------------------------------------------------------------------------------------------------------
+print(f"\nSetting up simulation...")
+
+# --- Convert the initial BLonD distribution to xsuite coordinates ---
+zeta, ptau = blond_beam_to_xsuite_coords(beam, 
+                                         line.particle_ref.beta0[0],
+                                         line.particle_ref.energy0[0],
+                                         phi_s=rfstation.phi_s[0] - rfstation.phi_rf[0, 0],      # Correct for RF phase
+                                         omega_rf=rfstation.omega_rf[0, 0])
+
+# --- Track matrix ---
+particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
+line.track(particles, num_turns=N_t, turn_by_turn_monitor=True, with_progress=True)
+mon = line.record_last_track
+
+# Saving turn-by-turn particle coordinates
+test_string = ''
+test_string += '{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\n'.format(
+    'dE', 'dt', 'momentum', 'gamma', 'beta', 'energy', 'x', 'px', 'y', 'py')
+test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
+                '\t{:+10.10e}\t{:+10.10e}\n').format(beam.dE[0], beam.dt[0], rfstation.momentum[0],
+                                                     rfstation.gamma[0], rfstation.beta[0], rfstation.energy[0],
+                                                     mon.x[0, 0].T, mon.px[0, 0].T, mon.y[0, 0].T, mon.py[0, 0].T)
+
+# Saving turn-by-turn Xsuite particle coordinates
+xsuite_string = ''
+xsuite_string += '{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\n'.format(
+    'ptau', 'zeta', 'momentum', 'gamma', 'beta')
+xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.format(
+    mon.ptau[0, 0].T, mon.zeta[0, 0].T, np.float64(mon.p0c[0, 0]), np.float64(mon.gamma0[0, 0]),
+    np.float64(mon.beta0[0, 0])
+)
+
+# Convert the xsuite particle coordinates back to BLonD
+for i in range(N_t):
+    dt, dE = xsuite_coords_to_blond_coords(mon.zeta[:, i].T, mon.ptau[:, i].T,
+                                           rfstation.beta[i],
+                                           rfstation.energy[i],
+                                           phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
+                                           omega_rf=rfstation.omega_rf[0, i])
+
+    # Statistics
+    test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
+                    '\t{:+10.10e}\t{:+10.10e}\n').format(dE[0], dt[0], rfstation.momentum[i], rfstation.gamma[i],
+                                                         rfstation.beta[i], rfstation.energy[i], mon.x[0, i].T,
+                                                         mon.px[0, i].T, mon.y[0, i].T, mon.py[0, i].T)
+
+    xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.format(
+        mon.ptau[0, i].T, mon.zeta[0, i].T, np.float64(mon.p0c[0, i]), np.float64(mon.gamma0[0, i]),
+        np.float64(mon.beta0[0, i])
+    )
+
+with open(results_directory + '/EX_04_output_data.txt', 'w') as f:
+    f.write(test_string)
+with open(results_directory + '/EX_04_output_data_xsuite.txt', 'w') as f:
+    f.write(xsuite_string)
+
+# Results --------------------------------------------------------------------------------------------------------------
+print(f"\nPlotting result...")
+
+# Plot Phasespace
+
+plt.figure()
+plt.plot(mon.zeta.T,mon.ptau.T)
+plt.scatter(np.mean(mon.zeta.T), np.mean(mon.ptau.T), label='mon')
+plt.grid()
+plt.title('Phasespace')
+plt.xlabel(r'$\zeta$')
+plt.ylabel(r'$p_{\tau}$')
+plt.savefig(results_directory + '/figure_Phasespace.png')
+
+# Use Xplt to plot Phasespace
+
+plot = xplt.PhaseSpacePlot(
+    mon
+)
+plot.fig.suptitle("Particle distribution for a single turn")
+plot.fig.savefig(results_directory + '/figure_ps_mon.png')
+
+# Show plots
+plt.show()
diff --git a/__EXAMPLES/main_files/EX_27_single_particle_ramp_psb.py b/__EXAMPLES/main_files/EX_27_single_particle_ramp_psb.py
new file mode 100644
index 00000000..18df366e
--- /dev/null
+++ b/__EXAMPLES/main_files/EX_27_single_particle_ramp_psb.py
@@ -0,0 +1,238 @@
+'''
+PSB simulation of a single particle with a ramp.
+
+:Author: **Thom Arnoldus van Rijswijk**
+'''
+
+# Imports
+import os
+import numpy as np
+from scipy.constants import c, e, m_p
+import matplotlib.pyplot as plt
+
+# Xsuite imports
+import xpart as xp
+import xtrack as xt
+import xplt
+
+# BLonD objects
+from blond.beam.beam import Beam, Proton
+from blond.input_parameters.ring import Ring
+from blond.input_parameters.rf_parameters import RFStation
+from blond.trackers.tracker import RingAndRFTracker
+
+# Interface
+from xsuite_blond_interface import (BlondElement, EnergyUpdate,
+                                    blond_beam_to_xsuite_coords,
+                                    xsuite_coords_to_blond_coords)
+
+# Monitor objects
+from blond.monitors.monitors import BunchMonitor
+from blond.plots.plot import Plot
+
+# Directory to save files
+this_directory = os.path.dirname(os.path.realpath(__file__)) + '/'
+results_directory = this_directory + '../output_files/EX_05'
+os.makedirs(results_directory, exist_ok=True)
+
+# Parameters ----------------------------------------------------------------------------------------------------------
+# Accelerator parameters
+C = 2 * np.pi * 25.000057945260654              # Machine circumference, radius 25 [m]
+gamma_t = 4.11635447373496                      # Transition gamma [-]
+alpha = 1./gamma_t/gamma_t                      # First order mom. comp. factor [-]
+h = 1                                           # Harmonic number [-]
+V = 8e3                                         # RF voltage [V]
+dphi = np.pi                                    # Phase modulation/offset [rad]
+
+# Beam parameters
+N_m = 1                                         # Number of macroparticles [-]
+N_p = 1e11                                      # Intensity / number of particles
+sigma_dt = 180e-9 / 4                           # [s]        
+blen = sigma_dt * 4                             # Initial bunch length, 4 sigma [s]
+kin_beam_energy = 1.4e9                         # Kinetic energy [eV]
+
+
+# Simulation parameters
+N_t = 6000                                      # Number of (tracked) turns [-]
+N_buckets = 1                                   # Number of buckets [-]
+dt_plt = 500                                    # Timestep between plots [-]
+input_dt = 5.720357923415153e-07 - sigma_dt     # Input particles dt [s]
+input_dE = 0.0                                  # Input particles dE [eV]
+
+# Derived parameters
+E_0 = m_p * c**2 / e                                  # [eV]
+tot_beam_energy = E_0 + kin_beam_energy               # [eV]
+sync_momentum = np.sqrt(tot_beam_energy**2 - E_0**2)  # [eV / c]
+momentum_increase = 0.001e9                           # [eV / c]
+
+# Preparing BLonD items -----------------------------------------------------------------------------------------------
+print(f"\nPreparing BLonD items...")
+# --- BLonD objects ---
+ring = Ring(C, alpha, np.linspace(sync_momentum, sync_momentum + momentum_increase, N_t + 1), Proton(), n_turns=N_t)
+rfstation = RFStation(ring, [h], [V], [dphi]) 
+beam = Beam(ring, N_m, N_p)
+
+# --- Insert particle ---
+beam.dt = np.array([input_dt])
+beam.dE = np.array([input_dE])
+
+# --- RF tracker object ---
+blond_tracker = RingAndRFTracker(rfstation, beam, Profile=None, interpolation=False,
+                                 CavityFeedback=None, BeamFeedback=None,
+                                 TotalInducedVoltage=None, 
+                                 with_xsuite=True)
+
+# --- Bunch monitor ---
+# Print parameters for plotting
+print(f"\n Calculating plotting parameters...")
+
+t_u = np.pi / rfstation.omega_rf[0, 0]
+Y_phi_s = np.sqrt(abs(-np.cos(rfstation.phi_s[0]) + (np.pi - 2 * rfstation.phi_s[0]) / 2 * np.sin(rfstation.phi_s[0])))
+dE_sep = np.sqrt(2 * abs(rfstation.charge) * rfstation.voltage[0, 0] * rfstation.beta[0] * rfstation.beta[0]
+                 * rfstation.energy[0] / (np.pi * rfstation.harmonic[0, 0] * abs(ring.eta_0[0, 0]))) * Y_phi_s
+
+print('t_u: ' + str(2 * t_u))
+print('dE_sep,m: ' + str(dE_sep))
+
+# Make bunchmonitor
+bunchmonitor = BunchMonitor(ring, rfstation, beam,
+                            results_directory + '/EX_05_output_data', Profile=None)
+
+format_options = {'dirname': results_directory + '/EX_05_fig'}
+plots = Plot(ring, rfstation, beam, dt_plt, N_t, 0, 2 * t_u,
+             -1.05 * dE_sep, 1.05 * dE_sep, xunit='s', separatrix_plot=True,
+             Profile=None, h5file=results_directory + '/EX_05_output_data',
+             format_options=format_options)
+
+# --- Creating interface elements ---
+# Blond tracker
+cavity = BlondElement(blond_tracker, beam)
+# BLonD monitor
+beam_monitor = BlondElement(bunchmonitor, beam)
+beam_plots = BlondElement(plots, beam)
+
+# Preparing Xsuite items -----------------------------------------------------------------------------------------------
+print(f"\nPreparing Xsuite items...")
+# --- Setup matrix ---
+# Make First order matrix map (takes care of drift in Xsuite)
+matrix = xt.LineSegmentMap(
+    longitudinal_mode='nonlinear',
+    qx=1.1, qy=1.2,
+    betx=1., 
+    bety=1., 
+    voltage_rf=0,
+    frequency_rf=0,
+    lag_rf=0,
+    momentum_compaction_factor=alpha,
+    length=C)
+
+# Create line
+line = xt.Line(elements=[matrix], element_names={'matrix'})
+line['matrix'].length = C
+
+# Insert the BLonD elements
+line.insert_element(index=0, element=cavity, name='blond_cavity')
+line.append_element(element=beam_monitor, name='blond_monitor')
+line.append_element(element=beam_plots, name='blond_plots')
+
+# Insert energy ramp
+energy_update = EnergyUpdate(ring.momentum[0, :])
+line.insert_element(index=0, element=energy_update, name='energy_update')
+
+# Add particles to line and build tracker 
+line.particle_ref = xp.Particles(p0c=sync_momentum, mass0=xp.PROTON_MASS_EV, q0=1.)
+line.build_tracker()
+
+# Show table
+line.get_table().show()
+# prints:
+
+# name               s element_type   isthick isreplica parent_name iscollective
+# energy_update      0 EnergyUpdate     False     False None                True
+# blond_cavity       0 BlondElement     False     False None                True
+# matrix             0 LineSegmentMap    True     False None               False
+# blond_monitor 157.08 BlondObserver    False     False None                True
+# blond_plots   157.08 BlondElement     False     False None                True
+# _end_point    157.08                  False     False None               False
+
+# Simulating ----------------------------------------------------------------------------------------------------------
+print(f"\nSetting up simulation...")
+
+# --- Convert the initial BLonD distribution to xsuite coordinates ---
+zeta, ptau = blond_beam_to_xsuite_coords(beam, 
+                                         line.particle_ref.beta0[0],
+                                         line.particle_ref.energy0[0],
+                                         phi_s=rfstation.phi_s[0] - rfstation.phi_rf[0, 0],  # Correct for RF phase
+                                         omega_rf=rfstation.omega_rf[0, 0])
+
+# --- Track matrix ---
+particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
+line.track(particles, num_turns=N_t, turn_by_turn_monitor=True, with_progress=True)
+mon = line.record_last_track
+
+# Saving turn-by-turn particle coordinates
+test_string = ''
+test_string += '{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\n'.format(
+    'dE', 'dt', 'momentum', 'gamma', 'beta', 'energy', 'x', 'px', 'y', 'py')
+test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
+                '\t{:+10.10e}\t{:+10.10e}\n').format(beam.dE[0], beam.dt[0], rfstation.momentum[0],
+                                                     rfstation.gamma[0], rfstation.beta[0], rfstation.energy[0],
+                                                     mon.x[0, 0].T, mon.px[0, 0].T, mon.y[0, 0].T, mon.py[0, 0].T)
+
+# Saving turn-by-turn Xsuite particle coordinates
+xsuite_string = ''
+xsuite_string += '{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\n'.format(
+    'ptau', 'zeta', 'momentum', 'gamma', 'beta')
+xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.format(
+    mon.ptau[0, 0].T, mon.zeta[0, 0].T, np.float64(mon.p0c[0, 0]), np.float64(mon.gamma0[0, 0]),
+    np.float64(mon.beta0[0, 0])
+)
+
+# Convert the xsuite particle coordinates back to BLonD
+for i in range(N_t):
+    dt, dE = xsuite_coords_to_blond_coords(mon.zeta[:, i].T, mon.ptau[:, i].T,
+                                           rfstation.beta[i],
+                                           rfstation.energy[i],
+                                           phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
+                                           omega_rf=rfstation.omega_rf[0, i])
+
+    # Statistics
+    test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
+                    '\t{:+10.10e}\t{:+10.10e}\n').format(dE[0], dt[0], rfstation.momentum[i], rfstation.gamma[i],
+                                                         rfstation.beta[i], rfstation.energy[i], mon.x[0, i].T,
+                                                         mon.px[0, i].T, mon.y[0, i].T, mon.py[0, i].T)
+
+    xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.format(
+        mon.ptau[0, i].T, mon.zeta[0, i].T, np.float64(mon.p0c[0, i]), np.float64(mon.gamma0[0, i]),
+        np.float64(mon.beta0[0, i])
+    )
+
+with open(results_directory + '/EX_05_output_data.txt', 'w') as f:
+    f.write(test_string)
+with open(results_directory + '/EX_05_output_data_xsuite.txt', 'w') as f:
+    f.write(xsuite_string)
+
+# Results --------------------------------------------------------------------------------------------------------------
+print(f"\nPlotting result...")
+
+# Plot Phasespace
+
+plt.figure()
+plt.plot(mon.zeta.T,mon.ptau.T)
+plt.scatter(np.mean(mon.zeta.T), np.mean(mon.ptau.T), label='mon')
+plt.grid()
+plt.title('Phasespace')
+plt.xlabel(r'$\zeta$')
+plt.ylabel(r'$p_{\tau}$')
+plt.savefig(results_directory + '/figure_Phasespace.png')
+
+# Use Xplt to plot Phasespace
+
+plot = xplt.PhaseSpacePlot(
+    mon
+)
+plot.fig.suptitle("Particle distribution for a single turn")
+plot.fig.savefig(results_directory + '/figure_ps_mon.png')
+
+# Show plots
+plt.show()
diff --git a/__EXAMPLES/main_files/EX_28_phase_loop_lhc.py b/__EXAMPLES/main_files/EX_28_phase_loop_lhc.py
new file mode 100644
index 00000000..b605c4d7
--- /dev/null
+++ b/__EXAMPLES/main_files/EX_28_phase_loop_lhc.py
@@ -0,0 +1,246 @@
+'''
+LHC simulation using the LHC phase loop.
+
+:Author: **Birk Emil Karlsen-Baeck**, **Thom Arnoldus van Rijswijk**
+'''
+
+# Imports
+import os
+import numpy as np
+import matplotlib.pyplot as plt
+
+# Xsuite imports
+import xpart as xp
+import xtrack as xt
+import xplt
+
+# BLonD objects
+from blond.beam.beam import Beam, Proton
+from blond.beam.profile import Profile, CutOptions, FitOptions
+from blond.beam.distributions import parabolic
+from blond.input_parameters.ring import Ring
+from blond.input_parameters.rf_parameters import RFStation
+from blond.trackers.tracker import RingAndRFTracker
+from blond.llrf.beam_feedback import BeamFeedback
+
+# Interface
+from xsuite_blond_interface import (BlondElement, BlondObserver,
+                                    blond_beam_to_xsuite_coords,
+                                    xsuite_coords_to_blond_coords)
+
+# Monitor objects
+from blond.monitors.monitors import BunchMonitor
+from blond.plots.plot import Plot
+
+# Directory to save files
+this_directory = os.path.dirname(os.path.realpath(__file__)) + '/'
+results_directory = this_directory + '../output_files/EX_06'
+os.makedirs(results_directory, exist_ok=True)
+
+# Parameters ----------------------------------------------------------------------------------------------------------
+# Accelerator parameters
+C = 26658.8832                                  # Machine circumference [m]
+p_s = 450e9                                     # Synchronous momentum [eV/c]
+p_f = 450.0e9                                   # Synchronous momentum, final
+h = 35640                                       # Harmonic number [-]
+alpha = 0.00034849575112251314                  # First order mom. comp. factor [-]
+V = 5e6                                         # RF voltage [V]
+dphi = 0                                        # Phase modulation/offset [rad]
+
+# Bunch parameters
+N_bunches = 1                                   # Number of bunches [-]
+N_m = int(1e3)                                  # Number of macroparticles [-]
+N_p = 1.15e11                                   # Intensity 
+blen = 1.25e-9                                  # Bunch length [s]
+energy_err = 100e6                              # Beamenergy error [eV/c]
+
+# Simulation parameters
+N_t = 2 * 481                                   # Number of (tracked) turns [-]
+N_buckets = 2                                   # Number of buckets [-]
+dt_plt = 50                                     # Timestep between plots [-]
+
+# Preparing BLonD items -----------------------------------------------------------------------------------------------
+print(f"\nPreparing BLonD items...")
+# --- BLonD objects ---
+ring = Ring(C, alpha, np.linspace(p_s, p_f, N_t + 1), Proton(), n_turns=N_t)
+rfstation = RFStation(ring, [h], [V], [dphi]) 
+beam = Beam(ring, N_m, N_p)
+
+# --- Insert particles ---
+parabolic(ring, rfstation, beam, bunch_length=blen)
+beam.dE += energy_err
+
+# Create Profile
+profile = Profile(beam,
+                  CutOptions(cut_left=-1.5 * rfstation.t_rf[0, 0],
+                             cut_right=(N_buckets + 1.5) * rfstation.t_rf[0, 0],
+                             n_slices=(N_buckets + 3) * 2**5),
+                  FitOptions(fit_option='gaussian'))
+profile.track()
+
+# --- LHC beam-phase loop and synchronization loop ---
+PL_gain = 1 / (5 * ring.t_rev[0])
+SL_gain = PL_gain / 10
+
+bl_config = {'machine': 'LHC',
+             'PL_gain': PL_gain,
+             'SL_gain': SL_gain}
+BL = BeamFeedback(ring, rfstation, profile, bl_config)
+
+# --- RF tracker object ---
+blond_tracker = RingAndRFTracker(rfstation, beam, Profile=profile, interpolation=False,
+                                 CavityFeedback=None, BeamFeedback=BL,
+                                 TotalInducedVoltage=None, 
+                                 with_xsuite=True)
+
+# --- Bunch monitor ---
+# Print parameters for plotting
+print(f"\n Calculating plotting parameters...")
+
+t_u = np.pi / rfstation.omega_rf[0, 0]
+Y_phi_s = np.sqrt(abs(-np.cos(rfstation.phi_s[0]) + (np.pi - 2 * rfstation.phi_s[0]) / 2 * np.sin(rfstation.phi_s[0])))
+dE_sep = np.sqrt(2 * abs(rfstation.charge) * rfstation.voltage[0, 0] * rfstation.beta[0] * rfstation.beta[0]
+                 * rfstation.energy[0] / (np.pi * rfstation.harmonic[0, 0] * abs(ring.eta_0[0, 0]))) * Y_phi_s
+
+print('t_u: ' + str(2 * t_u))
+print('dE_sep,m: ' + str(dE_sep))
+
+# Make bunchmonitor
+bunchmonitor = BunchMonitor(ring, rfstation, beam,
+                            results_directory + '/EX_06_output_data', Profile=profile, PhaseLoop=BL)
+
+format_options = {'dirname': results_directory + '/EX_06_fig'}
+plots = Plot(ring, rfstation, beam, dt_plt, N_t, 0, 2*t_u,
+             -1.05 * dE_sep, 1.05 * dE_sep, xunit='s', separatrix_plot=True,
+             Profile=profile, PhaseLoop=BL, h5file=results_directory + '/EX_06_output_data',
+             format_options=format_options)
+
+# --- Creating interface elements ---
+# Blond tracker
+cavity = BlondElement(blond_tracker, beam)
+# BLonD monitor
+beam_monitor = BlondElement(bunchmonitor, beam)
+beam_profile = BlondElement(profile, beam)
+beam_plots = BlondElement(plots, beam)
+
+# Preparing Xsuite items -----------------------------------------------------------------------------------------------
+print(f"\nPreparing Xsuite items...")
+# --- Setup matrix ---
+# Make First order matrix map (takes care of drift in Xsuite)
+matrix = xt.LineSegmentMap(
+    longitudinal_mode='nonlinear',
+    qx=1.1, qy=1.2,
+    betx=1., 
+    bety=1., 
+    voltage_rf=0,
+    frequency_rf=0,
+    lag_rf=0,
+    momentum_compaction_factor=alpha,
+    length=C)
+
+# Create line
+line = xt.Line(elements=[matrix], element_names={'matrix'})
+line['matrix'].length = C
+
+# Insert the BLonD elements
+line.insert_element(index=0, element=cavity, name='blond_cavity')
+line.append_element(element=beam_monitor, name='blond_monitor')
+line.append_element(element=beam_profile, name='blond_profile')
+line.append_element(element=beam_plots, name='blond_plots')
+
+# Add particles to line and build tracker 
+line.particle_ref = xp.Particles(p0c=p_s, mass0=xp.PROTON_MASS_EV, q0=1.)
+line.build_tracker()
+
+# Show table
+line.get_table().show()
+# prints:
+
+# name                s element_type   isthick isreplica parent_name iscollective
+# blond_cavity        0 BlondElement     False     False None                True
+# matrix              0 LineSegmentMap    True     False None               False
+# blond_monitor 26658.9 BlondObserver    False     False None                True
+# blond_plots   26658.9 BlondElement     False     False None                True
+# _end_point    26658.9                  False     False None               False
+
+# Simulating ----------------------------------------------------------------------------------------------------------
+print(f"\nSetting up simulation...")
+
+# --- Convert the initial BLonD distribution to xsuite coordinates ---
+zeta, ptau = blond_beam_to_xsuite_coords(beam, 
+                                         line.particle_ref.beta0[0],
+                                         line.particle_ref.energy0[0],
+                                         phi_s=rfstation.phi_s[0], 
+                                         omega_rf=rfstation.omega_rf[0, 0])
+
+# --- Track matrix ---
+particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
+line.track(particles, num_turns=N_t, turn_by_turn_monitor=True, with_progress=True)
+mon = line.record_last_track
+
+# Saving turn-by-turn particle coordinates
+test_string = ''
+test_string += '{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\n'.format(
+    'dE', 'dt', 'momentum', 'gamma', 'beta', 'energy', 'x', 'px', 'y', 'py')
+test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
+                '\t{:+10.10e}\t{:+10.10e}\n').format(beam.dE[0], beam.dt[0], rfstation.momentum[0],
+                                                     rfstation.gamma[0], rfstation.beta[0], rfstation.energy[0],
+                                                     mon.x[0, 0].T, mon.px[0, 0].T, mon.y[0, 0].T, mon.py[0, 0].T)
+
+# Saving turn-by-turn Xsuite particle coordinates
+xsuite_string = ''
+xsuite_string += '{:<17}\t{:<17}\t{:<17}\t{:<17}\t{:<17}\n'.format(
+    'ptau', 'zeta', 'momentum', 'gamma', 'beta')
+xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.format(
+    mon.ptau[0, 0].T, mon.zeta[0, 0].T, np.float64(mon.p0c[0, 0]), np.float64(mon.gamma0[0, 0]),
+    np.float64(mon.beta0[0, 0])
+)
+
+# Convert the xsuite particle coordinates back to BLonD
+for i in range(N_t):
+    dt, dE = xsuite_coords_to_blond_coords(mon.zeta[:, i].T, mon.ptau[:, i].T,
+                                           rfstation.beta[i],
+                                           rfstation.energy[i],
+                                           phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
+                                           omega_rf=rfstation.omega_rf[0, i])
+
+    # Statistics
+    test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
+                    '\t{:+10.10e}\t{:+10.10e}\n').format(dE[0], dt[0], rfstation.momentum[i], rfstation.gamma[i],
+                                                         rfstation.beta[i], rfstation.energy[i], mon.x[0, i].T,
+                                                         mon.px[0, i].T, mon.y[0, i].T, mon.py[0, i].T)
+
+    xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.format(
+        mon.ptau[0, i].T, mon.zeta[0, i].T, np.float64(mon.p0c[0, i]), np.float64(mon.gamma0[0, i]),
+        np.float64(mon.beta0[0, i])
+    )
+
+with open(results_directory + '/EX_06_output_data.txt', 'w') as f:
+    f.write(test_string)
+with open(results_directory + '/EX_06_output_data_xsuite.txt', 'w') as f:
+    f.write(xsuite_string)
+
+# Results --------------------------------------------------------------------------------------------------------------
+print(f"\nPlotting result...")
+
+# Plot Phasespace
+
+plt.figure()
+plt.plot(mon.zeta.T,mon.ptau.T)
+plt.scatter(np.mean(mon.zeta.T), np.mean(mon.ptau.T), label='mon')
+plt.grid()
+plt.title('Phasespace')
+plt.xlabel(r'$\zeta$')
+plt.ylabel(r'$p_{\tau}$')
+plt.savefig(results_directory + '/figure_Phasespace.png')
+
+# Use Xplt to plot Phasespace
+
+plot = xplt.PhaseSpacePlot(
+    mon
+)
+plot.fig.suptitle("Particle distribution for a single turn")
+plot.fig.savefig(results_directory + '/figure_ps_mon.png')
+
+# Show plots
+plt.show()
diff --git a/blond/interfaces/__init__.py b/blond/interfaces/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/blond/interfaces/xsuite.py b/blond/interfaces/xsuite.py
new file mode 100644
index 00000000..2a4260e1
--- /dev/null
+++ b/blond/interfaces/xsuite.py
@@ -0,0 +1,401 @@
+'''
+Functions and classes to interface BLonD with xsuite.
+
+:Authors: **Birk Emil Karlsen-Baeck**, **Thom Arnoldus van Rijswijk**, **Helga Timko**
+'''
+
+from __future__ import annotations
+from typing import TYPE_CHECKING
+import numpy as np
+from scipy.constants import c as clight
+
+from xtrack import ReferenceEnergyIncrease, ZetaShift
+from blond.trackers.tracker import RingAndRFTracker
+from blond.impedances.impedance import TotalInducedVoltage
+
+if TYPE_CHECKING:
+    from xtrack import Particles
+    from xtrack import Line
+    from blond.beam.beam import Beam
+    from blond.beam.profile import Profile
+    from typing import Any, Union
+
+
+def blond_beam_to_xsuite_coords(beam: Beam, beta0: float, energy0: float,
+                                omega_rf: float, phi_s: float = 0):
+    r'''
+    Coordinate transformation from Xsuite to BLonD at a given turn or multiple turns if numpy arrays ar given.
+
+    :param beam: blond.beam.beam.Beam class.
+        BLonD beam class to extract the initial coordinates dE and dt.
+    :param beta0: float.
+        Synchronous beta.
+    :param energy0: float.
+        Synchronous energy.
+    :param omega_rf: float.
+        The rf angular frequency.
+    :param phi_s: float.
+        Synchronous phase in radians equivalent to Xsuite's phi_rf (below transition energy input should be phi_s-phi_rf).
+        The default value is 0.
+
+    :return zeta, ptau: as numpy-arrays (or single variable)
+    '''
+    ptau = beam.dE / (beta0 * energy0)
+    zeta = -(beam.dt - phi_s / omega_rf) * beta0 * clight
+    return zeta, ptau
+
+
+def xsuite_coords_to_blond_coords(zeta: Union[float, np.ndarray], ptau: Union[float, np.ndarray],
+                                  beta0: float, energy0: float, omega_rf: float, phi_s: float = 0):
+    r'''
+    Coordinate transformation from Xsuite to BLonD.
+
+    :param zeta: float or numpy-array.
+        The zeta coordinate as defined in Xsuite.
+    :param ptau: float or numpy-array.
+        The ptau coordinate as defined in Xsuite.
+    :param beta0: float.
+        The synchronous beta.
+    :param energy0: float.
+        The synchronous energy in eV.
+    :param omega_rf: float.
+        The rf angular frequency.
+    :param phi_s: float.
+        The synchronous phase in radians equivalent to Xsuite's phi_rf
+        (below transition energy input should be phi_s-phi_rf)
+
+    :return dt, dE: as numpy-arrays (or single variable)
+    '''
+
+    dE = ptau * beta0 * energy0
+    dt = -zeta / (beta0 * clight) + phi_s / omega_rf
+    return dt, dE
+
+
+class BlondElement:
+
+    def __init__(self, trackable: Any, beam: Beam, update_zeta: bool = False) -> None:
+        r"""
+        The BlondElement class contains a trackable object from the BLonD simulation suite and the Beam object from
+        BLonD. The class is intended to be an element to be added to the Line in XTrack instead of the default
+        RF cavities in XTrack.
+
+        The behavior of the tracking of the class depends on what trackable object is passed.
+        If the RingAndRFTracker class is passed then a coordinate transformation will be performed and the energy
+        deviation of the particles will be updated. If the TotalInducedVoltage class is passed then the
+        TotalInducedVoltage.induced_voltage_sum() method will be called. Lastly, if any other class is passed then
+        the tracking of BlondElement will track this class.
+
+        trackable : BLonD trackable object
+            BLonD object to be tracked, e.g. the RingAndRFTracker.
+        beam : blond.beam.beam.Beam class
+            BLonD Beam class used for tracking.
+        """
+
+        self.trackable = trackable
+        self.beam = beam
+        self.update_zeta = update_zeta
+
+        # Initialize time- and orbit-shift to BLonD coordinates
+        self._dt_shift = None
+        self.orbit_shift = ZetaShift(dzeta=0)
+
+        # Check what BLonD trackable has been passed and track the object accordingly
+        if isinstance(self.trackable, RingAndRFTracker):
+            self.track = getattr(self, "rf_track")
+        elif isinstance(self.trackable, TotalInducedVoltage):
+            self.track = getattr(self, "ind_track")
+        else:
+            self.track = getattr(self, "obs_track")
+
+    def rf_track(self, particles: Particles) -> None:
+        r"""
+        Tracking method which is called if the trackable BLonD class is the RingAndRFTracker.
+
+        particles : xtrack.Particles
+            Particles class from xtrack
+        """
+
+        # Compute the shift to BLonD coordinates
+        self._get_time_shift()
+
+        # Convert the Xsuite coordinates to BLonD coordinates
+        self.xsuite_part_to_blond_beam(particles)
+
+        # Track with only the energy kick
+        self.trackable.track()
+
+        # Convert the BLonD energy coordinate to the equivalent Xsuite coordinate
+        self.blond_beam_to_xsuite_part(particles, self.update_zeta)
+
+        # Update the zeta shift due to potential frequency shifts in the RF
+        self._orbit_shift(particles)
+
+    def ind_track(self, particles: Particles) -> None:
+        r"""
+        Tracking method which is called if the trackable BLonD class is the TotalInducedVoltage.
+
+        particles : xtrack.Particles
+            Particles class from xtrack
+        """
+        self.trackable.induced_voltage_sum()
+
+    def obs_track(self, particles: Particles) -> None:
+        r"""
+        Tracking method which is called if the trackable BLonD class is not the RingAndRFTracker or TotalInducedVoltage.
+
+        particles : xtrack.Particles
+            Particles class from xtrack
+        """
+        self.trackable.track()
+
+    def xsuite_part_to_blond_beam(self, particles: Particles) -> None:
+        r"""
+        Coordinate transformation from Xsuite to BLonD.
+        It uses the initial particle coordinates and beam properties in stored in the Particles class from xtrack.
+
+        particles : xtrack.Particles
+            Particles class from xtrack
+        """
+        # Convert Xsuite momentum to BLonD energy deviation
+        self.beam.dE[:] = particles.beta0 * particles.energy0 * particles.ptau
+
+        # Convert Xsuite zeta coordinate to BLonD time deviation
+        self.beam.dt[:] = -particles.zeta / particles.beta0 / clight + self._dt_shift
+
+        # Check what particles are still alive
+        self.beam.id[:] = np.int_(particles.state > 0)
+
+    def blond_beam_to_xsuite_part(self, particles: Particles, update_zeta: bool = False) -> None:
+        r"""
+        Coordinate transformation from BLonD to Xsuite.
+        It uses the particle coordinates stored in Beam class of BLonD
+        It uses the beam properties in stored in the Particles class from xtrack
+
+        particles : xtrack.Particles
+            Particles class from xtrack
+        """
+        # Subtract the given acceleration kick in BLonD, in Xsuite this is dealt with differently
+        if isinstance(self.trackable, RingAndRFTracker):
+            self.beam.dE = self.beam.dE - self.trackable.acceleration_kick[self.trackable.counter[0] - 1]
+
+        # Convert BLonD energy deviation to Xsuite momentum
+        particles.ptau = self.beam.dE / (particles.beta0 * particles.energy0)
+
+        # Convert BLonD time deviation to Xsuite zeta.
+        # This step is not needed usually because the BLonD simulation only does the kick, so dt is not changed.
+        if update_zeta:
+            particles.zeta = - (self.beam.dt - self._dt_shift) * particles.beta0 * clight
+
+        # Check what particles are still alive after the BLonD track
+        mask_lost = (self.beam.id <= 0) & particles.state > 0
+
+        # If the particle is lost its state is set to -500 by convention
+        particles.state[mask_lost] = -500
+
+    def _get_time_shift(self) -> None:
+        r'''
+        Computes the time-shift between the Xsuite and BLonD coordinate systems.
+        '''
+        # Get turn counter from the RingAndRFTracker
+        counter = self.trackable.rf_params.counter[0]
+
+        # Compute the time-shift based on the synchronous phase
+        self._dt_shift = ((self.trackable.rf_params.phi_s[counter] - self.trackable.rf_params.phi_rf[0, counter])
+                          / self.trackable.rf_params.omega_rf[0, counter])
+
+    def _orbit_shift(self, particles) -> None:
+        r'''
+        Computes the radial steering due to rf periods which are not an integer multiple of the revolution period.
+        This is for example needed when tracking with global LLRF feedback loops.
+
+        particles : xtrack.Particles
+            Particles class from xtrack
+        '''
+        # Get turn counter from the RingAndRFTracker
+        counter = self.trackable.counter[0]
+
+        # Compute the orbit shift due to the difference in rf frequency
+        dzeta = self.trackable.rf_params.ring_circumference
+        omega_rf = self.trackable.rf_params.omega_rf[:, counter]
+        omega_rf_design = (2 * np.pi * self.trackable.rf_params.harmonic[:, counter]
+                           / self.trackable.rf_params.t_rev[counter])
+        domega = omega_rf - omega_rf_design
+
+        dzeta *= domega / omega_rf_design
+
+        # Apply the shift
+        self.orbit_shift.dzeta = dzeta
+
+        # Track the shift
+        self.orbit_shift.track(particles)
+
+
+class EnergyUpdate:
+
+    def __init__(self, momentum: Union[list, np.ndarray]) -> None:
+        r"""
+        Class to update the synchronous energy from the momentum program in BLonD.
+
+        :param momentum: list or numpy-array.
+            Momentum program from BLonD in units of eV
+        """
+
+        # Load momentum program
+        self.momentum = momentum
+
+        # Find initial momentum update
+        init_p0c = self.momentum[1] - self.momentum[0]
+
+        # Enter the initial momentum update in the ReferenceEnergyIncrease class in xsuite
+        self.xsuite_energy_update = ReferenceEnergyIncrease(Delta_p0c=init_p0c)
+
+    def track(self, particles: Particles) -> None:
+        r'''
+        Track method for the class to update the synchronous energy.
+
+        particles : xtrack.Particles
+            Particles class from xtrack
+        '''
+        # Check for particles which are still alive
+        mask_alive = particles.state > 0
+
+        # Use the still alive particles to find the current turn momentum
+        p0c_before = particles.p0c[mask_alive]
+
+        # Find the momentum for the next turn
+        p0c_after = self.momentum[particles.at_turn[mask_alive][0]]
+
+        # Update the energy increment
+        self.xsuite_energy_update.Delta_p0c = p0c_after - p0c_before[0]
+
+        # Apply the energy increment to the particles
+        self.xsuite_energy_update.track(particles)
+
+
+class EnergyFrequencyUpdate:
+
+    def __init__(self, momentum: np.ndarray, f_rf: np.ndarray, line: Line, cavity_name: str) -> None:
+        r"""
+        Class to update energy of Particles class turn-by-turn with the ReferenceEnergyIncrease function
+        from xtrack. Additionally it updates the frequency of the xtrack cavity in the line.
+        Intended to be used without BLonD-Xsuite interface.
+
+        :param momentum: numpy-array
+            The momentum program from BLonD in eV
+        :param f_rf: numpy-array
+            The frequency program from BLonD in Hz.
+        particles : xtrack.Line
+            Line class from xtrack
+        :param cavity_name: string
+            Name of cavity to update frequency.
+        """
+
+        # Load the parameters
+        self.momentum = momentum
+        self.f_rf = f_rf
+        self.line = line
+        self.cavity_name = cavity_name
+
+        # Find initial momentum update
+        init_p0c = self.momentum[1] - self.momentum[0]
+
+        # Enter the initial momentum update in the ReferenceEnergyIncrease class in xsuite
+        self.xsuite_energy_update = ReferenceEnergyIncrease(Delta_p0c=init_p0c)
+
+    def track(self, particles: Particles) -> None:
+        r'''
+        Track-method from for the class. This method updates the synchronous momentum and the rf frequency.
+
+        particles : xtrack.Particles
+            Particles class from xtrack
+        '''
+        # Check for particles which are still alive
+        mask_alive = particles.state > 0
+
+        # Use the still alive particles to find the current turn momentum
+        p0c_before = particles.p0c[mask_alive]
+
+        # Find the momentum for the next turn
+        p0c_after = self.momentum[particles.at_turn[mask_alive][0]]
+
+        # Update the energy increment
+        self.xsuite_energy_update.Delta_p0c = p0c_after - p0c_before[0]
+
+        # Apply the energy increment to the particles
+        self.xsuite_energy_update.track(particles)
+
+        # Update the rf frequency
+        self.line[self.cavity_name].frequency = self.f_rf[particles.at_turn[mask_alive][0]]
+
+
+class BlondObserver(BlondElement):
+
+    def __init__(self, trackable: Any, beam: Beam, blond_cavity: bool, update_zeta: bool = False,
+                 profile: Profile = None) -> None:
+        r'''
+        Child-class of the BlondElement, except that it updates the coordinates
+        in BLonD when an observing element is used such as BunchMonitor.
+
+        :param trackable: BLonD trackable object
+            BLonD object to be tracked, e.g. the RingAndRFTracker.
+        :param beam: blond.beam.beam.Beam class
+            BLonD Beam class used for tracking.
+        :param blond_cavity: bool.
+            If there is no BlondCavity (bool = False), it updates its own turn-counter.
+        :param update_zeta: bool.
+            Boolean that decides whether zeta is converter back to dt after tracking object or not.
+            Usually not necessary so default is False.
+        :param profile: blond.beam.profile.Profile class
+            BLonD Profile class used for tracking.
+        '''
+        # Initialize the parent class
+        super().__init__(trackable, beam, update_zeta)
+
+        # Load the parameters
+        self.blond_cavity = blond_cavity
+        # For bunch monitoring we need the profile separately added
+        self.profile = profile
+
+        # Initializing arrays for storing some turn-by-turn properties of xtrack.Particles class
+        self.xsuite_ref_energy = np.zeros(self.trackable.rf_params.n_turns + 1)
+        self.xsuite_trev = np.zeros(self.trackable.rf_params.n_turns + 1)
+
+    def obs_track(self, particles: Particles) -> None:
+        r'''
+        observation tracker which performs the coordinate transformations.
+
+        particles : xtrack.Particles
+            Particles class from xtrack
+        '''
+        # Compute the shift to BLonD coordinates
+        self._get_time_shift()
+
+        # Convert the Xsuite coordinates to BLonD coordinates
+        self.xsuite_part_to_blond_beam(particles)
+
+        # Track profile if given
+        if self.profile is not None:
+            self.profile.track()
+
+        # Track
+        self.trackable.track()
+
+        if not self.blond_cavity:
+            # Updating the beam synchronous momentum etc.
+            turn = self.trackable.rf_params.counter[0]
+
+            self.beam.beta = self.trackable.rf_params.beta[turn + 1]
+            self.beam.gamma = self.trackable.rf_params.gamma[turn + 1]
+            self.beam.energy = self.trackable.rf_params.energy[turn + 1]
+            self.beam.momentum = self.trackable.rf_params.momentum[turn + 1]
+
+            # Update custom counter
+            self.trackable.rf_params.counter[0] += 1
+
+        # Convert the BLonD energy coordinate to the equivalent Xsuite coordinate
+        self.blond_beam_to_xsuite_part(particles, self.update_zeta)
+
+        # Track properties of xtrack.Particles
+        self.xsuite_ref_energy[self.trackable.rf_params.counter[0] - 1] = particles.energy0[0]
+        self.xsuite_trev[self.trackable.rf_params.counter[0] - 1] = particles.t_sim  # Does not update per turn!
-- 
GitLab


From 6baa7a0e9e6f8b17dfa2f6e8390c103b1fb7a948 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Birk=20Karlsen-B=C3=A6ck?= <birk.karlsen.baeck@gmail.com>
Date: Fri, 1 Nov 2024 09:08:03 +0100
Subject: [PATCH 2/9] [examples][main files] Minor clean up of xsuite-blond
 examples

---
 ..._single_particle_with_oneturnmatrix_lhc.py | 19 +++++++++----------
 .../EX_24_single_particle_ramp_lhc.py         | 18 +++++++++---------
 ...X_25_single_particle_two_rfstations_lhc.py | 18 +++++++++---------
 ...EX_26_single_particle_oneturnmatrix_psb.py | 18 +++++++++---------
 .../EX_27_single_particle_ramp_psb.py         | 18 +++++++++---------
 __EXAMPLES/main_files/EX_28_phase_loop_lhc.py | 18 +++++++++---------
 6 files changed, 54 insertions(+), 55 deletions(-)

diff --git a/__EXAMPLES/main_files/EX_23_single_particle_with_oneturnmatrix_lhc.py b/__EXAMPLES/main_files/EX_23_single_particle_with_oneturnmatrix_lhc.py
index 988d40c9..9052aab0 100644
--- a/__EXAMPLES/main_files/EX_23_single_particle_with_oneturnmatrix_lhc.py
+++ b/__EXAMPLES/main_files/EX_23_single_particle_with_oneturnmatrix_lhc.py
@@ -9,7 +9,6 @@ import os
 import numpy as np
 import matplotlib as mpl
 import matplotlib.pyplot as plt
-#mpl.use('Agg')
 
 # Xsuite imports
 import xpart as xp
@@ -23,9 +22,9 @@ from blond.input_parameters.rf_parameters import RFStation
 from blond.trackers.tracker import RingAndRFTracker
 
 # Interface
-from xsuite_blond_interface import (BlondElement, BlondObserver,
-                                    blond_beam_to_xsuite_coords,
-                                    xsuite_coords_to_blond_coords)
+from blond.interfaces.xsuite import (BlondElement, BlondObserver,
+                                     blond_beam_to_xsuite_coords,
+                                     xsuite_coords_to_blond_coords)
 
 # Monitor objects
 from blond.monitors.monitors import BunchMonitor
@@ -33,7 +32,7 @@ from blond.plots.plot import Plot
 
 # Directory to save files
 this_directory = os.path.dirname(os.path.realpath(__file__)) + '/'
-results_directory = this_directory + '../output_files/EX_01'
+results_directory = this_directory + '../output_files/EX_23'
 os.makedirs(results_directory, exist_ok=True)
 
 # Parameters ----------------------------------------------------------------------------------------------------------
@@ -91,12 +90,12 @@ print('dE_sep,m: ' + str(dE_sep))
 
 # Make bunchmonitor
 bunchmonitor = BunchMonitor(ring, rfstation, beam,
-                            results_directory + '/EX_01_output_data', Profile=None)
+                            results_directory + '/EX_23_output_data', Profile=None)
 
-format_options = {'dirname': results_directory + '/EX_01_fig'}
+format_options = {'dirname': results_directory + '/EX_23_fig'}
 plots = Plot(ring, rfstation, beam, dt_plt, N_t, 0, 2 * t_u,
              -1.05 * dE_sep, 1.05 * dE_sep, xunit='s', separatrix_plot=True,
-             Profile=None, h5file=results_directory + '/EX_01_output_data',
+             Profile=None, h5file=results_directory + '/EX_23_output_data',
              format_options=format_options)
 
 # --- Creating interface elements ---
@@ -195,9 +194,9 @@ for i in range(N_t):
                 mon.ptau[0, i].T, mon.zeta[0, i].T, np.float64(mon.p0c[0, i]), np.float64(mon.gamma0[0, i]),
                 np.float64(mon.beta0[0, i]))
 
-with open(results_directory + '/EX_01_output_data.txt', 'w') as f:
+with open(results_directory + '/EX_23_output_data.txt', 'w') as f:
     f.write(test_string)
-with open(results_directory + '/EX_01_output_data_xsuite.txt', 'w') as f:
+with open(results_directory + '/EX_23_output_data_xsuite.txt', 'w') as f:
     f.write(xsuite_string)
 
 # Results --------------------------------------------------------------------------------------------------------------
diff --git a/__EXAMPLES/main_files/EX_24_single_particle_ramp_lhc.py b/__EXAMPLES/main_files/EX_24_single_particle_ramp_lhc.py
index 91e88d32..a36d3809 100644
--- a/__EXAMPLES/main_files/EX_24_single_particle_ramp_lhc.py
+++ b/__EXAMPLES/main_files/EX_24_single_particle_ramp_lhc.py
@@ -23,9 +23,9 @@ from blond.input_parameters.rf_parameters import RFStation
 from blond.trackers.tracker import RingAndRFTracker
 
 # Interface
-from xsuite_blond_interface import (BlondElement, BlondObserver, EnergyUpdate,
-                                    blond_beam_to_xsuite_coords,
-                                    xsuite_coords_to_blond_coords)
+from blond.interfaces.xsuite import (BlondElement, BlondObserver, EnergyUpdate,
+                                     blond_beam_to_xsuite_coords,
+                                     xsuite_coords_to_blond_coords)
 
 # Monitor objects
 from blond.monitors.monitors import BunchMonitor
@@ -33,7 +33,7 @@ from blond.plots.plot import Plot
 
 # Directory to save files
 this_directory = os.path.dirname(os.path.realpath(__file__)) + '/'
-results_directory = this_directory + '../output_files/EX_02'
+results_directory = this_directory + '../output_files/EX_24'
 os.makedirs(results_directory, exist_ok=True)
 
 # Parameters ----------------------------------------------------------------------------------------------------------
@@ -94,12 +94,12 @@ print('dE_sep,m: ' + str(dE_sep))
 
 # Make bunchmonitor
 bunchmonitor = BunchMonitor(ring, rfstation, beam,
-                            results_directory + '/EX_02_output_data', Profile=None)
+                            results_directory + '/EX_24_output_data', Profile=None)
 
-format_options = {'dirname': results_directory + '/EX_02_fig'}
+format_options = {'dirname': results_directory + '/EX_24_fig'}
 plots = Plot(ring, rfstation, beam, dt_plt, N_t, 0, 2*t_u,
              -1.05 * dE_sep, 1.05 * dE_sep, xunit='s', separatrix_plot=True,
-             Profile=None, h5file=results_directory + '/EX_02_output_data',
+             Profile=None, h5file=results_directory + '/EX_24_output_data',
              format_options=format_options)
 
 # --- Creating interface elements ---
@@ -205,9 +205,9 @@ for i in range(N_t):
         np.float64(mon.beta0[0, i])
     )
 
-with open(results_directory + '/EX_02_output_data.txt', 'w') as f:
+with open(results_directory + '/EX_24_output_data.txt', 'w') as f:
     f.write(test_string)
-with open(results_directory + '/EX_02_output_data_xsuite.txt', 'w') as f:
+with open(results_directory + '/EX_24_output_data_xsuite.txt', 'w') as f:
     f.write(xsuite_string)
 
 # Results --------------------------------------------------------------------------------------------------------------
diff --git a/__EXAMPLES/main_files/EX_25_single_particle_two_rfstations_lhc.py b/__EXAMPLES/main_files/EX_25_single_particle_two_rfstations_lhc.py
index 45c325e3..41e6e88f 100644
--- a/__EXAMPLES/main_files/EX_25_single_particle_two_rfstations_lhc.py
+++ b/__EXAMPLES/main_files/EX_25_single_particle_two_rfstations_lhc.py
@@ -21,9 +21,9 @@ from blond.input_parameters.rf_parameters import RFStation
 from blond.trackers.tracker import RingAndRFTracker
 
 # Interface
-from xsuite_blond_interface import (BlondElement,
-                                    blond_beam_to_xsuite_coords,
-                                    xsuite_coords_to_blond_coords)
+from blond.interfaces.xsuite import (BlondElement,
+                                     blond_beam_to_xsuite_coords,
+                                     xsuite_coords_to_blond_coords)
 
 # Monitor objects
 from blond.monitors.monitors import BunchMonitor
@@ -31,7 +31,7 @@ from blond.plots.plot import Plot
 
 # Directory to save files
 this_directory = os.path.dirname(os.path.realpath(__file__)) + '/'
-results_directory = this_directory + '../output_files/EX_03'
+results_directory = this_directory + '../output_files/EX_25'
 os.makedirs(results_directory, exist_ok=True)
 
 # Parameters ----------------------------------------------------------------------------------------------------------
@@ -102,12 +102,12 @@ print('dE_sep,m: ' + str(dE_sep))
 
 # Make bunchmonitor
 bunchmonitor = BunchMonitor(ring, rfstation_1, beam,
-                            results_directory + '/EX_03_output_data', Profile=None)
+                            results_directory + '/EX_25_output_data', Profile=None)
 
-format_options = {'dirname': results_directory + '/EX_03_fig'}
+format_options = {'dirname': results_directory + '/EX_25_fig'}
 plots = Plot(ring, rfstation_1, beam, dt_plt, N_t, 0, 2 * t_u,
              -1.05 * dE_sep, 1.05 * dE_sep, xunit='s', separatrix_plot=True,
-             Profile=None, h5file=results_directory + '/EX_03_output_data',
+             Profile=None, h5file=results_directory + '/EX_25_output_data',
              format_options=format_options)
 
 # --- Creating interface elements ---
@@ -225,9 +225,9 @@ for i in range(N_t):
         np.float64(mon.beta0[0, i])
     )
 
-with open(results_directory + '/EX_03_output_data.txt', 'w') as f:
+with open(results_directory + '/EX_25_output_data.txt', 'w') as f:
     f.write(test_string)
-with open(results_directory + '/EX_03_output_data_xsuite.txt', 'w') as f:
+with open(results_directory + '/EX_25_output_data_xsuite.txt', 'w') as f:
     f.write(xsuite_string)
 
 # Results --------------------------------------------------------------------------------------------------------------
diff --git a/__EXAMPLES/main_files/EX_26_single_particle_oneturnmatrix_psb.py b/__EXAMPLES/main_files/EX_26_single_particle_oneturnmatrix_psb.py
index aac0e0e4..6ea893ad 100644
--- a/__EXAMPLES/main_files/EX_26_single_particle_oneturnmatrix_psb.py
+++ b/__EXAMPLES/main_files/EX_26_single_particle_oneturnmatrix_psb.py
@@ -22,9 +22,9 @@ from blond.input_parameters.rf_parameters import RFStation
 from blond.trackers.tracker import RingAndRFTracker
 
 # Interface
-from xsuite_blond_interface import (BlondElement,
-                                    blond_beam_to_xsuite_coords,
-                                    xsuite_coords_to_blond_coords)
+from blond.interfaces.xsuite import (BlondElement,
+                                     blond_beam_to_xsuite_coords,
+                                     xsuite_coords_to_blond_coords)
 
 # Monitor objects
 from blond.monitors.monitors import BunchMonitor
@@ -32,7 +32,7 @@ from blond.plots.plot import Plot
 
 # Directory to save files
 this_directory = os.path.dirname(os.path.realpath(__file__)) + '/'
-results_directory = this_directory + '../output_files/EX_04'
+results_directory = this_directory + '../output_files/EX_26'
 os.makedirs(results_directory, exist_ok=True)
 
 # Parameters ----------------------------------------------------------------------------------------------------------
@@ -95,12 +95,12 @@ print('dE_sep,m: ' + str(dE_sep))
 
 # Make bunchmonitor
 bunchmonitor = BunchMonitor(ring, rfstation, beam,
-                            results_directory + '/EX_04_output_data', Profile=None)
+                            results_directory + '/EX_26_output_data', Profile=None)
 
-format_options = {'dirname': results_directory + '/EX_04_fig'}
+format_options = {'dirname': results_directory + '/EX_26_fig'}
 plots = Plot(ring, rfstation, beam, dt_plt, N_t, 0, 2 * t_u,
              -1.05 * dE_sep, 1.05 * dE_sep, xunit='s', separatrix_plot=True,
-             Profile=None, h5file=results_directory + '/EX_04_output_data',
+             Profile=None, h5file=results_directory + '/EX_26_output_data',
              format_options=format_options)
 
 # --- Creating interface elements ---
@@ -201,9 +201,9 @@ for i in range(N_t):
         np.float64(mon.beta0[0, i])
     )
 
-with open(results_directory + '/EX_04_output_data.txt', 'w') as f:
+with open(results_directory + '/EX_26_output_data.txt', 'w') as f:
     f.write(test_string)
-with open(results_directory + '/EX_04_output_data_xsuite.txt', 'w') as f:
+with open(results_directory + '/EX_26_output_data_xsuite.txt', 'w') as f:
     f.write(xsuite_string)
 
 # Results --------------------------------------------------------------------------------------------------------------
diff --git a/__EXAMPLES/main_files/EX_27_single_particle_ramp_psb.py b/__EXAMPLES/main_files/EX_27_single_particle_ramp_psb.py
index 18df366e..b4707f0f 100644
--- a/__EXAMPLES/main_files/EX_27_single_particle_ramp_psb.py
+++ b/__EXAMPLES/main_files/EX_27_single_particle_ramp_psb.py
@@ -22,9 +22,9 @@ from blond.input_parameters.rf_parameters import RFStation
 from blond.trackers.tracker import RingAndRFTracker
 
 # Interface
-from xsuite_blond_interface import (BlondElement, EnergyUpdate,
-                                    blond_beam_to_xsuite_coords,
-                                    xsuite_coords_to_blond_coords)
+from blond.interfaces.xsuite import (BlondElement, EnergyUpdate,
+                                     blond_beam_to_xsuite_coords,
+                                     xsuite_coords_to_blond_coords)
 
 # Monitor objects
 from blond.monitors.monitors import BunchMonitor
@@ -32,7 +32,7 @@ from blond.plots.plot import Plot
 
 # Directory to save files
 this_directory = os.path.dirname(os.path.realpath(__file__)) + '/'
-results_directory = this_directory + '../output_files/EX_05'
+results_directory = this_directory + '../output_files/EX_27'
 os.makedirs(results_directory, exist_ok=True)
 
 # Parameters ----------------------------------------------------------------------------------------------------------
@@ -96,12 +96,12 @@ print('dE_sep,m: ' + str(dE_sep))
 
 # Make bunchmonitor
 bunchmonitor = BunchMonitor(ring, rfstation, beam,
-                            results_directory + '/EX_05_output_data', Profile=None)
+                            results_directory + '/EX_27_output_data', Profile=None)
 
-format_options = {'dirname': results_directory + '/EX_05_fig'}
+format_options = {'dirname': results_directory + '/EX_27_fig'}
 plots = Plot(ring, rfstation, beam, dt_plt, N_t, 0, 2 * t_u,
              -1.05 * dE_sep, 1.05 * dE_sep, xunit='s', separatrix_plot=True,
-             Profile=None, h5file=results_directory + '/EX_05_output_data',
+             Profile=None, h5file=results_directory + '/EX_27_output_data',
              format_options=format_options)
 
 # --- Creating interface elements ---
@@ -207,9 +207,9 @@ for i in range(N_t):
         np.float64(mon.beta0[0, i])
     )
 
-with open(results_directory + '/EX_05_output_data.txt', 'w') as f:
+with open(results_directory + '/EX_27_output_data.txt', 'w') as f:
     f.write(test_string)
-with open(results_directory + '/EX_05_output_data_xsuite.txt', 'w') as f:
+with open(results_directory + '/EX_27_output_data_xsuite.txt', 'w') as f:
     f.write(xsuite_string)
 
 # Results --------------------------------------------------------------------------------------------------------------
diff --git a/__EXAMPLES/main_files/EX_28_phase_loop_lhc.py b/__EXAMPLES/main_files/EX_28_phase_loop_lhc.py
index b605c4d7..cefbbabd 100644
--- a/__EXAMPLES/main_files/EX_28_phase_loop_lhc.py
+++ b/__EXAMPLES/main_files/EX_28_phase_loop_lhc.py
@@ -24,9 +24,9 @@ from blond.trackers.tracker import RingAndRFTracker
 from blond.llrf.beam_feedback import BeamFeedback
 
 # Interface
-from xsuite_blond_interface import (BlondElement, BlondObserver,
-                                    blond_beam_to_xsuite_coords,
-                                    xsuite_coords_to_blond_coords)
+from blond.interfaces.xsuite import (BlondElement, BlondObserver,
+                                     blond_beam_to_xsuite_coords,
+                                     xsuite_coords_to_blond_coords)
 
 # Monitor objects
 from blond.monitors.monitors import BunchMonitor
@@ -34,7 +34,7 @@ from blond.plots.plot import Plot
 
 # Directory to save files
 this_directory = os.path.dirname(os.path.realpath(__file__)) + '/'
-results_directory = this_directory + '../output_files/EX_06'
+results_directory = this_directory + '../output_files/EX_28'
 os.makedirs(results_directory, exist_ok=True)
 
 # Parameters ----------------------------------------------------------------------------------------------------------
@@ -107,12 +107,12 @@ print('dE_sep,m: ' + str(dE_sep))
 
 # Make bunchmonitor
 bunchmonitor = BunchMonitor(ring, rfstation, beam,
-                            results_directory + '/EX_06_output_data', Profile=profile, PhaseLoop=BL)
+                            results_directory + '/EX_28_output_data', Profile=profile, PhaseLoop=BL)
 
-format_options = {'dirname': results_directory + '/EX_06_fig'}
+format_options = {'dirname': results_directory + '/EX_28_fig'}
 plots = Plot(ring, rfstation, beam, dt_plt, N_t, 0, 2*t_u,
              -1.05 * dE_sep, 1.05 * dE_sep, xunit='s', separatrix_plot=True,
-             Profile=profile, PhaseLoop=BL, h5file=results_directory + '/EX_06_output_data',
+             Profile=profile, PhaseLoop=BL, h5file=results_directory + '/EX_28_output_data',
              format_options=format_options)
 
 # --- Creating interface elements ---
@@ -215,9 +215,9 @@ for i in range(N_t):
         np.float64(mon.beta0[0, i])
     )
 
-with open(results_directory + '/EX_06_output_data.txt', 'w') as f:
+with open(results_directory + '/EX_28_output_data.txt', 'w') as f:
     f.write(test_string)
-with open(results_directory + '/EX_06_output_data_xsuite.txt', 'w') as f:
+with open(results_directory + '/EX_28_output_data_xsuite.txt', 'w') as f:
     f.write(xsuite_string)
 
 # Results --------------------------------------------------------------------------------------------------------------
-- 
GitLab


From ba7846b90fd105f9f1941f4c953231b481506119 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Birk=20Karlsen-B=C3=A6ck?= <birk.karlsen.baeck@gmail.com>
Date: Fri, 1 Nov 2024 11:49:28 +0100
Subject: [PATCH 3/9] [unittests][interfaces] Made a first draft of the
 unittests for the xsuite interface

---
 unittests/interfaces/__init__.py    |   0
 unittests/interfaces/test_xsuite.py | 482 ++++++++++++++++++++++++++++
 2 files changed, 482 insertions(+)
 create mode 100644 unittests/interfaces/__init__.py
 create mode 100644 unittests/interfaces/test_xsuite.py

diff --git a/unittests/interfaces/__init__.py b/unittests/interfaces/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/unittests/interfaces/test_xsuite.py b/unittests/interfaces/test_xsuite.py
new file mode 100644
index 00000000..be3770a7
--- /dev/null
+++ b/unittests/interfaces/test_xsuite.py
@@ -0,0 +1,482 @@
+# coding: utf8
+# Copyright 2014-2017 CERN. This software is distributed under the
+# terms of the GNU General Public Licence version 3 (GPL Version 3),
+# copied verbatim in the file LICENCE.md.
+# In applying this licence, CERN does not waive the privileges and immunities
+# granted to it by virtue of its status as an Intergovernmental Organization or
+# submit itself to any jurisdiction.
+# Project website: http://blond.web.cern.ch/
+
+"""
+Unittest for interfaces.xsuite
+
+:Authors: **Birk Emil Karlsen-Bæck**, **Thom Arnoldus van Rijswijk**
+"""
+
+import os
+import unittest
+
+import numpy as np
+from scipy.constants import c, e, m_p
+
+import xpart as xp
+import xtrack as xt
+
+from blond.beam.beam import Beam, Proton
+from blond.beam.profile import CutOptions, Profile
+from blond.input_parameters.rf_parameters import RFStation
+from blond.input_parameters.ring import Ring
+from blond.trackers.tracker import RingAndRFTracker, FullRingAndRF
+
+from blond.interfaces.xsuite import (BlondElement, BlondObserver,
+                                     EnergyUpdate,
+                                     blond_beam_to_xsuite_coords,
+                                     xsuite_coords_to_blond_coords)
+
+this_directory = os.path.dirname(os.path.realpath(__file__))
+
+# TODO: Make a test of the EnergyFrequencyUpdate and BlondObserver classes
+
+
+class TestXsuiteBLonDTransforms(unittest.TestCase):
+    # TODO: finish implementation of the test of the particle coordinates
+    def setUp(self):
+        pass
+
+
+class TestXsuiteLHC(unittest.TestCase):
+
+    def setUp(self):
+        # Accelerator parameters
+        self.C = 26658.8832                     # Machine circumference [m]
+        self.p_s = 450e9                        # Synchronous momentum [eV/c]
+        self.p_f = 450.1e9                      # Synchronous momentum, final
+        self.h = 35640                          # Harmonic number [-]
+        self.alpha = 0.00034849575112251314     # First order mom. comp. factor [-]
+        self.V = 5e6                            # RF voltage [V]
+        self.dphi = 0                           # Phase modulation/offset [rad]
+
+    def testSingleParticle(self):
+        r'''Test of a single particle simulation in the LHC at constant energy'''
+        rtol = 1e-2                             # relative tolerance
+        atol = 0                                # absolute tolerance
+
+        # ----- Interface Simulation -----
+        # Initialize the BLonD cavity
+        blond_tracker, beam = self.singleParticleBLonDSimulation()
+        cavity = BlondElement(blond_tracker, beam)
+
+        line = self.initializeXsuiteLine()
+
+        # Insert the BLonD elements
+        line.insert_element(index=0, element=cavity, name='blond_cavity')
+
+        dt_inter, dE_inter = self.performSingleParticleInterfaceSimulation(line, blond_tracker, beam)
+
+        # ----- Pure BLonD Simulation -----
+        blond_tracker, beam = self.singleParticleBLonDSimulation()
+        dt_blond, dE_blond = self.performSingleParticleBLonDSimulation(blond_tracker, beam)
+
+        # ----- Perform test -----
+        np.testing.assert_allclose(
+            dt_inter, dt_blond, rtol=rtol, atol=atol,
+            err_msg='In testSingleParticle the time-coordinate differs'
+        )
+        np.testing.assert_allclose(
+            dE_inter, dE_blond, rtol=rtol, atol=atol,
+            err_msg='In testSingleParticle the energy-coordinate differs'
+        )
+
+    def testSingleParticleRamp(self):
+        r'''Test of acceleration of a single particle simulation in the LHC'''
+        rtol = 1e-2                             # relative tolerance
+        atol = 0                                # absolute tolerance
+
+        # Initialize the BLonD cavity
+        blond_tracker, beam = self.singleParticleBLonDSimulation(ramp=True)
+        cavity = BlondElement(blond_tracker, beam)
+
+        line = self.initializeXsuiteLine()
+
+        # Insert the BLonD elements
+        line.insert_element(index=0, element=cavity, name='blond_cavity')
+
+        # Insert energy ramp
+        energy_update = EnergyUpdate(blond_tracker.rf_params.momentum)
+        line.insert_element(index='matrix', element=energy_update, name='energy_update')
+
+        dt_inter, dE_inter = self.performSingleParticleInterfaceSimulation(line, blond_tracker, beam)
+
+        # ----- Pure BLonD Simulation -----
+        blond_tracker, beam = self.singleParticleBLonDSimulation(ramp=True)
+        dt_blond, dE_blond = self.performSingleParticleBLonDSimulation(blond_tracker, beam)
+
+        # ----- Perform test -----
+        np.testing.assert_allclose(
+            dt_inter, dt_blond, rtol=rtol, atol=atol,
+            err_msg='In testSingleParticleRamp the time-coordinate differs'
+        )
+        np.testing.assert_allclose(
+            dE_inter, dE_blond, rtol=rtol, atol=atol,
+            err_msg='In testSingleParticleRamp the energy-coordinate differs'
+        )
+
+    def testSingleParticleTwoRFStations(self):
+        r'''Test of a single particle simulation in the LHC at constant energy with two RF stations'''
+        rtol = 1e-2                             # relative tolerance
+        atol = 0                                # absolute tolerance
+
+        # Initialize the BLonD cavities
+        blond_tracker_1, blond_tracker_2, beam = self.singleParticleBLonDSimulation(two_rfstations=True)
+        cavity_1 = BlondElement(blond_tracker_1, beam)
+        cavity_2 = BlondElement(blond_tracker_2, beam)
+
+        line = self.initializeXsuiteLine(two_rfstations=True)
+
+        # Insert the BLonD elements
+        line.insert_element(index='matrix', element=cavity_1, name='blond_cavity_1')
+        line.insert_element(index='matrix_2', element=cavity_2, name='blond_cavity_2')
+
+        dt_inter, dE_inter = self.performSingleParticleInterfaceSimulation(line, blond_tracker_1, beam)
+
+        # ----- Pure BLonD Simulation -----
+        blond_tracker, beam = self.singleParticleBLonDSimulation(two_rfstations=True)
+        dt_blond, dE_blond = self.performSingleParticleBLonDSimulation(blond_tracker, beam)
+
+        # ----- Perform test -----
+        np.testing.assert_allclose(
+            dt_inter, dt_blond, rtol=rtol, atol=atol,
+            err_msg='In testSingleParticleTwoRFStations the time-coordinate differs'
+        )
+        np.testing.assert_allclose(
+            dE_inter, dE_blond, rtol=rtol, atol=atol,
+            err_msg='In testSingleParticleTwoRFStations the energy-coordinate differs'
+        )
+
+    def testBunchWithPhaseLoop(self):
+        r'''Test of a single bunch simulation in the LHC at constant energy with the beam-phase loop'''
+        # TODO: finish implementation of the test with the beam-phase loop
+        line = self.initializeXsuiteLine()
+
+    def singleParticleBLonDSimulation(self, ramp: bool = False, two_rfstations: bool = False):
+        r'''
+        Method to generate a BLonD simulation in the LHC with a single particle with a time offset with respect to the
+        RF bucket.
+        '''
+        # Bunch parameters
+        N_m = 1                                 # Number of macroparticles [-]
+        N_p = 1.15e11                           # Intensity
+
+        # Simulation parameters
+        N_t = 481                               # Number of (tracked) turns [-]
+        dt_offset = 0.1                         # Input particles dt [s]
+        v_part = 1
+
+        # --- BLonD objects ---
+        if ramp:
+            ring = Ring(self.C, self.alpha, np.linspace(self.p_s, self.p_f, N_t + 1), Proton(), n_turns=N_t)
+        else:
+            if two_rfstations:
+                # Additional input
+                c_part = 0.3
+                v_part = 0.5
+
+                ring = Ring(
+                    [self.C * c_part, self.C * (1 - c_part)],
+                    [[self.alpha], [self.alpha]],
+                    [self.p_s, self.p_s],
+                    Proton(), n_turns=N_t, n_sections=2
+                )
+            else:
+                ring = Ring(self.C, self.alpha, self.p_s, Proton(), n_turns=N_t)
+
+        beam = Beam(ring, N_m, N_p)
+        rfstation = RFStation(ring, [self.h], [self.V * v_part], [self.dphi])
+
+        # --- Insert particle ---
+        beam.dt = np.array([rfstation.t_rf[0, 0] * (1 - dt_offset)])
+        beam.dE = np.array([0])
+
+        if two_rfstations:
+            second_rfstation = RFStation(ring, [self.h], [self.V * (1 - v_part)],
+                                         [self.dphi], section_index=2)
+
+            return RingAndRFTracker(rfstation, beam), RingAndRFTracker(second_rfstation, beam), beam
+
+        return RingAndRFTracker(rfstation, beam), beam
+
+    def initializeXsuiteLine(self, two_rfstations: bool = False):
+        r'''
+        Method to generate an Xsuite Line using the one-turn matrix.
+        '''
+        c_part = 1
+
+        if two_rfstations:
+            c_part = 0.3
+            matrix_2 = xt.LineSegmentMap(
+                longitudinal_mode='nonlinear',
+                qx=1.1, qy=1.2,
+                betx=1.,
+                bety=1.,
+                voltage_rf=0,
+                frequency_rf=0,
+                lag_rf=0,
+                momentum_compaction_factor=self.alpha,
+                length=self.C * (1 - c_part)
+            )
+
+        matrix = xt.LineSegmentMap(
+            longitudinal_mode='nonlinear',
+            qx=1.1, qy=1.2,
+            betx=1.,
+            bety=1.,
+            voltage_rf=0,
+            frequency_rf=0,
+            lag_rf=0,
+            momentum_compaction_factor=self.alpha,
+            length=self.C * c_part
+        )
+
+        # Create line
+        line = xt.Line(elements=[matrix], element_names={'matrix'})
+        line['matrix'].length = self.C * c_part
+
+        if two_rfstations:
+            line.append_element(element=matrix_2, name='matrix_2')
+            line['matrix_2'].length = (1 - c_part) * self.C
+
+        return line
+
+    def performSingleParticleInterfaceSimulation(self, line, blond_track, beam):
+        # Add particles to line and build tracker
+        line.particle_ref = xp.Particles(p0c=self.p_s, mass0=xp.PROTON_MASS_EV, q0=1.)
+        line.build_tracker()
+
+        # --- Convert the initial BLonD distribution to xsuite coordinates ---
+        zeta, ptau = blond_beam_to_xsuite_coords(beam,
+                                                 line.particle_ref.beta0[0],
+                                                 line.particle_ref.energy0[0],
+                                                 phi_s=blond_track.rf_params.phi_s[0],
+                                                 omega_rf=blond_track.rf_params.omega_rf[0, 0])
+
+        # --- Track matrix ---
+        N_t = len(blond_track.rf_params.phi_s) - 1
+        particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
+        line.track(particles, num_turns=N_t, turn_by_turn_monitor=True, with_progress=True)
+        mon = line.record_last_track
+
+        dt_array = np.zeros(N_t)
+        dE_array = np.zeros(N_t)
+        # Convert the xsuite particle coordinates back to BLonD
+        for i in range(N_t):
+            dt_array[i], dE_array[i] = xsuite_coords_to_blond_coords(
+                mon.zeta[:, i].T, mon.ptau[:, i].T, blond_track.rf_params.beta[i], blond_track.rf_params.energy[i],
+                phi_s=blond_track.rf_params.phi_s[i] - blond_track.rf_params.phi_rf[0, 0],
+                omega_rf=blond_track.rf_params.omega_rf[0, i]
+            )
+
+        return dt_array, dE_array
+
+    @staticmethod
+    def performSingleParticleBLonDSimulation(blond_track, beam, second_blond_tracker: RingAndRFTracker = None):
+        # The number of turns to track
+        N_t = len(blond_track.rf_params.phi_s) - 1
+
+        # Save particle coordinates
+        dt_array = np.zeros(N_t)
+        dE_array = np.zeros(N_t)
+
+        if second_blond_tracker is not None:
+            full_tracker = FullRingAndRF([blond_track, second_blond_tracker])
+        else:
+            full_tracker = blond_track
+
+        for i in range(N_t):
+            full_tracker.track()
+            dt_array[i] = beam.dt[0]
+            dE_array[i] = beam.dE[0]
+
+        return dt_array, dE_array
+
+
+class TestXsuitePSB(unittest.TestCase):
+
+    def setUp(self):
+        # Accelerator parameters
+        self.C = 2 * np.pi * 25.00005794526065  # Machine circumference, radius 25 [m]
+        gamma_t = 4.11635447373496              # Transition gamma [-]
+        self.alpha = 1. / gamma_t / gamma_t     # First order mom. comp. factor [-]
+        self.h = 1                              # Harmonic number [-]
+        self.V = 8e3                            # RF voltage [V]
+        self.dphi = np.pi                       # Phase modulation/offset [rad]
+
+        # Derived parameters
+        kin_energy = 1.4e9                      # Kinetic energy [eV]
+        E_0 = m_p * c ** 2 / e                  # [eV]
+        tot_energy = E_0 + kin_energy           # [eV]
+
+        self.p_s = np.sqrt(tot_energy ** 2 - E_0 ** 2)
+        self.p_f = self.p_s + 0.001e9
+
+    def testSingleParticle(self):
+        r'''Test of a single particle simulation in the PSB at constant energy'''
+        rtol = 1e-2                             # relative tolerance
+        atol = 0                                # absolute tolerance
+
+        # ----- Interface Simulation -----
+        # Initialize the BLonD cavity
+        blond_tracker, beam = self.singleParticleBLonDSimulation()
+        cavity = BlondElement(blond_tracker, beam)
+
+        line = self.initializeXsuiteLine()
+
+        # Insert the BLonD elements
+        line.insert_element(index=0, element=cavity, name='blond_cavity')
+
+        dt_inter, dE_inter = self.performSingleParticleInterfaceSimulation(line, blond_tracker, beam)
+
+        # ----- Pure BLonD Simulation -----
+        blond_tracker, beam = self.singleParticleBLonDSimulation()
+        dt_blond, dE_blond = self.performSingleParticleBLonDSimulation(blond_tracker, beam)
+
+        # ----- Perform test -----
+        np.testing.assert_allclose(
+            dt_inter, dt_blond, rtol=rtol, atol=atol,
+            err_msg='In testSingleParticle the time-coordinate differs'
+        )
+        np.testing.assert_allclose(
+            dE_inter, dE_blond, rtol=rtol, atol=atol,
+            err_msg='In testSingleParticle the energy-coordinate differs'
+        )
+
+    def testSingleParticleRamp(self):
+        r'''Test of acceleration of a single particle simulation in the PSB'''
+        rtol = 1e-2                             # relative tolerance
+        atol = 0                                # absolute tolerance
+
+        # Initialize the BLonD cavity
+        blond_tracker, beam = self.singleParticleBLonDSimulation(ramp=True)
+        cavity = BlondElement(blond_tracker, beam)
+
+        line = self.initializeXsuiteLine()
+
+        # Insert the BLonD elements
+        line.insert_element(index=0, element=cavity, name='blond_cavity')
+
+        # Insert energy ramp
+        energy_update = EnergyUpdate(blond_tracker.rf_params.momentum)
+        line.insert_element(index='matrix', element=energy_update, name='energy_update')
+
+        dt_inter, dE_inter = self.performSingleParticleInterfaceSimulation(line, blond_tracker, beam)
+
+        # ----- Pure BLonD Simulation -----
+        blond_tracker, beam = self.singleParticleBLonDSimulation(ramp=True)
+        dt_blond, dE_blond = self.performSingleParticleBLonDSimulation(blond_tracker, beam)
+
+        # ----- Perform test -----
+        np.testing.assert_allclose(
+            dt_inter, dt_blond, rtol=rtol, atol=atol,
+            err_msg='In testSingleParticleRamp the time-coordinate differs'
+        )
+        np.testing.assert_allclose(
+            dE_inter, dE_blond, rtol=rtol, atol=atol,
+            err_msg='In testSingleParticleRamp the energy-coordinate differs'
+        )
+
+    def singleParticleBLonDSimulation(self, ramp: bool = False):
+        r'''
+        Method to generate a BLonD simulation in the LHC with a single particle with a time offset with respect to the
+        RF bucket.
+        '''
+        # Bunch parameters
+        N_m = 1                                 # Number of macroparticles [-]
+        N_p = 1e11                              # Intensity
+
+        # Simulation parameters
+        N_t = 6000                              # Number of (tracked) turns [-]
+        dt_offset = 0.1                         # Input particles dt [s]
+
+        # --- BLonD objects ---
+        if ramp:
+            ring = Ring(self.C, self.alpha, np.linspace(self.p_s, self.p_f, N_t + 1), Proton(), n_turns=N_t)
+        else:
+            ring = Ring(self.C, self.alpha, self.p_s, Proton(), n_turns=N_t)
+
+        beam = Beam(ring, N_m, N_p)
+        rfstation = RFStation(ring, [self.h], [self.V], [self.dphi])
+
+        # --- Insert particle ---
+        beam.dt = np.array([rfstation.t_rf[0, 0] * (1 - dt_offset)])
+        beam.dE = np.array([0])
+
+        return RingAndRFTracker(rfstation, beam), beam
+
+    def initializeXsuiteLine(self):
+        r'''
+        Method to generate an Xsuite Line using the one-turn matrix.
+        '''
+        matrix = xt.LineSegmentMap(
+            longitudinal_mode='nonlinear',
+            qx=1.1, qy=1.2,
+            betx=1.,
+            bety=1.,
+            voltage_rf=0,
+            frequency_rf=0,
+            lag_rf=0,
+            momentum_compaction_factor=self.alpha,
+            length=self.C
+        )
+
+        # Create line
+        line = xt.Line(elements=[matrix], element_names={'matrix'})
+        line['matrix'].length = self.C
+
+        return line
+
+    def performSingleParticleInterfaceSimulation(self, line, blond_track, beam):
+        # Add particles to line and build tracker
+        line.particle_ref = xp.Particles(p0c=self.p_s, mass0=xp.PROTON_MASS_EV, q0=1.)
+        line.build_tracker()
+
+        # --- Convert the initial BLonD distribution to xsuite coordinates ---
+        zeta, ptau = blond_beam_to_xsuite_coords(beam,
+                                                 line.particle_ref.beta0[0],
+                                                 line.particle_ref.energy0[0],
+                                                 phi_s=blond_track.rf_params.phi_s[0],
+                                                 omega_rf=blond_track.rf_params.omega_rf[0, 0])
+
+        # --- Track matrix ---
+        N_t = len(blond_track.rf_params.phi_s) - 1
+        particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
+        line.track(particles, num_turns=N_t, turn_by_turn_monitor=True, with_progress=True)
+        mon = line.record_last_track
+
+        dt_array = np.zeros(N_t)
+        dE_array = np.zeros(N_t)
+        # Convert the xsuite particle coordinates back to BLonD
+        for i in range(N_t):
+            dt_array[i], dE_array[i] = xsuite_coords_to_blond_coords(
+                mon.zeta[:, i].T, mon.ptau[:, i].T, blond_track.rf_params.beta[i], blond_track.rf_params.energy[i],
+                phi_s=blond_track.rf_params.phi_s[i] - blond_track.rf_params.phi_rf[0, 0],
+                omega_rf=blond_track.rf_params.omega_rf[0, i]
+            )
+
+        return dt_array, dE_array
+
+    @staticmethod
+    def performSingleParticleBLonDSimulation(blond_track, beam):
+        # The number of turns to track
+        N_t = len(blond_track.rf_params.phi_s) - 1
+
+        # Save particle coordinates
+        dt_array = np.zeros(N_t)
+        dE_array = np.zeros(N_t)
+
+        full_tracker = blond_track
+
+        for i in range(N_t):
+            full_tracker.track()
+            dt_array[i] = beam.dt[0]
+            dE_array[i] = beam.dE[0]
+
+        return dt_array, dE_array
\ No newline at end of file
-- 
GitLab


From 104da833f958c11e4a3f2bcf24d660b5e617f87e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Birk=20Karlsen-B=C3=A6ck?= <birk.karlsen.baeck@gmail.com>
Date: Fri, 1 Nov 2024 12:07:28 +0100
Subject: [PATCH 4/9] [unittests][interfaces][test xsuite] changed initial
 energy to be float

---
 unittests/interfaces/test_xsuite.py | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/unittests/interfaces/test_xsuite.py b/unittests/interfaces/test_xsuite.py
index be3770a7..dd8f26ff 100644
--- a/unittests/interfaces/test_xsuite.py
+++ b/unittests/interfaces/test_xsuite.py
@@ -195,7 +195,7 @@ class TestXsuiteLHC(unittest.TestCase):
 
         # --- Insert particle ---
         beam.dt = np.array([rfstation.t_rf[0, 0] * (1 - dt_offset)])
-        beam.dE = np.array([0])
+        beam.dE = np.array([0.0])
 
         if two_rfstations:
             second_rfstation = RFStation(ring, [self.h], [self.V * (1 - v_part)],
@@ -407,7 +407,7 @@ class TestXsuitePSB(unittest.TestCase):
 
         # --- Insert particle ---
         beam.dt = np.array([rfstation.t_rf[0, 0] * (1 - dt_offset)])
-        beam.dE = np.array([0])
+        beam.dE = np.array([0.0])
 
         return RingAndRFTracker(rfstation, beam), beam
 
-- 
GitLab


From d47ae36d4b590e1f3084f76a1bff4afdd670dd1d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Birk=20Karlsen-B=C3=A6ck?= <birk.karlsen.baeck@gmail.com>
Date: Wed, 6 Nov 2024 23:08:29 +0100
Subject: [PATCH 5/9] [unittests][interfaces][test xsuite] Finished unit tests
 of single particle in the LHC and PSB

---
 unittests/interfaces/test_xsuite.py | 114 +++++++++++++++++++---------
 1 file changed, 78 insertions(+), 36 deletions(-)

diff --git a/unittests/interfaces/test_xsuite.py b/unittests/interfaces/test_xsuite.py
index dd8f26ff..d9709927 100644
--- a/unittests/interfaces/test_xsuite.py
+++ b/unittests/interfaces/test_xsuite.py
@@ -27,6 +27,7 @@ from blond.beam.profile import CutOptions, Profile
 from blond.input_parameters.rf_parameters import RFStation
 from blond.input_parameters.ring import Ring
 from blond.trackers.tracker import RingAndRFTracker, FullRingAndRF
+from blond.monitors.monitors import BunchMonitor
 
 from blond.interfaces.xsuite import (BlondElement, BlondObserver,
                                      EnergyUpdate,
@@ -58,8 +59,8 @@ class TestXsuiteLHC(unittest.TestCase):
 
     def testSingleParticle(self):
         r'''Test of a single particle simulation in the LHC at constant energy'''
-        rtol = 1e-2                             # relative tolerance
-        atol = 0                                # absolute tolerance
+        rtol = 4e-9                             # relative tolerance
+        atol = 5e-9                             # absolute tolerance
 
         # ----- Interface Simulation -----
         # Initialize the BLonD cavity
@@ -72,10 +73,14 @@ class TestXsuiteLHC(unittest.TestCase):
         line.insert_element(index=0, element=cavity, name='blond_cavity')
 
         dt_inter, dE_inter = self.performSingleParticleInterfaceSimulation(line, blond_tracker, beam)
+        dt_inter = dt_inter / np.max(dt_inter)
+        dE_inter = dE_inter / np.max(dE_inter)
 
         # ----- Pure BLonD Simulation -----
         blond_tracker, beam = self.singleParticleBLonDSimulation()
         dt_blond, dE_blond = self.performSingleParticleBLonDSimulation(blond_tracker, beam)
+        dt_blond = dt_blond / np.max(dt_blond)
+        dE_blond = dE_blond / np.max(dE_blond)
 
         # ----- Perform test -----
         np.testing.assert_allclose(
@@ -89,8 +94,8 @@ class TestXsuiteLHC(unittest.TestCase):
 
     def testSingleParticleRamp(self):
         r'''Test of acceleration of a single particle simulation in the LHC'''
-        rtol = 1e-2                             # relative tolerance
-        atol = 0                                # absolute tolerance
+        rtol = 4e-9                             # relative tolerance
+        atol = 5e-9                             # absolute tolerance
 
         # Initialize the BLonD cavity
         blond_tracker, beam = self.singleParticleBLonDSimulation(ramp=True)
@@ -103,13 +108,18 @@ class TestXsuiteLHC(unittest.TestCase):
 
         # Insert energy ramp
         energy_update = EnergyUpdate(blond_tracker.rf_params.momentum)
-        line.insert_element(index='matrix', element=energy_update, name='energy_update')
+        line.insert_element(index='blond_cavity', element=energy_update, name='energy_update')
 
         dt_inter, dE_inter = self.performSingleParticleInterfaceSimulation(line, blond_tracker, beam)
+        dt_inter = dt_inter / np.max(dt_inter)
+        dE_inter = dE_inter / np.max(dE_inter)
+        line.get_table().show()
 
         # ----- Pure BLonD Simulation -----
-        blond_tracker, beam = self.singleParticleBLonDSimulation(ramp=True)
+        blond_tracker, beam = self.singleParticleBLonDSimulation(ramp=True, pure_blond=True)
         dt_blond, dE_blond = self.performSingleParticleBLonDSimulation(blond_tracker, beam)
+        dt_blond = dt_blond / np.max(dt_blond)
+        dE_blond = dE_blond / np.max(dE_blond)
 
         # ----- Perform test -----
         np.testing.assert_allclose(
@@ -123,8 +133,8 @@ class TestXsuiteLHC(unittest.TestCase):
 
     def testSingleParticleTwoRFStations(self):
         r'''Test of a single particle simulation in the LHC at constant energy with two RF stations'''
-        rtol = 1e-2                             # relative tolerance
-        atol = 0                                # absolute tolerance
+        rtol = 4e-9                             # relative tolerance
+        atol = 5e-9                             # absolute tolerance
 
         # Initialize the BLonD cavities
         blond_tracker_1, blond_tracker_2, beam = self.singleParticleBLonDSimulation(two_rfstations=True)
@@ -138,10 +148,14 @@ class TestXsuiteLHC(unittest.TestCase):
         line.insert_element(index='matrix_2', element=cavity_2, name='blond_cavity_2')
 
         dt_inter, dE_inter = self.performSingleParticleInterfaceSimulation(line, blond_tracker_1, beam)
+        dt_inter = dt_inter / np.max(dt_inter)
+        dE_inter = dE_inter / np.max(dE_inter)
 
         # ----- Pure BLonD Simulation -----
-        blond_tracker, beam = self.singleParticleBLonDSimulation(two_rfstations=True)
-        dt_blond, dE_blond = self.performSingleParticleBLonDSimulation(blond_tracker, beam)
+        blond_tracker_1, blond_tracker_2, beam = self.singleParticleBLonDSimulation(two_rfstations=True)
+        dt_blond, dE_blond = self.performSingleParticleBLonDSimulation(blond_tracker_1, beam, blond_tracker_2)
+        dt_blond = dt_blond / np.max(dt_blond)
+        dE_blond = dE_blond / np.max(dE_blond)
 
         # ----- Perform test -----
         np.testing.assert_allclose(
@@ -153,12 +167,8 @@ class TestXsuiteLHC(unittest.TestCase):
             err_msg='In testSingleParticleTwoRFStations the energy-coordinate differs'
         )
 
-    def testBunchWithPhaseLoop(self):
-        r'''Test of a single bunch simulation in the LHC at constant energy with the beam-phase loop'''
-        # TODO: finish implementation of the test with the beam-phase loop
-        line = self.initializeXsuiteLine()
-
-    def singleParticleBLonDSimulation(self, ramp: bool = False, two_rfstations: bool = False):
+    def singleParticleBLonDSimulation(self, ramp: bool = False, two_rfstations: bool = False,
+                                      pure_blond: bool = False):
         r'''
         Method to generate a BLonD simulation in the LHC with a single particle with a time offset with respect to the
         RF bucket.
@@ -169,12 +179,17 @@ class TestXsuiteLHC(unittest.TestCase):
 
         # Simulation parameters
         N_t = 481                               # Number of (tracked) turns [-]
-        dt_offset = 0.1                         # Input particles dt [s]
+        dt_offset = 0.15                        # Input particles dt [s]
         v_part = 1
 
         # --- BLonD objects ---
         if ramp:
-            ring = Ring(self.C, self.alpha, np.linspace(self.p_s, self.p_f, N_t + 1), Proton(), n_turns=N_t)
+            cycle = np.linspace(self.p_s, self.p_f, N_t + 1)
+            if pure_blond:
+                cycle = np.concatenate(
+                    (np.array([self.p_s]), np.linspace(self.p_s, self.p_f, N_t + 1)[:-1])
+                )
+            ring = Ring(self.C, self.alpha, cycle, Proton(), n_turns=N_t)
         else:
             if two_rfstations:
                 # Additional input
@@ -201,7 +216,9 @@ class TestXsuiteLHC(unittest.TestCase):
             second_rfstation = RFStation(ring, [self.h], [self.V * (1 - v_part)],
                                          [self.dphi], section_index=2)
 
-            return RingAndRFTracker(rfstation, beam), RingAndRFTracker(second_rfstation, beam), beam
+            return (RingAndRFTracker(rfstation, beam),
+                    RingAndRFTracker(second_rfstation, beam),
+                    beam)
 
         return RingAndRFTracker(rfstation, beam), beam
 
@@ -248,6 +265,9 @@ class TestXsuiteLHC(unittest.TestCase):
         return line
 
     def performSingleParticleInterfaceSimulation(self, line, blond_track, beam):
+        r'''
+        Perform simulation with the interface.
+        '''
         # Add particles to line and build tracker
         line.particle_ref = xp.Particles(p0c=self.p_s, mass0=xp.PROTON_MASS_EV, q0=1.)
         line.build_tracker()
@@ -262,7 +282,7 @@ class TestXsuiteLHC(unittest.TestCase):
         # --- Track matrix ---
         N_t = len(blond_track.rf_params.phi_s) - 1
         particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
-        line.track(particles, num_turns=N_t, turn_by_turn_monitor=True, with_progress=True)
+        line.track(particles, num_turns=N_t, turn_by_turn_monitor=True, with_progress=False)
         mon = line.record_last_track
 
         dt_array = np.zeros(N_t)
@@ -279,6 +299,9 @@ class TestXsuiteLHC(unittest.TestCase):
 
     @staticmethod
     def performSingleParticleBLonDSimulation(blond_track, beam, second_blond_tracker: RingAndRFTracker = None):
+        r'''
+        Perform simulation in pure BLonD.
+        '''
         # The number of turns to track
         N_t = len(blond_track.rf_params.phi_s) - 1
 
@@ -292,9 +315,9 @@ class TestXsuiteLHC(unittest.TestCase):
             full_tracker = blond_track
 
         for i in range(N_t):
-            full_tracker.track()
             dt_array[i] = beam.dt[0]
             dE_array[i] = beam.dE[0]
+            full_tracker.track()
 
         return dt_array, dE_array
 
@@ -320,8 +343,8 @@ class TestXsuitePSB(unittest.TestCase):
 
     def testSingleParticle(self):
         r'''Test of a single particle simulation in the PSB at constant energy'''
-        rtol = 1e-2                             # relative tolerance
-        atol = 0                                # absolute tolerance
+        rtol = 4e-4                             # relative tolerance
+        atol = 4.1e-4                           # absolute tolerance
 
         # ----- Interface Simulation -----
         # Initialize the BLonD cavity
@@ -334,10 +357,14 @@ class TestXsuitePSB(unittest.TestCase):
         line.insert_element(index=0, element=cavity, name='blond_cavity')
 
         dt_inter, dE_inter = self.performSingleParticleInterfaceSimulation(line, blond_tracker, beam)
+        dt_inter = dt_inter / np.max(dt_inter)
+        dE_inter = dE_inter / np.max(dE_inter)
 
         # ----- Pure BLonD Simulation -----
         blond_tracker, beam = self.singleParticleBLonDSimulation()
         dt_blond, dE_blond = self.performSingleParticleBLonDSimulation(blond_tracker, beam)
+        dt_blond = dt_blond / np.max(dt_blond)
+        dE_blond = dE_blond / np.max(dE_blond)
 
         # ----- Perform test -----
         np.testing.assert_allclose(
@@ -351,8 +378,8 @@ class TestXsuitePSB(unittest.TestCase):
 
     def testSingleParticleRamp(self):
         r'''Test of acceleration of a single particle simulation in the PSB'''
-        rtol = 1e-2                             # relative tolerance
-        atol = 0                                # absolute tolerance
+        rtol = 4e-4                             # relative tolerance
+        atol = 4.1e-4                           # absolute tolerance
 
         # Initialize the BLonD cavity
         blond_tracker, beam = self.singleParticleBLonDSimulation(ramp=True)
@@ -368,10 +395,14 @@ class TestXsuitePSB(unittest.TestCase):
         line.insert_element(index='matrix', element=energy_update, name='energy_update')
 
         dt_inter, dE_inter = self.performSingleParticleInterfaceSimulation(line, blond_tracker, beam)
+        dt_inter = dt_inter / np.max(dt_inter)
+        dE_inter = dE_inter / np.max(dE_inter)
 
         # ----- Pure BLonD Simulation -----
-        blond_tracker, beam = self.singleParticleBLonDSimulation(ramp=True)
+        blond_tracker, beam = self.singleParticleBLonDSimulation(ramp=True, pure_blond=True)
         dt_blond, dE_blond = self.performSingleParticleBLonDSimulation(blond_tracker, beam)
+        dt_blond = dt_blond / np.max(dt_blond)
+        dE_blond = dE_blond / np.max(dE_blond)
 
         # ----- Perform test -----
         np.testing.assert_allclose(
@@ -383,7 +414,7 @@ class TestXsuitePSB(unittest.TestCase):
             err_msg='In testSingleParticleRamp the energy-coordinate differs'
         )
 
-    def singleParticleBLonDSimulation(self, ramp: bool = False):
+    def singleParticleBLonDSimulation(self, ramp: bool = False, pure_blond: bool = False):
         r'''
         Method to generate a BLonD simulation in the LHC with a single particle with a time offset with respect to the
         RF bucket.
@@ -394,11 +425,16 @@ class TestXsuitePSB(unittest.TestCase):
 
         # Simulation parameters
         N_t = 6000                              # Number of (tracked) turns [-]
-        dt_offset = 0.1                         # Input particles dt [s]
+        dt_offset = 0.15                         # Input particles dt [s]
 
         # --- BLonD objects ---
         if ramp:
-            ring = Ring(self.C, self.alpha, np.linspace(self.p_s, self.p_f, N_t + 1), Proton(), n_turns=N_t)
+            cycle = np.linspace(self.p_s, self.p_f, N_t + 1)
+            if pure_blond:
+                cycle = np.concatenate(
+                    (np.array([self.p_s]), np.linspace(self.p_s, self.p_f, N_t + 1)[:-1])
+                )
+            ring = Ring(self.C, self.alpha, cycle, Proton(), n_turns=N_t)
         else:
             ring = Ring(self.C, self.alpha, self.p_s, Proton(), n_turns=N_t)
 
@@ -434,21 +470,24 @@ class TestXsuitePSB(unittest.TestCase):
         return line
 
     def performSingleParticleInterfaceSimulation(self, line, blond_track, beam):
+        r'''
+        Perform simulation with the interface.
+        '''
         # Add particles to line and build tracker
         line.particle_ref = xp.Particles(p0c=self.p_s, mass0=xp.PROTON_MASS_EV, q0=1.)
         line.build_tracker()
 
         # --- Convert the initial BLonD distribution to xsuite coordinates ---
-        zeta, ptau = blond_beam_to_xsuite_coords(beam,
-                                                 line.particle_ref.beta0[0],
-                                                 line.particle_ref.energy0[0],
-                                                 phi_s=blond_track.rf_params.phi_s[0],
-                                                 omega_rf=blond_track.rf_params.omega_rf[0, 0])
+        zeta, ptau = blond_beam_to_xsuite_coords(
+            beam, line.particle_ref.beta0[0], line.particle_ref.energy0[0],
+            phi_s=blond_track.rf_params.phi_s[0] - blond_track.rf_params.phi_rf[0, 0],
+            omega_rf=blond_track.rf_params.omega_rf[0, 0]
+        )
 
         # --- Track matrix ---
         N_t = len(blond_track.rf_params.phi_s) - 1
         particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
-        line.track(particles, num_turns=N_t, turn_by_turn_monitor=True, with_progress=True)
+        line.track(particles, num_turns=N_t, turn_by_turn_monitor=True, with_progress=False)
         mon = line.record_last_track
 
         dt_array = np.zeros(N_t)
@@ -465,6 +504,9 @@ class TestXsuitePSB(unittest.TestCase):
 
     @staticmethod
     def performSingleParticleBLonDSimulation(blond_track, beam):
+        r'''
+        Perform simulation in pure BLonD.
+        '''
         # The number of turns to track
         N_t = len(blond_track.rf_params.phi_s) - 1
 
@@ -475,8 +517,8 @@ class TestXsuitePSB(unittest.TestCase):
         full_tracker = blond_track
 
         for i in range(N_t):
-            full_tracker.track()
             dt_array[i] = beam.dt[0]
             dE_array[i] = beam.dE[0]
+            full_tracker.track()
 
         return dt_array, dE_array
\ No newline at end of file
-- 
GitLab


From 838a615a1c78c7345f0f9afe0b2159586c1387c4 Mon Sep 17 00:00:00 2001
From: slauber <simon.fabian.lauber@cern.ch>
Date: Thu, 7 Nov 2024 10:08:49 +0100
Subject: [PATCH 6/9] Added xsuite to the Dockerfile

---
 .../docker/images/python310-cuda/Dockerfile   | 33 +++++++++++++++++--
 1 file changed, 31 insertions(+), 2 deletions(-)

diff --git a/developer_tools/docker/images/python310-cuda/Dockerfile b/developer_tools/docker/images/python310-cuda/Dockerfile
index 92547389..e7c492a5 100644
--- a/developer_tools/docker/images/python310-cuda/Dockerfile
+++ b/developer_tools/docker/images/python310-cuda/Dockerfile
@@ -1,9 +1,29 @@
-ENTRYPOINT["/bin/bash"]
 FROM python:3.10
 
+# Create a non-root user
+RUN useradd -m non_root
+
+# Switch to root to install packages
+USER root
+
 # Set the environment variable to non-interactive
 ENV DEBIAN_FRONTEND=noninteractive
 
+#######################################################################################
+# Obtaining CERN rf-noise-cpp
+#######################################################################################
+# requires read api token !
+# 1. obtained token at https://gitlab.cern.ch/-/user_settings/personal_access_tokens
+# 2. export GITLAB_READ_TOKEN=<YOUR-TOKEN>
+# 3. Build your docker container
+RUN --mount=type=secret,id=gitlab_token \
+    GITLAB_READ_TOKEN=$(cat /run/secrets/gitlab_token) && \
+    git clone https://oauth2:$GITLAB_READ_TOKEN@gitlab.cern.ch/be-rf-cs/Tools-and-libs/rf-noise-cpp.git/ /usr/local/rf-noise-cpp
+RUN apt-get update && \
+    apt-get install -y libboost-all-dev
+ENV RF_NOISE_DIR=/usr/local/rf-noise-cpp
+#######################################################################################
+
 RUN apt-get update && \
     apt-get install -y gcc g++  build-essential mpich libmpich-dev
 RUN wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_64/cuda-keyring_1.1-1_all.deb && \
@@ -13,6 +33,7 @@ RUN wget https://developer.download.nvidia.com/compute/cuda/repos/debian12/x86_6
     apt-get install -y cuda-toolkit && \
     rm -rf /var/lib/apt/lists/*
 
+
 # Make sure NVCC is accessible
 ENV CUDA_HOME=/usr/local/cuda
 ENV PATH=${CUDA_HOME}/bin:${PATH}
@@ -20,5 +41,13 @@ ENV LD_LIBRARY_PATH=${CUDA_HOME}/lib64:$LD_LIBRARY_PATH
 
 
 RUN bash -c 'python3 -m pip install --upgrade pip&& \
-       python3 -m pip install cupy-cuda12x  pytest_cov mpi4py'
+       python3 -m pip install cupy-cuda12x  pytest_cov mpi4py xsuite'
+
+# Switch to the non-root user
+USER non_root
+
+# Set the working directory (optional, adjust as needed)
+WORKDIR /home/non_root
 
+# Set the entrypoint
+ENTRYPOINT ["/bin/bash"]
\ No newline at end of file
-- 
GitLab


From 6e2fc298498919dd78b04cdc38471927e4500c7b Mon Sep 17 00:00:00 2001
From: Birk Emil Karlsen-Baeck <birk.beck@cern.ch>
Date: Fri, 31 Jan 2025 13:00:41 +0100
Subject: [PATCH 7/9] Renamed the transformation functions and changed the
 examples accordingly

---
 ..._single_particle_with_oneturnmatrix_lhc.py | 24 +++++++++---------
 .../EX_24_single_particle_ramp_lhc.py         | 25 ++++++++++---------
 ...X_25_single_particle_two_rfstations_lhc.py | 24 +++++++++---------
 ...EX_26_single_particle_oneturnmatrix_psb.py | 25 ++++++++++---------
 .../EX_27_single_particle_ramp_psb.py         | 25 ++++++++++---------
 __EXAMPLES/main_files/EX_28_phase_loop_lhc.py | 25 ++++++++++---------
 blond/interfaces/xsuite.py                    | 19 ++++++++------
 7 files changed, 87 insertions(+), 80 deletions(-)

diff --git a/__EXAMPLES/main_files/EX_23_single_particle_with_oneturnmatrix_lhc.py b/__EXAMPLES/main_files/EX_23_single_particle_with_oneturnmatrix_lhc.py
index 9052aab0..6edecffd 100644
--- a/__EXAMPLES/main_files/EX_23_single_particle_with_oneturnmatrix_lhc.py
+++ b/__EXAMPLES/main_files/EX_23_single_particle_with_oneturnmatrix_lhc.py
@@ -23,8 +23,8 @@ from blond.trackers.tracker import RingAndRFTracker
 
 # Interface
 from blond.interfaces.xsuite import (BlondElement, BlondObserver,
-                                     blond_beam_to_xsuite_coords,
-                                     xsuite_coords_to_blond_coords)
+                                     blond_to_xsuite_transform,
+                                     xsuite_to_blond_transform)
 
 # Monitor objects
 from blond.monitors.monitors import BunchMonitor
@@ -149,11 +149,10 @@ line.get_table().show()
 print(f"\nSetting up simulation...")
 
 # --- Convert the initial BLonD distribution to xsuite coordinates ---
-zeta, ptau = blond_beam_to_xsuite_coords(beam, 
-                                         line.particle_ref.beta0[0],
-                                         line.particle_ref.energy0[0],
-                                         phi_s=rfstation.phi_s[0], 
-                                         omega_rf=rfstation.omega_rf[0, 0])
+zeta, ptau = blond_to_xsuite_transform(
+    beam.dt, beam.dE, line.particle_ref.beta0[0], line.particle_ref.energy0[0],
+    phi_s=rfstation.phi_s[0], omega_rf=rfstation.omega_rf[0, 0]
+)
 
 # --- Track matrix ---
 particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
@@ -179,11 +178,12 @@ xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.
 
 # Convert the xsuite particle coordinates back to BLonD
 for i in range(N_t):
-    dt, dE = xsuite_coords_to_blond_coords(mon.zeta[:, i].T, mon.ptau[:, i].T,
-                                           rfstation.beta[i],
-                                           rfstation.energy[i],
-                                           phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
-                                           omega_rf=rfstation.omega_rf[0, i])
+    dt, dE = xsuite_to_blond_transform(
+        mon.zeta[:, i].T, mon.ptau[:, i].T,
+        rfstation.beta[i], rfstation.energy[i],
+        phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
+        omega_rf=rfstation.omega_rf[0, i]
+    )
 
     # Statistics
     test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
diff --git a/__EXAMPLES/main_files/EX_24_single_particle_ramp_lhc.py b/__EXAMPLES/main_files/EX_24_single_particle_ramp_lhc.py
index a36d3809..cfaa4644 100644
--- a/__EXAMPLES/main_files/EX_24_single_particle_ramp_lhc.py
+++ b/__EXAMPLES/main_files/EX_24_single_particle_ramp_lhc.py
@@ -24,8 +24,8 @@ from blond.trackers.tracker import RingAndRFTracker
 
 # Interface
 from blond.interfaces.xsuite import (BlondElement, BlondObserver, EnergyUpdate,
-                                     blond_beam_to_xsuite_coords,
-                                     xsuite_coords_to_blond_coords)
+                                     blond_to_xsuite_transform,
+                                     xsuite_to_blond_transform)
 
 # Monitor objects
 from blond.monitors.monitors import BunchMonitor
@@ -157,11 +157,11 @@ line.get_table().show()
 print(f"\nSetting up simulation...")
 
 # --- Convert the initial BLonD distribution to xsuite coordinates ---
-zeta, ptau = blond_beam_to_xsuite_coords(beam, 
-                                         line.particle_ref.beta0[0],
-                                         line.particle_ref.energy0[0],
-                                         phi_s=rfstation.phi_s[0], 
-                                         omega_rf=rfstation.omega_rf[0, 0])
+zeta, ptau = blond_to_xsuite_transform(
+    beam.dt, beam.dE, line.particle_ref.beta0[0],
+    line.particle_ref.energy0[0], phi_s=rfstation.phi_s[0],
+    omega_rf=rfstation.omega_rf[0, 0]
+)
 
 # --- Track matrix ---
 particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
@@ -188,11 +188,12 @@ xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.
 
 # Convert the xsuite particle coordinates back to BLonD
 for i in range(N_t):
-    dt, dE = xsuite_coords_to_blond_coords(mon.zeta[:, i].T, mon.ptau[:, i].T,
-                                           rfstation.beta[i],
-                                           rfstation.energy[i],
-                                           phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
-                                           omega_rf=rfstation.omega_rf[0, i])
+    dt, dE = xsuite_to_blond_transform(
+        mon.zeta[:, i].T, mon.ptau[:, i].T,
+        rfstation.beta[i], rfstation.energy[i],
+        phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
+        omega_rf=rfstation.omega_rf[0, i]
+    )
 
     # Statistics
     test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
diff --git a/__EXAMPLES/main_files/EX_25_single_particle_two_rfstations_lhc.py b/__EXAMPLES/main_files/EX_25_single_particle_two_rfstations_lhc.py
index 41e6e88f..9103a32f 100644
--- a/__EXAMPLES/main_files/EX_25_single_particle_two_rfstations_lhc.py
+++ b/__EXAMPLES/main_files/EX_25_single_particle_two_rfstations_lhc.py
@@ -22,8 +22,8 @@ from blond.trackers.tracker import RingAndRFTracker
 
 # Interface
 from blond.interfaces.xsuite import (BlondElement,
-                                     blond_beam_to_xsuite_coords,
-                                     xsuite_coords_to_blond_coords)
+                                     blond_to_xsuite_transform,
+                                     xsuite_to_blond_transform)
 
 # Monitor objects
 from blond.monitors.monitors import BunchMonitor
@@ -176,11 +176,10 @@ line.get_table().show()
 print(f"\nSetting up simulation...")
 
 # --- Convert the initial BLonD distribution to xsuite coordinates ---
-zeta, ptau = blond_beam_to_xsuite_coords(beam, 
-                                         line.particle_ref.beta0[0],
-                                         line.particle_ref.energy0[0],
-                                         phi_s=rfstation_1.phi_s[0], 
-                                         omega_rf=rfstation_1.omega_rf[0, 0])
+zeta, ptau = blond_to_xsuite_transform(
+    beam.dt, beam.dE, line.particle_ref.beta0[0], line.particle_ref.energy0[0],
+    phi_s=rfstation_1.phi_s[0], omega_rf=rfstation_1.omega_rf[0, 0]
+)
 
 # --- Track matrix ---
 particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
@@ -207,11 +206,12 @@ xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.
 
 # Convert the xsuite particle coordinates back to BLonD
 for i in range(N_t):
-    dt, dE = xsuite_coords_to_blond_coords(mon.zeta[:,i].T, mon.ptau[:,i].T,
-                                           rfstation_1.beta[i],
-                                           rfstation_1.energy[i],
-                                           phi_s=rfstation_1.phi_s[i] - rfstation_1.phi_rf[0, 0],
-                                           omega_rf=rfstation_1.omega_rf[0, i])
+    dt, dE = xsuite_to_blond_transform(
+        mon.zeta[:,i].T, mon.ptau[:,i].T,
+        rfstation_1.beta[i], rfstation_1.energy[i],
+        phi_s=rfstation_1.phi_s[i] - rfstation_1.phi_rf[0, 0],
+        omega_rf=rfstation_1.omega_rf[0, i]
+    )
 
     # Statistics
     test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
diff --git a/__EXAMPLES/main_files/EX_26_single_particle_oneturnmatrix_psb.py b/__EXAMPLES/main_files/EX_26_single_particle_oneturnmatrix_psb.py
index 6ea893ad..845ffa3c 100644
--- a/__EXAMPLES/main_files/EX_26_single_particle_oneturnmatrix_psb.py
+++ b/__EXAMPLES/main_files/EX_26_single_particle_oneturnmatrix_psb.py
@@ -23,8 +23,8 @@ from blond.trackers.tracker import RingAndRFTracker
 
 # Interface
 from blond.interfaces.xsuite import (BlondElement,
-                                     blond_beam_to_xsuite_coords,
-                                     xsuite_coords_to_blond_coords)
+                                     blond_to_xsuite_transform,
+                                     xsuite_to_blond_transform)
 
 # Monitor objects
 from blond.monitors.monitors import BunchMonitor
@@ -153,11 +153,11 @@ line.get_table().show()
 print(f"\nSetting up simulation...")
 
 # --- Convert the initial BLonD distribution to xsuite coordinates ---
-zeta, ptau = blond_beam_to_xsuite_coords(beam, 
-                                         line.particle_ref.beta0[0],
-                                         line.particle_ref.energy0[0],
-                                         phi_s=rfstation.phi_s[0] - rfstation.phi_rf[0, 0],      # Correct for RF phase
-                                         omega_rf=rfstation.omega_rf[0, 0])
+zeta, ptau = blond_to_xsuite_transform(
+    beam.dt, beam.dE, line.particle_ref.beta0[0],
+    line.particle_ref.energy0[0], phi_s=rfstation.phi_s[0] - rfstation.phi_rf[0, 0],      # Correct for RF phase
+    omega_rf=rfstation.omega_rf[0, 0]
+)
 
 # --- Track matrix ---
 particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
@@ -184,11 +184,12 @@ xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.
 
 # Convert the xsuite particle coordinates back to BLonD
 for i in range(N_t):
-    dt, dE = xsuite_coords_to_blond_coords(mon.zeta[:, i].T, mon.ptau[:, i].T,
-                                           rfstation.beta[i],
-                                           rfstation.energy[i],
-                                           phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
-                                           omega_rf=rfstation.omega_rf[0, i])
+    dt, dE = xsuite_to_blond_transform(
+        mon.zeta[:, i].T, mon.ptau[:, i].T,
+        rfstation.beta[i], rfstation.energy[i],
+        phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
+        omega_rf=rfstation.omega_rf[0, i]
+    )
 
     # Statistics
     test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
diff --git a/__EXAMPLES/main_files/EX_27_single_particle_ramp_psb.py b/__EXAMPLES/main_files/EX_27_single_particle_ramp_psb.py
index b4707f0f..dd82280e 100644
--- a/__EXAMPLES/main_files/EX_27_single_particle_ramp_psb.py
+++ b/__EXAMPLES/main_files/EX_27_single_particle_ramp_psb.py
@@ -23,8 +23,8 @@ from blond.trackers.tracker import RingAndRFTracker
 
 # Interface
 from blond.interfaces.xsuite import (BlondElement, EnergyUpdate,
-                                     blond_beam_to_xsuite_coords,
-                                     xsuite_coords_to_blond_coords)
+                                     blond_to_xsuite_transform,
+                                     xsuite_to_blond_transform)
 
 # Monitor objects
 from blond.monitors.monitors import BunchMonitor
@@ -159,11 +159,11 @@ line.get_table().show()
 print(f"\nSetting up simulation...")
 
 # --- Convert the initial BLonD distribution to xsuite coordinates ---
-zeta, ptau = blond_beam_to_xsuite_coords(beam, 
-                                         line.particle_ref.beta0[0],
-                                         line.particle_ref.energy0[0],
-                                         phi_s=rfstation.phi_s[0] - rfstation.phi_rf[0, 0],  # Correct for RF phase
-                                         omega_rf=rfstation.omega_rf[0, 0])
+zeta, ptau = blond_to_xsuite_transform(
+    beam.dt, beam.dE, line.particle_ref.beta0[0],
+    line.particle_ref.energy0[0], phi_s=rfstation.phi_s[0] - rfstation.phi_rf[0, 0],  # Correct for RF phase
+    omega_rf=rfstation.omega_rf[0, 0]
+)
 
 # --- Track matrix ---
 particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
@@ -190,11 +190,12 @@ xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.
 
 # Convert the xsuite particle coordinates back to BLonD
 for i in range(N_t):
-    dt, dE = xsuite_coords_to_blond_coords(mon.zeta[:, i].T, mon.ptau[:, i].T,
-                                           rfstation.beta[i],
-                                           rfstation.energy[i],
-                                           phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
-                                           omega_rf=rfstation.omega_rf[0, i])
+    dt, dE = xsuite_to_blond_transform(
+        mon.zeta[:, i].T, mon.ptau[:, i].T,
+        rfstation.beta[i], rfstation.energy[i],
+        phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
+        omega_rf=rfstation.omega_rf[0, i]
+    )
 
     # Statistics
     test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
diff --git a/__EXAMPLES/main_files/EX_28_phase_loop_lhc.py b/__EXAMPLES/main_files/EX_28_phase_loop_lhc.py
index cefbbabd..bcb349ff 100644
--- a/__EXAMPLES/main_files/EX_28_phase_loop_lhc.py
+++ b/__EXAMPLES/main_files/EX_28_phase_loop_lhc.py
@@ -25,8 +25,8 @@ from blond.llrf.beam_feedback import BeamFeedback
 
 # Interface
 from blond.interfaces.xsuite import (BlondElement, BlondObserver,
-                                     blond_beam_to_xsuite_coords,
-                                     xsuite_coords_to_blond_coords)
+                                     blond_to_xsuite_transform,
+                                     xsuite_to_blond_transform)
 
 # Monitor objects
 from blond.monitors.monitors import BunchMonitor
@@ -167,11 +167,11 @@ line.get_table().show()
 print(f"\nSetting up simulation...")
 
 # --- Convert the initial BLonD distribution to xsuite coordinates ---
-zeta, ptau = blond_beam_to_xsuite_coords(beam, 
-                                         line.particle_ref.beta0[0],
-                                         line.particle_ref.energy0[0],
-                                         phi_s=rfstation.phi_s[0], 
-                                         omega_rf=rfstation.omega_rf[0, 0])
+zeta, ptau = blond_to_xsuite_transform(
+    beam.dt, beam.dE, line.particle_ref.beta0[0],
+    line.particle_ref.energy0[0], phi_s=rfstation.phi_s[0],
+    omega_rf=rfstation.omega_rf[0, 0]
+)
 
 # --- Track matrix ---
 particles = line.build_particles(x=0, y=0, px=0, py=0, zeta=np.copy(zeta), ptau=np.copy(ptau))
@@ -198,11 +198,12 @@ xsuite_string += '{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\n'.
 
 # Convert the xsuite particle coordinates back to BLonD
 for i in range(N_t):
-    dt, dE = xsuite_coords_to_blond_coords(mon.zeta[:, i].T, mon.ptau[:, i].T,
-                                           rfstation.beta[i],
-                                           rfstation.energy[i],
-                                           phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
-                                           omega_rf=rfstation.omega_rf[0, i])
+    dt, dE = xsuite_to_blond_transform(
+        mon.zeta[:, i].T, mon.ptau[:, i].T,
+        rfstation.beta[i], rfstation.energy[i],
+        phi_s=rfstation.phi_s[i] - rfstation.phi_rf[0, 0],  # Correct for RF phase
+        omega_rf=rfstation.omega_rf[0, i]
+    )
 
     # Statistics
     test_string += ('{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}\t{:+10.10e}'
diff --git a/blond/interfaces/xsuite.py b/blond/interfaces/xsuite.py
index 2a4260e1..bd30be38 100644
--- a/blond/interfaces/xsuite.py
+++ b/blond/interfaces/xsuite.py
@@ -21,13 +21,15 @@ if TYPE_CHECKING:
     from typing import Any, Union
 
 
-def blond_beam_to_xsuite_coords(beam: Beam, beta0: float, energy0: float,
-                                omega_rf: float, phi_s: float = 0):
+def blond_to_xsuite_transform(dt: Union[float, np.ndarray], de: Union[float, np.ndarray], beta0: float,
+                              energy0: float, omega_rf: float, phi_s: float = 0):
     r'''
     Coordinate transformation from Xsuite to BLonD at a given turn or multiple turns if numpy arrays ar given.
 
-    :param beam: blond.beam.beam.Beam class.
-        BLonD beam class to extract the initial coordinates dE and dt.
+    :param dt: float or NDArray.
+        The deviation in time from the reference clock in BLonD.
+    :param de: float or NDArray.
+        The deviation in energy from the synchronous particle.
     :param beta0: float.
         Synchronous beta.
     :param energy0: float.
@@ -40,13 +42,14 @@ def blond_beam_to_xsuite_coords(beam: Beam, beta0: float, energy0: float,
 
     :return zeta, ptau: as numpy-arrays (or single variable)
     '''
-    ptau = beam.dE / (beta0 * energy0)
-    zeta = -(beam.dt - phi_s / omega_rf) * beta0 * clight
+
+    ptau = de / (beta0 * energy0)
+    zeta = -(dt - phi_s / omega_rf) * beta0 * clight
     return zeta, ptau
 
 
-def xsuite_coords_to_blond_coords(zeta: Union[float, np.ndarray], ptau: Union[float, np.ndarray],
-                                  beta0: float, energy0: float, omega_rf: float, phi_s: float = 0):
+def xsuite_to_blond_transform(zeta: Union[float, np.ndarray], ptau: Union[float, np.ndarray],
+                              beta0: float, energy0: float, omega_rf: float, phi_s: float = 0):
     r'''
     Coordinate transformation from Xsuite to BLonD.
 
-- 
GitLab


From 03a13d1b23f0ff7567295b9c5dbd05ed30b4419a Mon Sep 17 00:00:00 2001
From: Birk Emil Karlsen-Baeck <birk.beck@cern.ch>
Date: Fri, 31 Jan 2025 13:01:33 +0100
Subject: [PATCH 8/9] [unittests][interfaces][xsuite] Added unittests for the
 coordinate transformations between BLonD and Xsuite

---
 unittests/interfaces/test_xsuite.py | 135 ++++++++++++++++++++++++----
 1 file changed, 120 insertions(+), 15 deletions(-)

diff --git a/unittests/interfaces/test_xsuite.py b/unittests/interfaces/test_xsuite.py
index d9709927..c5a0e57d 100644
--- a/unittests/interfaces/test_xsuite.py
+++ b/unittests/interfaces/test_xsuite.py
@@ -31,19 +31,124 @@ from blond.monitors.monitors import BunchMonitor
 
 from blond.interfaces.xsuite import (BlondElement, BlondObserver,
                                      EnergyUpdate,
-                                     blond_beam_to_xsuite_coords,
-                                     xsuite_coords_to_blond_coords)
-
-this_directory = os.path.dirname(os.path.realpath(__file__))
+                                     blond_to_xsuite_transform,
+                                     xsuite_to_blond_transform)
 
 # TODO: Make a test of the EnergyFrequencyUpdate and BlondObserver classes
 
 
 class TestXsuiteBLonDTransforms(unittest.TestCase):
-    # TODO: finish implementation of the test of the particle coordinates
+
     def setUp(self):
-        pass
+        # Accelerator parameters
+        C = 26658.8832
+        p_s = 450e9
+        h = 35640
+        alpha = 0.00034849575112251314
+        V = 5e6
+        dphi = 0
+
+        # BLonD ring and RF station
+        self.ring = Ring(C, alpha, p_s, Proton(), n_turns=1)
+        self.rfstation = RFStation(self.ring, [h], [V], [dphi])
+
+    def testBlondToXsuiteTransform(self):
+        # Arrays of time and energy offsets
+        dts = np.linspace(
+            self.rfstation.t_rf[0, 0] * 1e-3,
+            self.rfstation.t_rf[0, 0], 10
+        )
+        dEs = np.linspace(-200e6, 200e6, 10)
+
+        # Transform BLonD coordinates to Xsuite
+        zeta, ptau = blond_to_xsuite_transform(
+            dts, dEs, self.ring.beta[0, 0], self.ring.energy[0, 0],
+            self.rfstation.omega_rf[0, 0], self.rfstation.phi_s[0]
+        )
+
+        # Correct values
+        act_zeta = np.array(
+            [0.37325428,  0.29022578,  0.20719727,  0.12416876,  0.04114025,
+             -0.04188826, -0.12491676, -0.20794527, -0.29097378, -0.37400229]
+        )
+        act_ptau = np.array(
+            [-4.44444444e-04, -3.45679012e-04, -2.46913580e-04, -1.48148148e-04, -4.93827160e-05,
+             4.93827160e-05,  1.48148148e-04,  2.46913580e-04, 3.45679012e-04,  4.44444444e-04]
+        )
+
+        # Check if the coordinates are the same
+        np.testing.assert_allclose(
+            act_zeta, zeta,
+            err_msg='In testBlondToXsuiteTransform the zeta-coordinate differs'
+        )
+        np.testing.assert_allclose(
+            act_ptau, ptau,
+            err_msg='In testBlondToXsuiteTransform the ptau-coordinate differs'
+        )
+
+    def testXsuiteToBlondTransform(self):
+        # Arrays of z and ptau from xsuite
+        zeta = np.linspace(
+            -0.37, 0.37, 10
+        )
+        ptau = np.linspace(
+            -4e-4, 4e-4, 10
+        )
 
+        # Transform BLonD coordinates to Xsuite
+        dt, de = xsuite_to_blond_transform(
+            zeta, ptau, self.ring.beta[0, 0], self.ring.energy[0, 0],
+            self.rfstation.omega_rf[0, 0], self.rfstation.phi_s[0]
+        )
+
+        # Correct values
+        act_dt = np.array(
+            [2.48172990e-09, 2.20746549e-09, 1.93320108e-09, 1.65893668e-09, 1.38467227e-09,
+             1.11040786e-09, 8.36143453e-10, 5.61879046e-10, 2.87614638e-10, 1.33502300e-11]
+        )
+        act_de = np.array(
+            [-1.8e+08, -1.4e+08, -1.0e+08, -6.0e+07, -2.0e+07,
+             2.0e+07,  6.0e+07,  1.0e+08, 1.4e+08,  1.8e+08]
+        )
+
+        # Check if the coordinates are the same
+        np.testing.assert_allclose(
+            act_dt, dt,
+            err_msg='In testXsuiteToBlondTransform the time-coordinate differs'
+        )
+        np.testing.assert_allclose(
+            act_de, de,
+            err_msg='In testXsuiteToBlondTransform the energy-coordinate differs'
+        )
+
+    def testTransformInverse(self):
+        # Arrays of time and energy offsets
+        dts = np.linspace(
+            self.rfstation.t_rf[0, 0] * 1e-3,
+            self.rfstation.t_rf[0, 0], 20
+        )
+        dEs = np.linspace(-200e6, 200e6, 20)
+
+        # Perform transforms to xsuite coordinates and back
+        zeta, ptau = blond_to_xsuite_transform(
+            dts, dEs, self.ring.beta[0, 0], self.ring.energy[0, 0],
+            self.rfstation.omega_rf[0, 0], self.rfstation.phi_s[0]
+        )
+
+        _dts, _dEs = xsuite_to_blond_transform(
+            zeta, ptau, self.ring.beta[0, 0], self.ring.energy[0, 0],
+            self.rfstation.omega_rf[0, 0], self.rfstation.phi_s[0]
+        )
+
+        # Check if the coordinates are the same
+        np.testing.assert_allclose(
+            dts, _dts,
+            err_msg='In testTransformInverse the time-coordinate differs'
+        )
+        np.testing.assert_allclose(
+            dEs, _dEs,
+            err_msg='In testTransformInverse the energy-coordinate differs'
+        )
 
 class TestXsuiteLHC(unittest.TestCase):
 
@@ -273,11 +378,11 @@ class TestXsuiteLHC(unittest.TestCase):
         line.build_tracker()
 
         # --- Convert the initial BLonD distribution to xsuite coordinates ---
-        zeta, ptau = blond_beam_to_xsuite_coords(beam,
-                                                 line.particle_ref.beta0[0],
-                                                 line.particle_ref.energy0[0],
-                                                 phi_s=blond_track.rf_params.phi_s[0],
-                                                 omega_rf=blond_track.rf_params.omega_rf[0, 0])
+        zeta, ptau = blond_to_xsuite_transform(
+            beam.dt, beam.dE, line.particle_ref.beta0[0],
+            line.particle_ref.energy0[0], phi_s=blond_track.rf_params.phi_s[0],
+            omega_rf=blond_track.rf_params.omega_rf[0, 0]
+        )
 
         # --- Track matrix ---
         N_t = len(blond_track.rf_params.phi_s) - 1
@@ -289,7 +394,7 @@ class TestXsuiteLHC(unittest.TestCase):
         dE_array = np.zeros(N_t)
         # Convert the xsuite particle coordinates back to BLonD
         for i in range(N_t):
-            dt_array[i], dE_array[i] = xsuite_coords_to_blond_coords(
+            dt_array[i], dE_array[i] = xsuite_to_blond_transform(
                 mon.zeta[:, i].T, mon.ptau[:, i].T, blond_track.rf_params.beta[i], blond_track.rf_params.energy[i],
                 phi_s=blond_track.rf_params.phi_s[i] - blond_track.rf_params.phi_rf[0, 0],
                 omega_rf=blond_track.rf_params.omega_rf[0, i]
@@ -478,8 +583,8 @@ class TestXsuitePSB(unittest.TestCase):
         line.build_tracker()
 
         # --- Convert the initial BLonD distribution to xsuite coordinates ---
-        zeta, ptau = blond_beam_to_xsuite_coords(
-            beam, line.particle_ref.beta0[0], line.particle_ref.energy0[0],
+        zeta, ptau = blond_to_xsuite_transform(
+            beam.dt, beam.dE, line.particle_ref.beta0[0], line.particle_ref.energy0[0],
             phi_s=blond_track.rf_params.phi_s[0] - blond_track.rf_params.phi_rf[0, 0],
             omega_rf=blond_track.rf_params.omega_rf[0, 0]
         )
@@ -494,7 +599,7 @@ class TestXsuitePSB(unittest.TestCase):
         dE_array = np.zeros(N_t)
         # Convert the xsuite particle coordinates back to BLonD
         for i in range(N_t):
-            dt_array[i], dE_array[i] = xsuite_coords_to_blond_coords(
+            dt_array[i], dE_array[i] = xsuite_to_blond_transform(
                 mon.zeta[:, i].T, mon.ptau[:, i].T, blond_track.rf_params.beta[i], blond_track.rf_params.energy[i],
                 phi_s=blond_track.rf_params.phi_s[i] - blond_track.rf_params.phi_rf[0, 0],
                 omega_rf=blond_track.rf_params.omega_rf[0, i]
-- 
GitLab


From cf5d4ba55c7cff2e4233a4b3c678226aa1e7b073 Mon Sep 17 00:00:00 2001
From: slauber <simon.fabian.lauber@cern.ch>
Date: Tue, 11 Feb 2025 15:01:55 +0100
Subject: [PATCH 9/9] Fixed CI to not require admin privileges

---
 .gitlab-ci.yml | 1 -
 1 file changed, 1 deletion(-)

diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 364f6330..1d021d2a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -12,7 +12,6 @@ variables:
 
 .before:
   before_script:
-    - apt-get update
     # Verify CUDA installation
     - nvidia-smi
     - gcc --version
-- 
GitLab