diff --git a/CITATION.cff b/CITATION.cff
new file mode 100644
index 0000000000000000000000000000000000000000..1c6a56bc3885c651e681de0ba368ef21ec410ad7
--- /dev/null
+++ b/CITATION.cff
@@ -0,0 +1,26 @@
+cff-version: 1.2.0
+title: mbtrack2
+message: 'If you use this software, please cite it as below.'
+type: software
+authors:
+  - family-names: Gamelin
+    given-names: Alexis
+  - family-names: Foosang
+    given-names: Watanyu
+  - family-names: Yamomoto
+    given-names: Naoto
+  - family-names: Gubaidulin
+    given-names: Vadim
+  - family-names: Nagaoka
+    given-names: Ryutaro
+identifiers:
+  - type: doi
+    value: 10.5281/zenodo.14418989
+repository-code: >-
+  https://gitlab.synchrotron-soleil.fr/PA/collective-effects/mbtrack2
+abstract: >
+  mbtrack2 is a coherent object-oriented framework written
+  in python to work on collective effects in synchrotrons.
+license: BSD-3-Clause
+version: 0.8.0
+date-released: '2024-12-16'
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
index 18257107f17c4a6026d7dd4e3d99187cd914bd71..fe1acd4431445d6a901e71b9bd0834e150c67f9d 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -2,7 +2,5 @@ FROM gitlab-registry.synchrotron-soleil.fr/pa/collective-effects/python_mpi:late
 LABEL name mbtrack2
 USER dockeruser
 WORKDIR '/home/dockeruser'
-ENV PATH ${PATH}:/home/dockeruser/miniconda3/bin:/home/dockeruser/miniconda3/condabin
-RUN pip3 install accelerator-toolbox==0.5.0
 COPY --chown=dockeruser mbtrack2 /home/dockeruser/mbtrack2
 ENV PYTHONPATH=/home/dockeruser/
\ No newline at end of file
diff --git a/mbtrack2/instability/__init__.py b/mbtrack2/instability/__init__.py
index f7a1516f07a5ba85219041527106425accf715ce..15fd68680eb6632db29e1eaf0a90369ddc7f4db3 100644
--- a/mbtrack2/instability/__init__.py
+++ b/mbtrack2/instability/__init__.py
@@ -7,6 +7,7 @@ from mbtrack2.instability.instabilities import (
     mbi_threshold,
     rwmbi_growth_rate,
     rwmbi_threshold,
+    transverse_gaussian_space_charge_tune_shift,
 )
 from mbtrack2.instability.ions import (
     fast_beam_ion,
diff --git a/mbtrack2/instability/instabilities.py b/mbtrack2/instability/instabilities.py
index 0a198eb98c90e9f55f16dc5a0805faa948889837..c67bc2205ce9b1de9642ed7c18f4210d128be374 100644
--- a/mbtrack2/instability/instabilities.py
+++ b/mbtrack2/instability/instabilities.py
@@ -454,3 +454,82 @@ def rwmbi_threshold(ring, beff, rho_material, plane='x'):
         (1-frac_tune) * omega0) / (2*c*Z0*rho_material))**0.5
 
     return Ith
+
+
+def transverse_gaussian_space_charge_tune_shift(ring, bunch_current, **kwargs):
+    """
+    Return the (maximum) transverse space charge tune shift for a Gaussian 
+    bunch in the linear approximation, see Eq.(1) of [1].
+
+    Parameters
+    ----------
+    ring : Synchrotron object
+        Ring parameters.
+    bunch_current : float
+        Bunch current in [A].
+    sigma_s : float, optional
+        RMS bunch length in [s].
+        Default is ring.sigma_0.
+    emit_x : float, optional
+        Horizontal emittance in [m.rad].
+        Default is ring.emit[0].
+    emit_y : float, optional
+        Vertical emittance in [m.rad].
+        Default is ring.emit[1].
+    use_lattice : bool, optional
+        If True, use beta fonctions along the lattice.
+        If False, local values of beta fonctions are used.
+        Default is ring.optics.use_local_values.
+    n_points : int, optional
+        Number of points in the lattice to be considered if use_lattice ==
+        True. Default is 1000.
+    sigma_delta : float, optional
+        Relative energy spread.
+        Default is ring.sigma_delta.
+    gamma : float, optional
+        Relativistic Lorentz gamma.
+        Default is ring.gamma.
+
+    Returns
+    -------
+    tune_shift : array of shape (2,)
+        Horizontal and vertical space charge tune shift.
+        
+    Reference
+    ---------
+    [1] : Antipov, S. A., Gubaidulin, V., Agapov, I., Garcia, E. C., & 
+    Gamelin, A. (2024). Space Charge and Future Light Sources. 
+    arXiv preprint arXiv:2409.08637.
+
+    """
+    sigma_s = kwargs.get('sigma_s', ring.sigma_0)
+    emit_x = kwargs.get('emit_x', ring.emit[0])
+    emit_y = kwargs.get('emit_y', ring.emit[1])
+    use_lattice = kwargs.get('use_lattice', not ring.optics.use_local_values)
+    sigma_delta = kwargs.get('sigma_delta', ring.sigma_delta)
+    gamma = kwargs.get('gamma', ring.gamma)
+    n_points = kwargs.get('n_points', 1000)
+    q = ring.particle.charge
+    m = ring.particle.mass
+    r_0 = 1 / (4*pi*epsilon_0) * q**2 / (m * c**2)
+    N = np.abs(bunch_current / ring.f0 / q)
+    sigma_z = sigma_s * c
+
+    if use_lattice:
+        s = np.linspace(0, ring.L, n_points)
+        beta = ring.optics.beta(s)
+        sig_x = (emit_x * beta[0] +
+                 ring.optics.dispersion(s)[0]**2 * sigma_delta**2)**0.5
+        sig_y = (emit_y * beta[1] +
+                 ring.optics.dispersion(s)[2]**2 * sigma_delta**2)**0.5
+        sig_xy = np.array([sig_x, sig_y])
+        return -r_0 * N / ((2 * pi)**1.5 * gamma**3 * sigma_z) * np.trapz(
+            beta / (sig_xy * (sig_y+sig_x)), s)
+    else:
+        beta = ring.optics.local_beta
+        sig_x = np.sqrt(beta[0] * emit_x)
+        sig_y = np.sqrt(beta[1] * emit_y)
+        sig_xy = np.array([sig_x, sig_y])
+        return -r_0 * N * ring.L / (
+            (2 * pi)**1.5 * gamma**3 * sigma_z) * beta / (sig_xy *
+                                                          (sig_y+sig_x))
diff --git a/mbtrack2/tracking/beam_ion_effects.py b/mbtrack2/tracking/beam_ion_effects.py
index a041fee621da8f0249c792213eb0bf5e28ea3901..5e49ca50bad420935cb9df601373f1ca51d77323 100644
--- a/mbtrack2/tracking/beam_ion_effects.py
+++ b/mbtrack2/tracking/beam_ion_effects.py
@@ -6,7 +6,6 @@ IonMonitor
 IonAperture
 IonParticles
 """
-import warnings
 from abc import ABCMeta
 from functools import wraps
 from itertools import count
@@ -36,14 +35,18 @@ class IonMonitor(Monitor, metaclass=ABCMeta):
     total_size : int
         The total number of steps to be simulated.
     file_name : str, optional
-        The name of the HDF5 file to store the data. If not provided, a new file will be created. Defaults to None.
+        The name of the HDF5 file to store the data. If not provided, a new 
+        file will be created. 
+        Defaults to None.
 
     Methods
     -------
-    monitor_init(group_name, save_every, buffer_size, total_size, dict_buffer, dict_file, file_name=None, dict_dtype=None)
+    monitor_init(group_name, save_every, buffer_size, total_size, dict_buffer, 
+                 dict_file, file_name=None, dict_dtype=None)
         Initialize the monitor object.
     track(bunch)
         Tracking method for the element.
+
     Raises
     ------
     ValueError
@@ -56,16 +59,16 @@ class IonMonitor(Monitor, metaclass=ABCMeta):
     def __init__(self, save_every, buffer_size, total_size, file_name=None):
         group_name = "IonData_" + str(next(self._n_monitors))
         dict_buffer = {
-            "mean": (6, buffer_size),
-            "std": (6, buffer_size),
+            "mean": (4, buffer_size),
+            "std": (4, buffer_size),
             "charge": (buffer_size, ),
-            "charge_per_mp": (buffer_size, ),
+            "mp_number": (buffer_size, ),
         }
         dict_file = {
-            "mean": (6, total_size),
-            "std": (6, total_size),
+            "mean": (4, total_size),
+            "std": (4, total_size),
             "charge": (total_size, ),
-            "charge_per_mp": (buffer_size, ),
+            "mp_number": (buffer_size, ),
         }
         self.monitor_init(group_name, save_every, buffer_size, total_size,
                           dict_buffer, dict_file, file_name)
@@ -96,13 +99,18 @@ class IonMonitor(Monitor, metaclass=ABCMeta):
         total_size : int
             The total number of steps to be simulated.
         dict_buffer : dict
-            A dictionary containing the names and sizes of the attribute buffers.
+            A dictionary containing the names and sizes of the attribute 
+            buffers.
         dict_file : dict
-            A dictionary containing the names and shapes of the datasets to be created.
+            A dictionary containing the names and shapes of the datasets to be 
+            created.
         file_name : str, optional
-            The name of the HDF5 file to store the data. If not provided, a new file will be created. Defaults to None.
+            The name of the HDF5 file to store the data. If not provided, a new 
+            file will be created. 
+            Defaults to None.
         dict_dtype : dict, optional
-            A dictionary containing the names and data types of the datasets. Defaults to None.
+            A dictionary containing the names and data types of the datasets. 
+            Defaults to None.
 
         Raises
         ------
@@ -157,8 +165,10 @@ class IonAperture(ElipticalAperture):
     """
     Class representing an ion aperture.
 
-    Inherits from ElipticalAperture. Unlike in ElipticalAperture, ions are removed from IonParticles instead of just being flagged as not "alive".
-    For beam-ion simulations there are too many lost particles and it is better to remove them.
+    Inherits from ElipticalAperture. Unlike in ElipticalAperture, ions are 
+    removed from IonParticles instead of just being flagged as not "alive".
+    For beam-ion simulations there are too many lost particles and it is better 
+    to remove them.
 
     Attributes
     ----------
@@ -208,15 +218,33 @@ class IonParticles(Bunch):
     ring : Synchrotron class object
         The ring object representing the accelerator ring.
     track_alive : bool, optional
-        Flag indicating whether to track the alive particles. Default is False.
+        Flag indicating whether to track the alive particles. 
+        Default is False.
     alive : bool, optional
-        Flag indicating whether the particles are alive. Default is True.
+        Flag indicating whether the particles are alive. 
+        Default is True.
+        
+    Attributes
+    ----------
+    charge : float
+        Bunch charge in [C].
+    mean : array of shape (4,)
+        Charge-weighted mean values for x, xp, y and yp coordinates.
+    mean_xy : tuple of 2 float
+        Charge-weighted mean values for x and y coordinates.
+    std : array of shape (4,)
+        Charge-weighted std values for x, xp, y and yp coordinates.
+    mean_std_xy : tuple of 4 float
+        Charge-weighted mean and std values for x and y coordinates.
+        
     Methods:
     --------
-    generate_as_a_distribution(electron_bunch)
-        Generates the particle positions based on a normal distribution, taking distribution parameters from an electron bunch.
-    generate_from_random_samples(electron_bunch)
-        Generates the particle positions and times based on random samples from electron positions.
+    generate_as_a_distribution(electron_bunch, charge)
+        Generates the particle positions based on a normal distribution, taking 
+        distribution parameters from an electron bunch.
+    generate_from_random_samples(electron_bunch, charge)
+        Generates the particle positions and times based on random samples from 
+        electron positions.
     """
 
     def __init__(self,
@@ -242,6 +270,7 @@ class IonParticles(Bunch):
             "yp": np.zeros((mp_number, ), dtype=np.float64),
             "tau": np.zeros((mp_number, ), dtype=np.float64),
             "delta": np.zeros((mp_number, ), dtype=np.float64),
+            "charge": np.zeros((mp_number, ), dtype=np.float64),
         }
         self.charge_per_mp = 0
 
@@ -254,14 +283,73 @@ class IonParticles(Bunch):
     def mp_number(self, value):
         self._mp_number = int(value)
 
-    def generate_as_a_distribution(self, electron_bunch):
+    @property
+    def charge(self):
+        """Bunch charge in [C]"""
+        return self["charge"].sum()
+
+    def _mean_weighted(self, coord):
+        if self.charge == 0:
+            return np.zeros_like(coord, dtype=float)
+        else:
+            mean = [[np.average(self[name], weights=self["charge"])]
+                    for name in coord]
+            return np.squeeze(np.array(mean))
+
+    def _mean_std_weighted(self, coord):
+        if self.charge == 0:
+            return np.zeros_like(coord,
+                                 dtype=float), np.zeros_like(coord,
+                                                             dtype=float)
+        else:
+            mean = [[np.average(self[name], weights=self["charge"])]
+                    for name in coord]
+            var = []
+            for i, name in enumerate(coord):
+                var.append(
+                    np.average((self[name] - mean[i])**2,
+                               weights=self["charge"]))
+            var = np.array(var)
+            return np.squeeze(np.array(mean)), np.squeeze(np.sqrt(var))
+
+    @property
+    def mean(self):
+        """Return charge-weighted mean values for x, xp, y and yp coordinates."""
+        coord = ["x", "xp", "y", "yp"]
+        return self._mean_weighted(coord)
+
+    @property
+    def mean_xy(self):
+        """Return charge-weighted mean values for x and y coordinates."""
+        coord = ["x", "y"]
+        mean = self._mean_weighted(coord)
+        return mean[0], mean[1]
+
+    @property
+    def std(self):
+        """Return charge-weighted std values for x, xp, y and yp coordinates."""
+        coord = ["x", "xp", "y", "yp"]
+        _, std = self._mean_std_weighted(coord)
+        return std
+
+    @property
+    def mean_std_xy(self):
+        """Return charge-weighted mean and std values for x and y coordinates."""
+        coord = ["x", "y"]
+        mean, std = self._mean_std_weighted(coord)
+        return mean[0], mean[1], std[0], std[1]
+
+    def generate_as_a_distribution(self, electron_bunch, charge):
         """
-        Generates the particle positions based on a normal distribution, taking distribution parameters from an electron bunch.
+        Generates the particle positions based on a normal distribution, taking 
+        distribution parameters from an electron bunch.
 
         Parameters:
         ----------
         electron_bunch : Bunch
             An instance of the Bunch class representing the electron bunch.
+        charge : float
+            Total ion charge generated in [C].
         """
         if electron_bunch.is_empty:
             raise ValueError("Electron bunch is empty.")
@@ -289,15 +377,19 @@ class IonParticles(Bunch):
             high=self.ion_element_length / c,
             size=self.mp_number,
         )
+        self["charge"] = np.ones((self.mp_number, )) * charge / self.mp_number
 
-    def generate_from_random_samples(self, electron_bunch):
+    def generate_from_random_samples(self, electron_bunch, charge):
         """
-        Generates the particle positions and times based on random samples from electron positions in the bunch.
+        Generates the particle positions and times based on random samples from 
+        electron positions in the bunch.
 
         Parameters:
         ----------
         electron_bunch : Bunch
             An instance of the Bunch class representing the electron bunch.
+        charge : float
+            Total ion charge generated in [C].
         """
         if electron_bunch.is_empty:
             raise ValueError("Electron bunch is empty.")
@@ -316,6 +408,7 @@ class IonParticles(Bunch):
             high=self.ion_element_length / c,
             size=self.mp_number,
         )
+        self["charge"] = np.ones((self.mp_number, )) * charge / self.mp_number
 
     def __add__(self, new_particles):
         self.mp_number += new_particles.mp_number
@@ -330,47 +423,71 @@ class IonParticles(Bunch):
 class BeamIonElement(Element):
     """
     Represents an element for simulating beam-ion interactions.
+    
+    Apertures and monitors for the ion beam (instances of IonAperture and 
+    IonMonitor) can be added to tracking after the BeamIonElement object has 
+    been initialized by using:
+        beam_ion.apertures.append(aperture)
+        beam_ion.monitors.append(monitor)
+        
+    If the a IonMonitor object is used to record the ion beam, at the end of 
+    tracking the user should close the monitor, for example by calling:
+        beam_ion.monitors[0].close()
 
     Parameters
     ----------
     ion_mass : float
-        The mass of the ions in kg.
+        The mass of the ions in [kg].
     ion_charge : float
-        The charge of the ions in Coulomb.
+        The charge of the ions in [C].
     ionization_cross_section : float
-        The cross section of ionization in meters^2.
+        The cross section of ionization in [m^2].
     residual_gas_density : float
-        The residual gas density in meters^-3.
+        The residual gas density in [m^-3].
     ring : instance of Synchrotron()
         The ring.
-    ion_field_model : str
-        The ion field model, the options are 'weak' (acts on each macroparticle), 'strong' (acts on c.m.), 'PIC'.
+    ion_field_model : {'weak', 'strong', 'PIC'}
+        The ion field model used to update electron beam coordinates:
+            - 'weak': ion field acts on each macroparticle.
+            - 'strong': ion field acts on electron bunch c.m. 
+            - 'PIC': a PIC solver is used to get the ion electric field and the
+            result is interpolated on the electron bunch coordinates.
+        For both 'weak' and 'strong' models, the electric field is computed 
+        using the Bassetti-Erskine formula [1], so assuming a Gaussian beam 
+        distribution.
         For 'PIC' the PyPIC package is required.
-    electron_field_model : str
-        The electron field model, the options are 'weak', 'strong', 'PIC'.
+    electron_field_model : {'weak', 'strong', 'PIC'}
+        The electron field model, defined in the same way as ion_field_model.
     ion_element_length : float
-        The length of the beam-ion interaction region. For example, if only a single interaction point is used this should be equal to ring.L. 
-    x_radius : float
-        The x radius of the aperture.
-    y_radius : float
-        The y radius of the aperture.
-    n_steps : int
-        The number of records in the built-in ion beam monitor. Should be number of turns times number of bunches because the monitor records every turn after each bunch passage.
+        The length of the beam-ion interaction region. For example, if only a 
+        single interaction point is used this should be equal to ring.L. 
     n_ion_macroparticles_per_bunch : int, optional
-        The number of ion macroparticles generated per electron bunch passed. Defaults to 30.
-    ion_beam_monitor_name : str, optional
-         If provided, the name of the ion monitor output file. It must end with an extension '.hdf5'.
-         If None, no ion monitor file is generated.
-    use_ion_phase_space_monitor : bool, optional
-        Whether to use the ion phase space monitor.
-    generate_method : str, optional
-        The method to generate the ion macroparticles, the options are 'distribution', 'samples'. Defaults to 'distribution'. 
-        'distribution' generates a distribution statistically equivalent to the distribution of electrons. 
-        'samples' generates ions from random samples of electron positions.
+        The number of ion macroparticles generated per electron bunch passage. 
+        Defaults to 30.
+    generate_method : {'distribution', 'samples'}, optional
+        The method to generate the ion macroparticles:
+            - 'distribution' generates a distribution statistically equivalent 
+            to the distribution of electrons. 
+            - 'samples' generates ions from random samples of electron 
+            positions.
+        Defaults to 'distribution'.
+    generate_ions : bool, optional
+        If True, generate ions during BeamIonElement.track calls.
+        Default is True.
+    beam_ion_interaction : bool, optional
+        If True, update both beam and ion beam coordinate due to beam-ion 
+        interaction during BeamIonElement.track calls.
+        Default is True.
+    ion_drift : bool, optional
+        If True, update ion beam coordinate due to drift time between bunches.
+        Default is True.
 
     Methods
     -------
-    __init__(ion_mass, ion_charge, ionization_cross_section, residual_gas_density, ring, ion_field_model, electron_field_model, ion_element_length, n_steps, x_radius, y_radius, ion_beam_monitor_name=None, use_ion_phase_space_monitor=False, n_ion_macroparticles_per_bunch=30, generate_method='distribution')
+    __init__(ion_mass, ion_charge, ionization_cross_section, 
+             residual_gas_density, ring, ion_field_model, electron_field_model, 
+             ion_element_length, n_ion_macroparticles_per_bunch=30, 
+             generate_method='distribution')
         Initializes the BeamIonElement object.
     parallel(track)
         Defines the decorator @parallel to handle tracking of Beam() objects.
@@ -386,9 +503,13 @@ class BeamIonElement(Element):
     Raises
     ------
     UserWarning
-        If the BeamIonMonitor object is used, the user should call the close() method at the end of tracking.
-    NotImplementedError
-        If the ion phase space monitor is used.
+        If the BeamIonMonitor object is used, the user should call the close() 
+        method at the end of tracking.
+        
+    References
+    ----------
+    [1] : Bassetti, M., & Erskine, G. A. (1980). Closed expression for the 
+    electrical field of a two-dimensional Gaussian charge (No. ISR-TH-80-06).
     """
 
     def __init__(self,
@@ -400,16 +521,11 @@ class BeamIonElement(Element):
                  ion_field_model,
                  electron_field_model,
                  ion_element_length,
-                 n_steps,
-                 x_radius,
-                 y_radius,
-                 ion_beam_monitor_name=None,
-                 use_ion_phase_space_monitor=False,
                  n_ion_macroparticles_per_bunch=30,
-                 generate_method='distribution'):
-        if use_ion_phase_space_monitor:
-            raise NotImplementedError(
-                "Ion phase space monitor is not implemented.")
+                 generate_method='distribution',
+                 generate_ions=True,
+                 beam_ion_interaction=True,
+                 ion_drift=True):
         self.ring = ring
         self.bunch_spacing = ring.L / ring.h
         self.ion_mass = ion_mass
@@ -423,30 +539,18 @@ class BeamIonElement(Element):
         if not self.generate_method in ["distribution", "samples"]:
             raise ValueError("Wrong generate_method.")
         self.n_ion_macroparticles_per_bunch = n_ion_macroparticles_per_bunch
-        self.ion_beam_monitor_name = ion_beam_monitor_name
         self.ion_beam = IonParticles(
             mp_number=1,
             ion_element_length=self.ion_element_length,
             ring=self.ring)
-        self.ion_beam["x"] = 0
-        self.ion_beam["xp"] = 0
-        self.ion_beam["y"] = 0
-        self.ion_beam["yp"] = 0
-        self.ion_beam["tau"] = 0
-        self.ion_beam["delta"] = 0
-
-        if self.ion_beam_monitor_name:
-            warnings.warn(
-                'BeamIonMonitor.beam_monitor.close() should be called at the end of tracking',
-                UserWarning,
-                stacklevel=2)
-            self.beam_monitor = IonMonitor(
-                1,
-                int(n_steps / 10),
-                n_steps,
-                file_name=self.ion_beam_monitor_name)
-
-        self.aperture = IonAperture(X_radius=x_radius, Y_radius=y_radius)
+
+        self.generate_ions = generate_ions
+        self.beam_ion_interaction = beam_ion_interaction
+        self.ion_drift = ion_drift
+
+        # interfaces for apertures and montiors
+        self.apertures = []
+        self.monitors = []
 
     def parallel(track):
         """
@@ -514,16 +618,17 @@ class BeamIonElement(Element):
 
     def _get_efields(self, first_beam, second_beam, field_model):
         """
-        Calculates the electromagnetic field of the first beam acting on the second beam for a given field model.
+        Calculates the electromagnetic field of the second beam acting on the 
+        first beam for a given field model.
     
         Parameters
         ----------
         first_beam : IonParticles or Bunch
-            The first beam, represented as an instance of IonParticles() or Bunch().
+            The first beam, which is being acted on.
         second_beam : IonParticles or Bunch
-            The second beam, represented as an instance of IonParticles() or Bunch().
-        field_model : str, optional
-            The field model used for the interaction. Options are 'weak', 'strong', or 'PIC'.
+            The second beam, which is generating an electric field.
+        field_model : {'weak', 'strong', 'PIC'}
+            The field model used for the interaction.
     
         Returns
         -------
@@ -534,16 +639,19 @@ class BeamIonElement(Element):
         """
         if not field_model in ["weak", "strong", "PIC"]:
             raise ValueError(
-                f"The implementation for required beam-ion interaction model {field_model} is not implemented"
+                f"The implementation for required beam-ion interaction model \
+                {field_model} is not implemented")
+        if isinstance(second_beam, IonParticles):
+            sb_mx, sb_my, sb_stdx, sb_stdy = second_beam.mean_std_xy
+        else:
+            sb_mx, sb_stdx = (
+                second_beam["x"].mean(),
+                second_beam["x"].std(),
+            )
+            sb_my, sb_stdy = (
+                second_beam["y"].mean(),
+                second_beam["y"].std(),
             )
-        sb_mx, sb_stdx = (
-            second_beam["x"].mean(),
-            second_beam["x"].std(),
-        )
-        sb_my, sb_stdy = (
-            second_beam["y"].mean(),
-            second_beam["y"].std(),
-        )
         if field_model == "weak":
             en_x, en_y = get_displaced_efield(
                 _efieldn_mit,
@@ -556,10 +664,13 @@ class BeamIonElement(Element):
             )
 
         elif field_model == "strong":
-            fb_mx, fb_my = (
-                first_beam["x"].mean(),
-                first_beam["y"].mean(),
-            )
+            if isinstance(first_beam, IonParticles):
+                fb_mx, fb_my = first_beam.mean_xy
+            else:
+                fb_mx, fb_my = (
+                    first_beam["x"].mean(),
+                    first_beam["y"].mean(),
+                )
             en_x, en_y = get_displaced_efield(_efieldn_mit, fb_mx, fb_my,
                                               sb_stdx, sb_stdy, sb_mx, sb_my)
 
@@ -594,18 +705,19 @@ class BeamIonElement(Element):
                                prefactor,
                                field_model="strong"):
         """
-        Calculates the new momentum of the first beam due to the interaction with the second beam.
+        Calculates the new momentum of the first beam due to the interaction 
+        with the second beam.
         
         Parameters
         ----------
         first_beam : IonParticles or Bunch
-            The first beam, represented as an instance of IonParticles() or Bunch().
+            The first beam, which is being acted on.
         second_beam : IonParticles or Bunch
-            The second beam, represented as an instance of IonParticles() or Bunch().
+            The second beam, which is generating an electric field.
         prefactor : float
             A scaling factor applied to the calculation of the new momentum.
-        field_model : str
-            The field model used for the interaction. Options are 'weak', 'strong', or 'PIC'.
+        field_model : {'weak', 'strong', 'PIC'}
+            The field model used for the interaction.
             Default is "strong".
         
         Returns
@@ -635,7 +747,7 @@ class BeamIonElement(Element):
 
         Parameters
         ----------
-        electron_bunch : ElectronBunch
+        electron_bunch : Bunch
             The electron bunch used to generate new ions.
 
         Returns
@@ -647,18 +759,16 @@ class BeamIonElement(Element):
             ion_element_length=self.ion_element_length,
             ring=self.ring,
         )
+        new_ion_charge = (electron_bunch.charge *
+                          self.ionization_cross_section *
+                          self.residual_gas_density * self.ion_element_length)
         if self.generate_method == 'distribution':
             new_ion_particles.generate_as_a_distribution(
-                electron_bunch=electron_bunch)
+                electron_bunch=electron_bunch, charge=new_ion_charge)
         elif self.generate_method == 'samples':
             new_ion_particles.generate_from_random_samples(
-                electron_bunch=electron_bunch)
+                electron_bunch=electron_bunch, charge=new_ion_charge)
         self.ion_beam += new_ion_particles
-        self.ion_beam.charge_per_mp = (electron_bunch.charge *
-                                       self.ionization_cross_section *
-                                       self.residual_gas_density *
-                                       self.ion_element_length /
-                                       self.n_ion_macroparticles_per_bunch)
 
     @parallel
     def track(self, electron_bunch):
@@ -676,15 +786,16 @@ class BeamIonElement(Element):
         else:
             empty_bucket = False
 
-        if not empty_bucket:
+        if not empty_bucket and self.generate_ions:
             self.generate_new_ions(electron_bunch=electron_bunch)
 
-        self.aperture.track(self.ion_beam)
+        for aperture in self.apertures:
+            aperture.track(self.ion_beam)
 
-        if self.ion_beam_monitor_name is not None:
-            self.beam_monitor.track(self.ion_beam)
+        for monitor in self.monitors:
+            monitor.track(self.ion_beam)
 
-        if not empty_bucket:
+        if not empty_bucket and self.beam_ion_interaction:
             prefactor_to_ion_field = -self.ion_beam.charge / (self.ring.E0)
             prefactor_to_electron_field = -electron_bunch.charge * (
                 e / (self.ion_mass * c**2))
@@ -704,4 +815,5 @@ class BeamIonElement(Element):
             self._update_beam_momentum(electron_bunch, new_xp_electrons,
                                        new_yp_electrons)
 
-        self.track_ions_in_a_drift(drift_length=self.bunch_spacing)
+        if self.ion_drift:
+            self.track_ions_in_a_drift(drift_length=self.bunch_spacing)
diff --git a/mbtrack2/tracking/element.py b/mbtrack2/tracking/element.py
index 0f69f6cfb391776d5f747b2230fb9a9c374df895..1575a1973bc4a1ddedc33b0318f4f464f95ff051 100644
--- a/mbtrack2/tracking/element.py
+++ b/mbtrack2/tracking/element.py
@@ -415,7 +415,7 @@ class TransverseMap(TransverseMapSector):
                          2 * np.pi * ring.tune, ring.chro, ring.adts)
 
 
-def transverse_map_sector_generator(ring, positions):
+def transverse_map_sector_generator(ring, positions, **kwargs):
     """
     Convenience function which generate a list of TransverseMapSector elements
     from a ring:
@@ -435,6 +435,8 @@ def transverse_map_sector_generator(ring, positions):
         of the TransverseMapSector elements.
         The array should contain the initial position (s=0) but not the end
         position (s=ring.L), so like position = np.array([0, pos1, pos2, ...]).
+    
+    See at.physics.nonlinear.chromaticity for **kwargs
 
     Returns
     -------
@@ -463,8 +465,10 @@ def transverse_map_sector_generator(ring, positions):
                                     adts=adts))
     else:
         import at
+        dp = kwargs.get('dp', 1e-2)
+        order = kwargs.get('order', 1)
 
-        def _compute_chro(ring, N_sec, dp=1e-2, order=4):
+        def _compute_chro(ring, N_sec, dp, order):
             lat = deepcopy(ring.optics.lattice)
             lat.append(at.Marker("END"))
             fit, _, _ = at.physics.nonlinear.chromaticity(lat,
@@ -472,12 +476,18 @@ def transverse_map_sector_generator(ring, positions):
                                                           dpm=dp,
                                                           n_points=100,
                                                           order=order)
-            Chrox, Chroy = fit[0, 1:], fit[1, 1:]
-            chrox = np.array([np.linspace(0, val, N_sec) for val in Chrox])
-            chroy = np.array([np.linspace(0, val, N_sec) for val in Chroy])
-            return np.array([chrox, chroy])
+            chro_xy = [
+                elem for pair in zip(fit[0, 1:], fit[1, 1:]) for elem in pair
+            ]
+            len_chro = int(order * 2)
+            _chro = np.zeros((len_chro, N_sec))
+            for i in range(len_chro):
+                chro_order_splited = np.linspace(0, chro_xy[i], N_sec)
+                _chro[i, :] = chro_order_splited
+
+            return _chro
 
-        _chro = _compute_chro(ring, N_sec)
+        _chro = _compute_chro(ring, N_sec, dp, order)
         for i in range(N_sec):
             alpha0 = ring.optics.alpha(positions[i])
             beta0 = ring.optics.beta(positions[i])
diff --git a/mbtrack2/tracking/synchrotron.py b/mbtrack2/tracking/synchrotron.py
index 41458eff38988e909c1f3c90b2d4863c0a3e12ae..87717d06aec2dbed49728178571168faa5bc4bc0 100644
--- a/mbtrack2/tracking/synchrotron.py
+++ b/mbtrack2/tracking/synchrotron.py
@@ -373,23 +373,78 @@ class Synchrotron:
         tuneS = phi / (2 * np.pi)
         return tuneS
 
-    def get_adts(self):
+    def get_adts(self,
+                 xm=1e-4,
+                 ym=1e-4,
+                 npoints=9,
+                 plot=False,
+                 ax=None,
+                 **kwargs):
         """
         Compute and add Amplitude-Dependent Tune Shifts (ADTS) sextupolar
         componenet from AT lattice.
+        
+        Parameters
+        ----------
+        xm : float, optional
+            Maximum horizontal amplitude in [m].
+            Default is 1e-4.
+        ym : float, optional
+            Maximum vertical amplitude in [m].
+            Default is 1e-4.
+        npoints : int, optional
+            Number of points in each plane.
+            Default is 9.
+        plot : bool, optional
+            If True, plot the computed tune shift with amplitude.
+            Default is False.
+        ax : array of shape (2,) of matplotlib.axes.Axes, optional
+            Axes where to plot the figures.
+            Default is None.
+            
+        Returns
+        -------
+        ax : array of shape (2,) of matplotlib.axes.Axes
+            Only if plot is True.
+        
+        See at.physics.nonlinear.detuning for **kwargs
         """
         import at
         if self.optics.use_local_values:
             raise ValueError("ADTS needs to be provided manualy as no AT" +
                              " lattice file is loaded.")
-
-        det = at.physics.nonlinear.gen_detuning_elem(self.optics.lattice)
-        coef_xx = np.array([det.A1 / 2, 0])
-        coef_yx = np.array([det.A2 / 2, 0])
-        coef_xy = np.array([det.A2 / 2, 0])
-        coef_yy = np.array([det.A3 / 2, 0])
+        r0, r1, x, q_dx, y, q_dy = at.physics.nonlinear.detuning(
+            self.optics.lattice.radiation_off(copy=True),
+            npoints=npoints,
+            xm=xm,
+            ym=ym,
+            **kwargs)
+        coef_xx = np.array([r1[0][0] / 2, 0])
+        coef_yx = np.array([r1[0][1] / 2, 0])
+        coef_xy = np.array([r1[0][1] / 2, 0])
+        coef_yy = np.array([r1[1][1] / 2, 0])
         self.adts = [coef_xx, coef_yx, coef_xy, coef_yy]
 
+        if plot:
+            if ax is None:
+                fig, ax = plt.subplots(2, 1)
+
+            ax[0].scatter(x * 1e6, q_dx[:, 0])
+            ax[0].scatter(y * 1e6, q_dy[:, 0])
+            ax[0].set_ylabel("Delta Tune X")
+            ax[0].set_xlabel("Amplitude [um]")
+            ax[0].legend(['dqx/dx', 'dqx/dy'])
+            ax[0].grid()
+
+            ax[1].scatter(x * 1e6, q_dx[:, 1])
+            ax[1].scatter(y * 1e6, q_dy[:, 1])
+            ax[1].set_ylabel("Delta Tune Y")
+            ax[1].set_xlabel("Amplitude [um]")
+            ax[1].legend(['dqy/dx', 'dqy/dy'])
+            ax[1].grid()
+
+            return ax
+
     def get_chroma(self, order=4, dpm=0.02, n_points=100):
         """
         Compute chromaticity (linear and nonlinear) from AT lattice and update the property.
diff --git a/tests/unit/tracking/test_beam_ion_effects.py b/tests/unit/tracking/test_beam_ion_effects.py
index 920ca6caa66bd3450b6a52f5b833476d70d64c44..6f3c393b5d2d3c15b8536d44fd8bc1bf94c7edaa 100644
--- a/tests/unit/tracking/test_beam_ion_effects.py
+++ b/tests/unit/tracking/test_beam_ion_effects.py
@@ -31,39 +31,31 @@ def generate_ion_particles(demo_ring):
 class TestIonParticles:
 
     # Generate particle distribution using electron bunch parameters via generate_as_a_distribution()
-    def test_generate_as_distribution(self, generate_ion_particles,
-                                      large_bunch):
-        ions = generate_ion_particles(mp_number=1e5)
-        ions.generate_as_a_distribution(large_bunch)
-
-        assert np.isclose(ions["x"].mean(),
-                          large_bunch["x"].mean(),
-                          rtol=0.1,
-                          atol=1e-5)
-        assert np.isclose(ions["x"].std(),
-                          large_bunch["x"].std(),
-                          rtol=0.1,
-                          atol=1e-5)
-        assert np.isclose(ions["y"].mean(),
-                          large_bunch["y"].mean(),
-                          rtol=0.1,
-                          atol=1e-5)
-        assert np.isclose(ions["y"].std(),
-                          large_bunch["y"].std(),
-                          rtol=0.1,
-                          atol=1e-5)
+    def test_generate_as_distribution(self, generate_ion_particles, large_bunch):
+        mp_number = 1e5
+        charge = 1e-9
+        ions = generate_ion_particles(mp_number)
+        ions.generate_as_a_distribution(large_bunch, charge)
+    
+        assert np.isclose(ions["x"].mean(), large_bunch["x"].mean(), rtol=0.1, atol=1e-5)
+        assert np.isclose(ions["x"].std(), large_bunch["x"].std(), rtol=0.1, atol=1e-5)
+        assert np.isclose(ions["y"].mean(), large_bunch["y"].mean(), rtol=0.1, atol=1e-5)
+        assert np.isclose(ions["y"].std(), large_bunch["y"].std(), rtol=0.1, atol=1e-5)
         assert np.all(ions["xp"] == 0)
         assert np.all(ions["yp"] == 0)
         assert np.all(ions["delta"] == 0)
         assert np.all(ions["tau"] >= -ions.ion_element_length)
         assert np.all(ions["tau"] <= ions.ion_element_length)
+        assert np.isclose(ions.charge, charge)
+        assert np.isclose(ions["charge"].mean(), charge/mp_number, rtol=0.01, atol=1e-9)
 
     # Generate random particle samples from electron bunch via generate_from_random_samples()
-    def test_generate_from_random_samples(self, generate_ion_particles,
-                                          large_bunch):
-        ions = generate_ion_particles(mp_number=1e5)
-        ions.generate_from_random_samples(large_bunch)
-
+    def test_generate_from_random_samples(self, generate_ion_particles, large_bunch):
+        mp_number = 1e5
+        charge = 1e-9
+        ions = generate_ion_particles(mp_number)
+        ions.generate_from_random_samples(large_bunch, charge)
+    
         assert np.all(np.isin(ions["x"], large_bunch["x"]))
         assert np.all(np.isin(ions["y"], large_bunch["y"]))
         assert np.all(ions["xp"] == 0)
@@ -71,7 +63,9 @@ class TestIonParticles:
         assert np.all(ions["delta"] == 0)
         assert np.all(ions["tau"] >= -ions.ion_element_length)
         assert np.all(ions["tau"] <= ions.ion_element_length)
-
+        assert np.isclose(ions.charge, charge)
+        assert np.isclose(ions["charge"].mean(), charge/mp_number, rtol=0.01, atol=1e-9)
+        
     # Add two IonParticles instances together and verify combined particle arrays
     def test_add_ion_particles(self, generate_ion_particles):
         ions1 = generate_ion_particles(mp_number=100)
@@ -79,16 +73,14 @@ class TestIonParticles:
 
         combined = ions1 + ions2
         assert combined.mp_number == 150
-        assert all(combined[coord].shape == (150, )
-                   for coord in ["x", "xp", "y", "yp", "tau", "delta"])
+        assert all(combined[coord].shape == (150,) for coord in ["x","xp","y","yp","tau","delta","charge"])
         assert np.all(combined.alive == True)
 
     # Initialize with alive=False and verify all particles marked as dead
     def test_init_dead_particles(self, generate_ion_particles):
         ions = generate_ion_particles(alive=False)
         assert np.all(ions.alive == False)
-        assert all(ions[coord].shape == (1, )
-                   for coord in ["x", "xp", "y", "yp", "tau", "delta"])
+        assert all(ions[coord].shape == (1,) for coord in ["x","xp","y","yp","tau","delta","charge"])
 
     # Generate distributions with electron bunch containing no particles
     def test_generate_from_empty_bunch(self, generate_ion_particles,
@@ -97,11 +89,10 @@ class TestIonParticles:
         ions = generate_ion_particles()
 
         with pytest.raises(ValueError):
-            ions.generate_as_a_distribution(small_bunch)
+            ions.generate_as_a_distribution(small_bunch, 1e-9)
         with pytest.raises(ValueError):
-            ions.generate_from_random_samples(small_bunch)
-
-
+            ions.generate_from_random_samples(small_bunch, 1e-9)
+            
 @pytest.fixture
 def generate_ion_monitor(tmp_path):
 
@@ -147,11 +138,11 @@ class TestIonMonitor:
     # Data structures are properly initialized with correct shapes and types
     def test_data_structures_initialization(self, generate_ion_monitor):
         monitor = generate_ion_monitor()
-
-        assert monitor.mean.shape == (6, 10)
-        assert monitor.std.shape == (6, 10)
-        assert monitor.charge.shape == (10, )
-        assert monitor.time.shape == (10, )
+    
+        assert monitor.mean.shape == (4, 10)
+        assert monitor.std.shape == (4, 10)
+        assert monitor.charge.shape == (10,)
+        assert monitor.time.shape == (10,)
         assert monitor.time.dtype == int
 
     # Initialize monitor with total_size not divisible by buffer_size
@@ -221,23 +212,18 @@ class TestElipticalAperture:
 
 @pytest.fixture
 def generate_beam_ion(demo_ring):
-
-    def generate(ion_mass=1.67e-27,
-                 ion_charge=1.6e-19,
-                 ionization_cross_section=1e-22,
-                 residual_gas_density=1e50,
-                 ring=demo_ring,
-                 ion_field_model="strong",
-                 electron_field_model="strong",
-                 ion_element_length=demo_ring.L,
-                 n_steps=int(demo_ring.h * 10),
-                 x_radius=0.1,
-                 y_radius=0.1,
-                 ion_beam_monitor_name=None,
-                 use_ion_phase_space_monitor=False,
-                 n_ion_macroparticles_per_bunch=30,
-                 generate_method='samples'):
-
+    def generate(
+            ion_mass=1.67e-27,
+            ion_charge=1.6e-19,
+            ionization_cross_section=1e-22,
+            residual_gas_density=1e50,
+            ring=demo_ring,
+            ion_field_model="strong",
+            electron_field_model="strong",
+            ion_element_length=demo_ring.L,
+            n_ion_macroparticles_per_bunch=30,
+            generate_method='samples'):
+        
         beam_ion = BeamIonElement(
             ion_mass=ion_mass,
             ion_charge=ion_charge,
@@ -247,11 +233,6 @@ def generate_beam_ion(demo_ring):
             ion_field_model=ion_field_model,
             electron_field_model=electron_field_model,
             ion_element_length=ion_element_length,
-            n_steps=n_steps,
-            x_radius=x_radius,
-            y_radius=y_radius,
-            ion_beam_monitor_name=ion_beam_monitor_name,
-            use_ion_phase_space_monitor=use_ion_phase_space_monitor,
             n_ion_macroparticles_per_bunch=n_ion_macroparticles_per_bunch,
             generate_method=generate_method)
         return beam_ion
@@ -271,14 +252,18 @@ class TestBeamIonElement:
         assert_attr_changed(beam_ion, small_bunch, attrs_changed=["xp", "yp"])
 
     @pytest.mark.parametrize('ion_field_model, electron_field_model',
-                             [('weak', 'weak'), ('weak', 'strong'),
-                              ('strong', 'weak'), ('strong', 'strong')])
-    def test_track_bunch_partially_lost(self, generate_beam_ion, small_bunch,
-                                        ion_field_model, electron_field_model):
+                             [('weak','weak'), ('weak','strong'), 
+                              ('strong','weak'), ('strong', 'strong')])
+    def test_track_bunch_partially_lost(self, generate_beam_ion, small_bunch, ion_field_model, electron_field_model):
+        beam_ion = generate_beam_ion(ion_field_model=ion_field_model, electron_field_model=electron_field_model)
+        assert_attr_changed(beam_ion, small_bunch, attrs_changed=["xp","yp"])
+        charge_gen = beam_ion.ion_beam.charge
+        
+        # loose half of electron bunch
         small_bunch.alive[0:5] = False
-        beam_ion = generate_beam_ion(ion_field_model=ion_field_model,
-                                     electron_field_model=electron_field_model)
-        assert_attr_changed(beam_ion, small_bunch, attrs_changed=["xp", "yp"])
+        assert_attr_changed(beam_ion, small_bunch, attrs_changed=["xp","yp"])
+        
+        assert np.isclose(beam_ion.ion_beam.charge, charge_gen*1.5)
 
     @pytest.mark.parametrize('ion_field_model, electron_field_model',
                              [('weak', 'weak'), ('weak', 'strong'),
@@ -337,15 +322,20 @@ class TestBeamIonElement:
         assert np.allclose(beam_ion.ion_beam["y"], initial_y + drift_length)
 
     # Monitor records ion beam data at specified intervals when enabled
-    def test_monitor_recording(self, generate_beam_ion, small_bunch, tmp_path):
-        monitor_file = str(tmp_path / "test_monitor.hdf5")
-        with pytest.warns(UserWarning):
-            beam_ion = generate_beam_ion(ion_beam_monitor_name=monitor_file)
-
+    def test_monitor_recording(self, 
+                               generate_beam_ion, 
+                               small_bunch, 
+                               generate_ion_monitor,
+                               tmp_path):
+        file_name=tmp_path / "test_monitor.hdf5"
+        monitor = generate_ion_monitor(file_name=file_name)
+        beam_ion = generate_beam_ion()
+        beam_ion.monitors.append(monitor)
+    
         beam_ion.track(small_bunch)
-
-        assert os.path.exists(monitor_file)
-        with hp.File(monitor_file, 'r') as f:
+    
+        assert os.path.exists(file_name)
+        with hp.File(file_name, 'r') as f:
             cond = False
             for key in f.keys():
                 if key.startswith('IonData'):
@@ -363,14 +353,15 @@ class TestBeamIonElement:
     # Boundary conditions at aperture edges
     def test_aperture_boundary(self, generate_beam_ion, small_bunch):
         x_radius = 0.001
-        beam_ion = generate_beam_ion(x_radius=x_radius, y_radius=x_radius)
-
+        aperture = IonAperture(X_radius=x_radius, Y_radius=x_radius)
+        beam_ion = generate_beam_ion()
+        beam_ion.apertures.append(aperture)
+    
         beam_ion.generate_new_ions(small_bunch)
-
-        beam_ion.ion_beam["x"] = np.ones_like(
-            beam_ion.ion_beam["x"]) * (x_radius*1.1)
-        beam_ion.aperture.track(beam_ion.ion_beam)
-
+    
+        beam_ion.ion_beam["x"] = np.ones_like(beam_ion.ion_beam["x"]) * (x_radius * 1.1)
+        beam_ion.track(beam_ion.ion_beam)
+    
         assert len(beam_ion.ion_beam["x"]) == 0
 
     # Ion clearing removes all particles as expected
@@ -382,3 +373,23 @@ class TestBeamIonElement:
         beam_ion.clear_ions()
         assert len(beam_ion.ion_beam["x"]) == 1
         assert beam_ion.ion_beam["x"][0] == 0
+        
+    # Tracking with a pre-set cloud
+    def test_track_preset_cloud(self, generate_beam_ion, small_bunch, generate_ion_particles):
+        beam_ion = generate_beam_ion()
+        assert beam_ion.ion_beam.charge == 0
+        
+        preset_ion_particles = generate_ion_particles(mp_number=1000)
+        preset_charge = 1e-9
+        preset_ion_particles.generate_as_a_distribution(small_bunch, preset_charge)
+        beam_ion.ion_beam += preset_ion_particles
+        assert np.isclose(beam_ion.ion_beam.charge, preset_charge)
+        
+        beam_ion.generate_ions = False
+        assert_attr_changed(beam_ion, small_bunch, attrs_changed=["xp","yp"])
+        assert np.isclose(beam_ion.ion_beam.charge, preset_charge)
+        
+        beam_ion.generate_ions = True
+        assert_attr_changed(beam_ion, small_bunch, attrs_changed=["xp","yp"])
+        assert beam_ion.ion_beam.charge > preset_charge
+        
diff --git a/tests/unit/tracking/test_synchrotron.py b/tests/unit/tracking/test_synchrotron.py
index 252551f2a4ce1783182c15937b3138e18a31fac9..2a7dc4054cc84e8c5fac7fd79e95b257edf66dbe 100644
--- a/tests/unit/tracking/test_synchrotron.py
+++ b/tests/unit/tracking/test_synchrotron.py
@@ -142,7 +142,11 @@ class TestSynchrotron:
     def test_get_adts(self, ring_with_at_lattice):
         ring_with_at_lattice.get_adts()
         assert ring_with_at_lattice.adts is not None
-
+        
+    def test_get_adts_plot(self, ring_with_at_lattice):
+        axes = ring_with_at_lattice.get_adts(plot=True)
+        assert axes is not None
+        
     def test_get_chroma(self, ring_with_at_lattice):
         ring_with_at_lattice.get_chroma()
         assert len(ring_with_at_lattice.chro) == 8