From 70c221751bbfc59a85107d2d40acf353422d4cc6 Mon Sep 17 00:00:00 2001
From: Alexis GAMELIN <alexis.gamelin@synchrotron-soleil.fr>
Date: Wed, 4 Dec 2024 15:25:10 +0100
Subject: [PATCH] Add a test suite and fix some bugs.

Code modifications:

* [Fix] adts in TransverseMapSector
* ComplexData.name_and_coefficients_table is now a staticmethod.
* Extend WakeField to "mixed" componenets (xydip, yxdip, ...).
* Small fix/improvements for rf.py (CavityResonator for n_bin=1)
* [Fix] WakePotential.check_sampling method.
* [Fix] Sweep.plot method.
* Add Element.track_bunch_if_non_empty decorator.
* Modify Bunch.binning to add a return_full_length optional parameter.
* Modify Mpi.share_distributions for n_bin=1.
* Modify TransverseSpaceCharge to work with track_alive = True.
* Modify WakePotential for n_bin=1 and use track_bunch_if_non_empty decorator.
* [Fix] IntrabeamScattering for nonuniform fills with Beam.mpi_switch == False
* Modify PhysicalModel to return errors if needed.
* Update BeamIonElement

Project management:

* Update pyproject.toml and poetry.lock with pytest-mock
* Add assert_attr_changed test function.
---
 mbtrack2/impedance/wakefield.py               |   19 +-
 mbtrack2/tracking/__init__.py                 |   14 +-
 mbtrack2/tracking/beam_ion_effects.py         |  105 +-
 mbtrack2/tracking/element.py                  |   41 +-
 mbtrack2/tracking/excite.py                   |    4 +-
 mbtrack2/tracking/ibs.py                      |   13 +-
 mbtrack2/tracking/parallel.py                 |   28 +-
 mbtrack2/tracking/particles.py                |  126 +-
 mbtrack2/tracking/rf.py                       |   42 +-
 mbtrack2/tracking/spacecharge.py              |   22 +-
 mbtrack2/tracking/wakepotential.py            |   67 +-
 mbtrack2/utilities/optics.py                  |   18 +
 mbtrack2/utilities/spectrum.py                |    7 +-
 poetry.lock                                   | 1114 +++++++++--------
 pyproject.toml                                |    1 +
 tests/conftest.py                             |  109 +-
 .../{test_ibs.py => physics/test_ibs_phys.py} |   77 +-
 tests/test_bunch.py                           |  105 --
 tests/test_optics.py                          |   17 -
 tests/test_synchrotron.py                     |   63 -
 tests/unit/impedance/test_impedance_model.py  |  266 ++++
 tests/unit/impedance/test_wakefield.py        |  321 +++++
 tests/unit/tracking/test_aperture.py          |  181 +++
 tests/unit/tracking/test_beam_ion_effects.py  |  333 +++++
 tests/unit/tracking/test_element.py           |  360 ++++++
 tests/unit/tracking/test_emfields.py          |  102 ++
 tests/unit/tracking/test_excite.py            |   72 ++
 tests/unit/tracking/test_ibs.py               |   73 ++
 tests/unit/tracking/test_parallel.py          |   98 ++
 tests/unit/tracking/test_particle.py          |  306 +++++
 tests/unit/tracking/test_rf.py                |  420 +++++++
 tests/unit/tracking/test_spacecharge.py       |   37 +
 tests/unit/tracking/test_synchrotron.py       |  120 ++
 tests/unit/tracking/test_wakepotential.py     |  316 +++++
 tests/unit/utilities/test_optics.py           |  295 +++++
 tests/utility_test_functions.py               |   63 +
 36 files changed, 4369 insertions(+), 986 deletions(-)
 rename tests/{test_ibs.py => physics/test_ibs_phys.py} (67%)
 delete mode 100644 tests/test_bunch.py
 delete mode 100644 tests/test_optics.py
 delete mode 100644 tests/test_synchrotron.py
 create mode 100644 tests/unit/impedance/test_impedance_model.py
 create mode 100644 tests/unit/impedance/test_wakefield.py
 create mode 100644 tests/unit/tracking/test_aperture.py
 create mode 100644 tests/unit/tracking/test_beam_ion_effects.py
 create mode 100644 tests/unit/tracking/test_element.py
 create mode 100644 tests/unit/tracking/test_emfields.py
 create mode 100644 tests/unit/tracking/test_excite.py
 create mode 100644 tests/unit/tracking/test_ibs.py
 create mode 100644 tests/unit/tracking/test_parallel.py
 create mode 100644 tests/unit/tracking/test_particle.py
 create mode 100644 tests/unit/tracking/test_rf.py
 create mode 100644 tests/unit/tracking/test_spacecharge.py
 create mode 100644 tests/unit/tracking/test_synchrotron.py
 create mode 100644 tests/unit/tracking/test_wakepotential.py
 create mode 100644 tests/unit/utilities/test_optics.py
 create mode 100644 tests/utility_test_functions.py

diff --git a/mbtrack2/impedance/wakefield.py b/mbtrack2/impedance/wakefield.py
index ead052e..56c7e39 100644
--- a/mbtrack2/impedance/wakefield.py
+++ b/mbtrack2/impedance/wakefield.py
@@ -223,7 +223,8 @@ class ComplexData:
         self.d = component_coefficients["d"]
         self.plane = component_coefficients["plane"]
 
-    def name_and_coefficients_table(self):
+    @staticmethod
+    def name_and_coefficients_table():
         """
         Return a table associating the human readbale names of an impedance
         component and its associated coefficients and plane.
@@ -895,7 +896,8 @@ class WakeField:
         Return an array of the impedance component names for the element.
         """
         valid = [
-            "Zlong", "Zxdip", "Zydip", "Zxquad", "Zyquad", "Zxcst", "Zycst"
+            "Zlong", "Zxdip", "Zydip", "Zxquad", "Zyquad", "Zxcst", "Zycst",
+            "Zxydip", "Zyxdip", "Zxyquad", "Zyxquad"
         ]
         return np.array([comp for comp in dir(self) if comp in valid])
 
@@ -905,7 +907,8 @@ class WakeField:
         Return an array of the wake function component names for the element.
         """
         valid = [
-            "Wlong", "Wxdip", "Wydip", "Wxquad", "Wyquad", "Wxcst", "Wycst"
+            "Wlong", "Wxdip", "Wydip", "Wxquad", "Wyquad", "Wxcst", "Wycst",
+            "Wxydip", "Wyxdip", "Wxyquad", "Wyxquad"
         ]
         return np.array([comp for comp in dir(self) if comp in valid])
 
@@ -916,7 +919,9 @@ class WakeField:
         """
         valid = [
             "Wlong", "Wxdip", "Wydip", "Wxquad", "Wyquad", "Wxcst", "Wycst",
-            "Zlong", "Zxdip", "Zydip", "Zxquad", "Zyquad", "Zxcst", "Zycst"
+            "Wxydip", "Wyxdip", "Wxyquad", "Wyxquad", "Zlong", "Zxdip",
+            "Zydip", "Zxquad", "Zyquad", "Zxcst", "Zycst", "Zxydip", "Zyxdip",
+            "Zxyquad", "Zyxquad"
         ]
         return np.array([comp for comp in dir(self) if comp in valid])
 
@@ -1073,12 +1078,12 @@ class WakeField:
         if len(wakefields) == 1:
             return wakefields[0]
         elif len(wakefields) > 1:
-            wake_sum = WakeField.add_wakefields(wakefields[0], beta[:, 0],
-                                                wakefields[1], beta[:, 1])
+            wake_sum = WakeField.add_wakefields(wakefields[0], beta[0, :],
+                                                wakefields[1], beta[1, :])
             for i in range(len(wakefields) - 2):
                 wake_sum = WakeField.add_wakefields(wake_sum, [1, 1],
                                                     wakefields[i + 2],
-                                                    beta[:, i + 2])
+                                                    beta[i + 2, :])
             return wake_sum
         else:
             raise ValueError("Error in input.")
diff --git a/mbtrack2/tracking/__init__.py b/mbtrack2/tracking/__init__.py
index 2b4a60e..c18034b 100644
--- a/mbtrack2/tracking/__init__.py
+++ b/mbtrack2/tracking/__init__.py
@@ -20,12 +20,24 @@ from mbtrack2.tracking.element import (
     TransverseMapSector,
     transverse_map_sector_generator,
 )
+from mbtrack2.tracking.emfields import (
+    add_sigma_check,
+    efieldn_gauss_round,
+    get_displaced_efield,
+)
 from mbtrack2.tracking.excite import Sweep
 from mbtrack2.tracking.feedback import ExponentialDamper, FIRDamper
 from mbtrack2.tracking.ibs import IntrabeamScattering
 from mbtrack2.tracking.monitors import *
 from mbtrack2.tracking.parallel import Mpi
-from mbtrack2.tracking.particles import Beam, Bunch, Electron, Particle, Proton
+from mbtrack2.tracking.particles import (
+    Beam,
+    Bunch,
+    Electron,
+    Ion,
+    Particle,
+    Proton,
+)
 from mbtrack2.tracking.rf import (
     CavityResonator,
     DirectFeedback,
diff --git a/mbtrack2/tracking/beam_ion_effects.py b/mbtrack2/tracking/beam_ion_effects.py
index da354ba..a041fee 100644
--- a/mbtrack2/tracking/beam_ion_effects.py
+++ b/mbtrack2/tracking/beam_ion_effects.py
@@ -189,7 +189,7 @@ class IonAperture(ElipticalAperture):
         """
         alive = (bunch.particles["x"]**2 / self.X_radius_squared +
                  bunch.particles["y"]**2 / self.Y_radius_squared <= 1)
-        for stat in ['x', 'xp', 'y', 'yp', 'tau', 'delta']:
+        for stat in bunch:
             bunch.particles[stat] = bunch.particles[stat][alive]
         bunch.mp_number = len(bunch.particles['x'])
         bunch.alive = np.ones((bunch.mp_number, ))
@@ -226,7 +226,8 @@ class IonParticles(Bunch):
                  track_alive=False,
                  alive=True):
         self.ring = ring
-        self._mp_number = int(mp_number)
+        mp_number = int(mp_number)
+        self._mp_number = mp_number
         self.alive = np.ones((self.mp_number, ), dtype=bool)
         if not alive:
             self.alive = np.zeros((self.mp_number, ), dtype=bool)
@@ -262,6 +263,9 @@ class IonParticles(Bunch):
         electron_bunch : Bunch
             An instance of the Bunch class representing the electron bunch.
         """
+        if electron_bunch.is_empty:
+            raise ValueError("Electron bunch is empty.")
+
         self["x"], self["y"] = (
             normal(
                 loc=electron_bunch["x"].mean(),
@@ -295,6 +299,9 @@ class IonParticles(Bunch):
         electron_bunch : Bunch
             An instance of the Bunch class representing the electron bunch.
         """
+        if electron_bunch.is_empty:
+            raise ValueError("Electron bunch is empty.")
+
         self["x"], self["y"] = (
             choice(electron_bunch["x"], size=self.mp_number),
             choice(electron_bunch["y"], size=self.mp_number),
@@ -312,7 +319,7 @@ class IonParticles(Bunch):
 
     def __add__(self, new_particles):
         self.mp_number += new_particles.mp_number
-        for t in ["x", "xp", "y", "yp", "tau", "delta"]:
+        for t in new_particles:
             self.particles[t] = np.append(self.particles[t],
                                           new_particles.particles[t])
         self.alive = np.append(
@@ -341,8 +348,6 @@ class BeamIonElement(Element):
         For 'PIC' the PyPIC package is required.
     electron_field_model : str
         The electron field model, the options are 'weak', 'strong', 'PIC'.
-    bunch_spacing : float
-        The bunch spacing, the distance between bunches in meters. Used to propagate the ions in between bunches or in the gaps between the bunches.
     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
@@ -360,22 +365,22 @@ class BeamIonElement(Element):
         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.
+        'distribution' generates a distribution statistically equivalent to the distribution of electrons. 
+        'samples' generates ions from random samples of electron positions.
 
     Methods
     -------
-    __init__(self, ion_mass, ion_charge, ionization_cross_section, residual_gas_density, ring, ion_field_model, electron_field_model, bunch_spacing, 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_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')
         Initializes the BeamIonElement object.
     parallel(track)
         Defines the decorator @parallel to handle tracking of Beam() objects.
-    clear_ions(self)
+    clear_ions()
         Clear the ion particles in the ion beam.
-    track_ions_in_a_drift(self, drift_length)
+    track_ions_in_a_drift(drift_length)
         Tracks the ions in a drift.
-    generate_new_ions(self, electron_bunch)
+    generate_new_ions(electron_bunch)
         Generate new ions based on the given electron bunch.
-    track(self, electron_bunch)
+    track(electron_bunch)
         Beam-ion interaction kicks.
     
     Raises
@@ -394,7 +399,6 @@ class BeamIonElement(Element):
                  ring,
                  ion_field_model,
                  electron_field_model,
-                 bunch_spacing,
                  ion_element_length,
                  n_steps,
                  x_radius,
@@ -407,7 +411,7 @@ class BeamIonElement(Element):
             raise NotImplementedError(
                 "Ion phase space monitor is not implemented.")
         self.ring = ring
-        self.bunch_spacing = bunch_spacing
+        self.bunch_spacing = ring.L / ring.h
         self.ion_mass = ion_mass
         self.ionization_cross_section = ionization_cross_section
         self.residual_gas_density = residual_gas_density
@@ -416,7 +420,9 @@ class BeamIonElement(Element):
         self.ion_field_model = ion_field_model
         self.ion_element_length = ion_element_length
         self.generate_method = generate_method
-        self.n_ion_macroparticles_per_bunch = 30
+        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,
@@ -470,11 +476,10 @@ class BeamIonElement(Element):
                 self = args[0]
                 beam = args[1]
                 if beam.mpi_switch:
-                    warnings.warn(
-                        'Tracking through beam-ion element is performed sequentially. Bunches are not parallelized.',
-                        UserWarning,
-                        stacklevel=2)
-                for bunch in beam.bunch_list:
+                    raise ValueError(
+                        "Tracking through beam-ion element is performed sequentially."
+                    )
+                for bunch in beam:
                     track(self, bunch, *args[2:], **kwargs)
             else:
                 self = args[0]
@@ -488,7 +493,9 @@ class BeamIonElement(Element):
         Clear the ion particles in the ion beam.
         """
         self.ion_beam.particles = IonParticles(
-            mp_number=1, ion_element_length=self.ion_element_length)
+            mp_number=1,
+            ion_element_length=self.ion_element_length,
+            ring=self.ring)
 
     def track_ions_in_a_drift(self, drift_length):
         """
@@ -525,12 +532,10 @@ class BeamIonElement(Element):
         en_y : numpy.ndarray
             The y component of the electric field.
         """
-        assert field_model in [
-            "weak",
-            "strong",
-            "PIC",
-        ], "The implementation for required beam-ion interaction model {:} is not implemented".format(
-            self.interaction_model)
+        if not field_model in ["weak", "strong", "PIC"]:
+            raise ValueError(
+                f"The implementation for required beam-ion interaction model {field_model} is not implemented"
+            )
         sb_mx, sb_stdx = (
             second_beam["x"].mean(),
             second_beam["x"].std(),
@@ -666,29 +671,37 @@ class BeamIonElement(Element):
             An electron bunch to be interacted with.
         """
 
-        self.generate_new_ions(electron_bunch=electron_bunch)
+        if electron_bunch.is_empty:
+            empty_bucket = True
+        else:
+            empty_bucket = False
+
+        if not empty_bucket:
+            self.generate_new_ions(electron_bunch=electron_bunch)
 
         self.aperture.track(self.ion_beam)
 
         if self.ion_beam_monitor_name is not None:
             self.beam_monitor.track(self.ion_beam)
 
-        prefactor_to_ion_field = -self.ion_beam.charge / (self.ring.E0)
-        prefactor_to_electron_field = -electron_bunch.charge * (
-            e / (self.ion_mass * c**2))
-        new_xp_ions, new_yp_ions = self._get_new_beam_momentum(
-            self.ion_beam,
-            electron_bunch,
-            prefactor_to_electron_field,
-            field_model=self.electron_field_model,
-        )
-        new_xp_electrons, new_yp_electrons = self._get_new_beam_momentum(
-            electron_bunch,
-            self.ion_beam,
-            prefactor_to_ion_field,
-            field_model=self.ion_field_model,
-        )
-        self._update_beam_momentum(self.ion_beam, new_xp_ions, new_yp_ions)
-        self._update_beam_momentum(electron_bunch, new_xp_electrons,
-                                   new_yp_electrons)
+        if not empty_bucket:
+            prefactor_to_ion_field = -self.ion_beam.charge / (self.ring.E0)
+            prefactor_to_electron_field = -electron_bunch.charge * (
+                e / (self.ion_mass * c**2))
+            new_xp_ions, new_yp_ions = self._get_new_beam_momentum(
+                self.ion_beam,
+                electron_bunch,
+                prefactor_to_electron_field,
+                field_model=self.electron_field_model,
+            )
+            new_xp_electrons, new_yp_electrons = self._get_new_beam_momentum(
+                electron_bunch,
+                self.ion_beam,
+                prefactor_to_ion_field,
+                field_model=self.ion_field_model,
+            )
+            self._update_beam_momentum(self.ion_beam, new_xp_ions, new_yp_ions)
+            self._update_beam_momentum(electron_bunch, new_xp_electrons,
+                                       new_yp_electrons)
+
         self.track_ions_in_a_drift(drift_length=self.bunch_spacing)
diff --git a/mbtrack2/tracking/element.py b/mbtrack2/tracking/element.py
index 049f700..4c6d3dc 100644
--- a/mbtrack2/tracking/element.py
+++ b/mbtrack2/tracking/element.py
@@ -36,7 +36,7 @@ class Element(metaclass=ABCMeta):
     @staticmethod
     def parallel(track):
         """
-        Defines the decorator @parallel which handle the embarrassingly
+        Defines the decorator @parallel which handles the embarrassingly
         parallel case which happens when there is no bunch to bunch
         interaction in the tracking routine.
 
@@ -73,6 +73,39 @@ class Element(metaclass=ABCMeta):
 
         return track_wrapper
 
+    @staticmethod
+    def track_bunch_if_non_empty(track):
+        """
+        Defines the decorator @track_bunch_if_non_empty which handles the case 
+        where a track method should not be called if the bunch is empty.
+
+        Should be added only the track method defined for Bunch elements.
+
+        Parameters
+        ----------
+        track : function, method of an Element subclass
+            track method of an Element subclass which takes a Bunch object as
+            input
+
+        Returns
+        -------
+        track_wrapper: function, method of an Element subclass
+            track method of an Element subclass which takes a Bunch object as 
+            input
+            
+        """
+
+        @wraps(track)
+        def track_wrapper(*args):
+            #self = args[0]
+            bunch = args[1]
+            if bunch.is_empty:
+                pass
+            else:
+                track(*args)
+
+        return track_wrapper
+
 
 class LongitudinalMap(Element):
     """
@@ -272,7 +305,7 @@ class TransverseMapSector(Element):
                               self.chro_diff[5] / 6 * bunch["delta"]**3 +
                               self.chro_diff[7] / 24 * bunch["delta"]**4)
         else:
-            coefs = np.array([1 / factorial(i + 1) for i in range(order + 1)])
+            coefs = np.array([1 / factorial(i) for i in range(order + 1)])
             coefs[0] = 0
             self.chro_diff = np.concatenate(([0, 0], self.chro_diff))
             tune_advance_x = np.polynomial.polynomial.Polynomial(
@@ -328,13 +361,13 @@ class TransverseMapSector(Element):
 
         if self.adts_poly is not None:
             Jx = ((self.gamma0[0] * bunch["x"]**2) +
-                  (2 * self.alpha0[0] * bunch["x"] * self["xp"]) +
+                  (2 * self.alpha0[0] * bunch["x"] * bunch["xp"]) +
                   (self.beta0[0] * bunch["xp"]**2))
             Jy = ((self.gamma0[1] * bunch["y"]**2) +
                   (2 * self.alpha0[1] * bunch["y"] * bunch["yp"]) +
                   (self.beta0[1] * bunch["yp"]**2))
             tune_advance_x += (self.adts_poly[0](Jx) + self.adts_poly[2](Jy))
-            tune_advance_x += (self.adts_poly[0](Jx) + self.adts_poly[2](Jy))
+            tune_advance_y += (self.adts_poly[1](Jx) + self.adts_poly[3](Jy))
 
         bunch['x'], bunch['xp'] = self._compute_new_coords(
             bunch, tune_advance_x, 'x')
diff --git a/mbtrack2/tracking/excite.py b/mbtrack2/tracking/excite.py
index 818fceb..7afa3f3 100644
--- a/mbtrack2/tracking/excite.py
+++ b/mbtrack2/tracking/excite.py
@@ -113,6 +113,6 @@ class Sweep(Element):
         """Plot the sweep voltage applied."""
         fig, ax = plt.subplots()
         ax.plot(self.t, self.sweep)
-        ax.xlabel("Time [s]")
-        ax.ylabel("Sweep voltage [V]")
+        ax.set_xlabel("Time [s]")
+        ax.set_ylabel("Sweep voltage [V]")
         return fig
diff --git a/mbtrack2/tracking/ibs.py b/mbtrack2/tracking/ibs.py
index 13e7b74..a02efd0 100644
--- a/mbtrack2/tracking/ibs.py
+++ b/mbtrack2/tracking/ibs.py
@@ -93,9 +93,8 @@ class IntrabeamScattering(Element):
 
     """
 
-    def __init__(self, ring, bunch, model, n_points=1000, n_bin=100):
+    def __init__(self, ring, model, n_points=1000, n_bin=100):
         self.ring = ring
-        self.bunch = bunch
         self.n_points = int(n_points)
         self.n_bin = int(n_bin)
         self.s = np.linspace(0, self.ring.L, self.n_points)
@@ -134,7 +133,7 @@ class IntrabeamScattering(Element):
         bunch: Bunch or Beam object
             Bunch or Beam object which will be tracked.
         """
-        self.N = self.bunch.current * self.ring.T0 / elementary_charge
+        self.N = bunch.current * self.ring.T0 / elementary_charge
         self.d = 4 * np.std(bunch['y'])
         self.sigma_s = np.std(bunch['tau'])
         self.sigma_p = np.std(bunch['delta'])
@@ -412,21 +411,23 @@ class IntrabeamScattering(Element):
         else:
             Rho = 1.0
 
+        N_mp = len(bunch)
         Delta_pz = self.sigma_p * np.sqrt(
             np.sqrt(2) * T_p * self.ring.T0 *
-            Rho) * np.random.normal(size=bunch.mp_number)
+            Rho) * np.random.normal(size=N_mp)
         Delta_px = self.sigma_px * np.sqrt(
             np.sqrt(2) * T_x * self.ring.T0 *
-            Rho) * np.random.normal(size=bunch.mp_number)
+            Rho) * np.random.normal(size=N_mp)
         Delta_py = self.sigma_py * np.sqrt(
             np.sqrt(2) * T_y * self.ring.T0 *
-            Rho) * np.random.normal(size=bunch.mp_number)
+            Rho) * np.random.normal(size=N_mp)
 
         bunch['xp'] += Delta_px
         bunch['yp'] += Delta_py
         bunch['delta'] += Delta_pz
 
     @Element.parallel
+    @Element.track_bunch_if_non_empty
     def track(self, bunch):
         """
         Tracking method of IntrabeamScattering takes T_(x,y,p) and apply 
diff --git a/mbtrack2/tracking/parallel.py b/mbtrack2/tracking/parallel.py
index 6ef1846..a441f2d 100644
--- a/mbtrack2/tracking/parallel.py
+++ b/mbtrack2/tracking/parallel.py
@@ -64,9 +64,9 @@ class Mpi:
         self.MPI = MPI
         self.comm = MPI.COMM_WORLD
         self.rank = self.comm.Get_rank()
-        if self.rank != 0:
-            sys.stdout = open('/dev/null', 'w')
-            sys.stderr = open('/dev/null', 'w')
+        # if self.rank != 0:
+        #     sys.stdout = open('/dev/null', 'w')
+        #     sys.stderr = open('/dev/null', 'w')
         self.size = self.comm.Get_size()
         self.write_table(filling_pattern)
 
@@ -180,31 +180,39 @@ class Mpi:
             dim = dimensions[i]
             n = n_bin[i]
 
-            if len(bunch) != 0:
+            if not bunch.is_empty:
                 bins, sorted_index, profile, center = bunch.binning(
                     dimension=dim, n_bin=n)
+                bin_length = bins[1] - bins[0]
             else:
                 sorted_index = None
-                profile = np.zeros((n - 1, ), dtype=np.int64)
-                center = np.zeros((n - 1, ), dtype=np.float64)
+                profile = np.zeros((n, ), dtype=np.int64)
+                center = np.zeros((n, ), dtype=np.float64)
+                bin_length = np.zeros((1, ), dtype=np.float64)
                 if beam.filling_pattern[self.bunch_num] is True:
                     beam.update_filling_pattern()
                     beam.update_distance_between_bunches()
 
             self.__setattr__(dim + "_center",
-                             np.empty((self.size, n - 1), dtype=np.float64))
+                             np.empty((self.size, n), dtype=np.float64))
             self.comm.Allgather(
                 [center, self.MPI.DOUBLE],
                 [self.__getattribute__(dim + "_center"), self.MPI.DOUBLE])
 
             self.__setattr__(dim + "_profile",
-                             np.empty((self.size, n - 1), dtype=np.int64))
+                             np.empty((self.size, n), dtype=np.int64))
             self.comm.Allgather(
                 [profile, self.MPI.INT64_T],
                 [self.__getattribute__(dim + "_profile"), self.MPI.INT64_T])
 
             self.__setattr__(dim + "_sorted_index", sorted_index)
 
+            self.__setattr__(dim + "_bin_length",
+                             np.empty((self.size, 1), dtype=np.float64))
+            self.comm.Allgather(
+                [bin_length, self.MPI.DOUBLE],
+                [self.__getattribute__(dim + "_bin_length"), self.MPI.DOUBLE])
+
     def share_means(self, beam):
         """
         Compute the bunch means and share it between the different bunches.
@@ -224,7 +232,7 @@ class Mpi:
         self.charge_all = charge_all
 
         self.mean_all = np.empty((self.size, 6), dtype=np.float64)
-        if len(bunch) != 0:
+        if not bunch.is_empty:
             mean = bunch.mean
         else:
             mean = np.zeros((6, ), dtype=np.float64)
@@ -250,7 +258,7 @@ class Mpi:
         self.charge_all = charge_all
 
         self.std_all = np.empty((self.size, 6), dtype=np.float64)
-        if len(bunch) != 0:
+        if not bunch.is_empty:
             std = bunch.std
         else:
             std = np.zeros((6, ), dtype=np.float64)
diff --git a/mbtrack2/tracking/particles.py b/mbtrack2/tracking/particles.py
index 2b3edbb..0deea66 100644
--- a/mbtrack2/tracking/particles.py
+++ b/mbtrack2/tracking/particles.py
@@ -144,6 +144,13 @@ class Bunch:
         if not alive:
             mp_number = 1
             current = 0
+            track_alive = True
+
+        if current == 0 and track_alive == False:
+            raise ValueError(
+                "Can not initialize empty bunch with track_alive=False, use the empty=True keyword."
+            )
+
         self._mp_number = int(mp_number)
 
         self.particles = {
@@ -165,7 +172,10 @@ class Bunch:
 
     def __len__(self):
         """Return the number of alive particles"""
-        return self.alive.sum()
+        if self.track_alive is True:
+            return self.alive.sum()
+        else:
+            return self.mp_number
 
     def __getitem__(self, label):
         """Return the columns label for alive particles"""
@@ -240,7 +250,10 @@ class Bunch:
     @property
     def is_empty(self):
         """Return True if the bunch is empty."""
-        return ~np.any(self.alive)
+        if self.track_alive is True:
+            return ~np.any(self.alive)
+        else:
+            return False
 
     @property
     def mean(self):
@@ -282,38 +295,60 @@ class Bunch:
         """
         Return the bunch emittance for each plane.
         """
-
-        cov_x = np.cov(self['x'], self['xp'])
-        cov_y = np.cov(self['y'], self['yp'])
-        cov_z = np.cov(self['tau'], self['delta'])
-
-        if (np.array(self.ring.optics.local_dispersion)
-                != np.array([0, 0, 0, 0])).any():
-            cov_xdelta = np.cov(self['x'], self['delta'])
-            cov_xpdelta = np.cov(self['xp'], self['delta'])
-            cov_ydelta = np.cov(self['y'], self['delta'])
-            cov_ypdelta = np.cov(self['yp'], self['delta'])
-
-            sig11 = cov_x[
-                0, 0] - cov_xdelta[0, 1] * cov_xdelta[0, 1] / cov_z[1, 1]
-            sig12 = cov_x[
-                0, 1] - cov_xdelta[0, 1] * cov_xpdelta[0, 1] / cov_z[1, 1]
-            sig22 = cov_x[
-                1, 1] - cov_xpdelta[0, 1] * cov_xpdelta[0, 1] / cov_z[1, 1]
-            emitX = np.sqrt(sig11*sig22 - sig12*sig12)
-
-            sig11 = cov_y[
-                0, 0] - cov_ydelta[0, 1] * cov_ydelta[0, 1] / cov_z[1, 1]
-            sig12 = cov_y[
-                0, 1] - cov_ydelta[0, 1] * cov_ypdelta[0, 1] / cov_z[1, 1]
-            sig22 = cov_y[
-                1, 1] - cov_ypdelta[0, 1] * cov_ypdelta[0, 1] / cov_z[1, 1]
-            emitY = np.sqrt(sig11*sig22 - sig12*sig12)
+        if len(self) > 1:
+            cov_x = np.cov(self['x'], self['xp'])
+            cov_y = np.cov(self['y'], self['yp'])
+            cov_z = np.cov(self['tau'], self['delta'])
+
+            if (np.array(self.ring.optics.local_dispersion)
+                    != np.array([0, 0, 0, 0])).any():
+                cov_xdelta = np.cov(self['x'], self['delta'])
+                cov_xpdelta = np.cov(self['xp'], self['delta'])
+                cov_ydelta = np.cov(self['y'], self['delta'])
+                cov_ypdelta = np.cov(self['yp'], self['delta'])
+
+                sig11 = cov_x[
+                    0, 0] - cov_xdelta[0, 1] * cov_xdelta[0, 1] / cov_z[1, 1]
+                sig12 = cov_x[
+                    0, 1] - cov_xdelta[0, 1] * cov_xpdelta[0, 1] / cov_z[1, 1]
+                sig22 = cov_x[
+                    1, 1] - cov_xpdelta[0, 1] * cov_xpdelta[0, 1] / cov_z[1, 1]
+                with np.errstate(invalid='raise'):
+                    try:
+                        emitX = np.sqrt(sig11*sig22 - sig12*sig12)
+                    except FloatingPointError:
+                        emitX = 0
+
+                sig11 = cov_y[
+                    0, 0] - cov_ydelta[0, 1] * cov_ydelta[0, 1] / cov_z[1, 1]
+                sig12 = cov_y[
+                    0, 1] - cov_ydelta[0, 1] * cov_ypdelta[0, 1] / cov_z[1, 1]
+                sig22 = cov_y[
+                    1, 1] - cov_ypdelta[0, 1] * cov_ypdelta[0, 1] / cov_z[1, 1]
+                with np.errstate(invalid='raise'):
+                    try:
+                        emitY = np.sqrt(sig11*sig22 - sig12*sig12)
+                    except FloatingPointError:
+                        emitY = 0
+            else:
+                with np.errstate(invalid='raise'):
+                    try:
+                        emitX = np.sqrt(np.linalg.det(cov_x))
+                    except FloatingPointError:
+                        emitX = 0
+                    try:
+                        emitY = np.sqrt(np.linalg.det(cov_y))
+                    except FloatingPointError:
+                        emitY = 0
+
+            with np.errstate(invalid='raise'):
+                try:
+                    emitS = np.sqrt(np.linalg.det(cov_z))
+                except FloatingPointError:
+                    emitS = 0
         else:
-            emitX = np.sqrt(np.linalg.det(cov_x))
-            emitY = np.sqrt(np.linalg.det(cov_y))
+            emitX, emitY, emitS = 0, 0, 0
 
-        emitS = np.sqrt(np.linalg.det(cov_z))
         return np.array([emitX, emitY, emitS])
 
     @property
@@ -412,7 +447,7 @@ class Bunch:
         self.particles["tau"] = values[:, 4]
         self.particles["delta"] = values[:, 5]
 
-    def binning(self, dimension="tau", n_bin=75):
+    def binning(self, dimension="tau", n_bin=75, return_full_length=False):
         """
         Bin macro-particles.
 
@@ -422,16 +457,21 @@ class Bunch:
             Dimension in which the binning is done. The default is "tau".
         n_bin : int, optional
             Number of bins. The default is 75.
+        return_full_length : bool, optional
+            If True (and self.track_alive), also assign bins to untracked 
+            macro-particles.
+            The returned sorted_index has shape (self.mp_number,).
+            Default is False.
 
         Returns
         -------
-        bins : array of shape (n_bin,)
+        bins : array of shape (n_bin + 1,)
             Bins where the particles are sorted.
-        sorted_index : array of shape (self.mp_number,)
+        sorted_index : array of shape (len(self),) or (self.mp_number,)
             Bin number of each macro-particles.
-        profile : array of shape (n_bin - 1,)
+        profile : array of shape (n_bin,)
             Number of marco-particles in each bin.
-        center : array of shape (n_bin - 1,)
+        center : array of shape (n_bin,)
             Center of each bin.
 
         """
@@ -440,11 +480,17 @@ class Bunch:
         bin_max = self[dimension].max()
         bin_max = max(bin_max * 0.99, bin_max * 1.01)
 
-        bins = np.linspace(bin_min, bin_max, n_bin)
+        bins = np.linspace(bin_min, bin_max, n_bin + 1)
         center = (bins[1:] + bins[:-1]) / 2
         sorted_index = np.searchsorted(bins, self[dimension], side="left")
         sorted_index -= 1
-        profile = np.bincount(sorted_index, minlength=n_bin - 1)
+        profile = np.bincount(sorted_index, minlength=n_bin)
+
+        if self.track_alive and return_full_length:
+            sorted_index = np.searchsorted(bins,
+                                           self.particles[dimension],
+                                           side="left")
+            sorted_index -= 1
 
         return (bins, sorted_index, profile, center)
 
@@ -946,7 +992,7 @@ class Beam:
                    "bunch_mean", "bunch_std", "bunch_emit"}
             Variable to be plotted.
         option : str, optional
-            If var is "bunch_mean", "bunch_std", or "bunch_emit, option needs
+            If var is "bunch_mean", "bunch_std", or "bunch_emit", option needs
             to be specified.
             For "bunch_mean" and "bunch_std",
                 option = {"x","xp","y","yp","tau","delta"}.
diff --git a/mbtrack2/tracking/rf.py b/mbtrack2/tracking/rf.py
index 62a2798..3abc5dc 100644
--- a/mbtrack2/tracking/rf.py
+++ b/mbtrack2/tracking/rf.py
@@ -231,7 +231,7 @@ class CavityResonator():
         self.detune = detune
         self.Vc = Vc
         self.theta = theta
-        self.beam_phasor = np.zeros(1, dtype=complex)
+        self.beam_phasor = 0 + 0j
         self.beam_phasor_record = np.zeros((self.ring.h), dtype=complex)
         self.generator_phasor_record = np.zeros((self.ring.h), dtype=complex)
         self.tracking = False
@@ -288,16 +288,16 @@ class CavityResonator():
                     # mpi -> get shared bunch profile for current bunch
                     center = beam.mpi.tau_center[rank]
                     profile = beam.mpi.tau_profile[rank]
-                    bin_length = center[1] - center[0]
-                    charge_per_mp = beam.mpi.charge_per_mp_all[rank]
+                    bin_length = float(beam.mpi.tau_bin_length[rank][0])
+                    charge_per_mp = float(beam.mpi.charge_per_mp_all[rank])
                     if index == self.bunch_index:
                         sorted_index = beam.mpi.tau_sorted_index
                 else:
                     # no mpi -> get bunch profile for current bunch
-                    if len(bunch) != 0:
+                    if not bunch.is_empty:
                         (bins, sorted_index, profile,
                          center) = bunch.binning(n_bin=self.n_bin)
-                        bin_length = center[1] - center[0]
+                        bin_length = bins[1] - bins[0]
                         charge_per_mp = bunch.charge_per_mp
                         self.bunch_index = index
                     else:
@@ -323,7 +323,7 @@ class CavityResonator():
                 else:
                     # modify beam phasor
                     for i, center0 in enumerate(center):
-                        mp_per_bin = profile[i]
+                        mp_per_bin = int(profile[i])
 
                         if mp_per_bin == 0:
                             self.phasor_decay(bin_length, ref_frame="beam")
@@ -389,7 +389,7 @@ class CavityResonator():
                     # get shared bunch profile for current bunch
                     center = beam.mpi.tau_center[j]
                     profile = beam.mpi.tau_profile[j]
-                    bin_length = center[1] - center[0]
+                    bin_length = beam.mpi.tau_bin_length[j]
                     charge_per_mp = beam.mpi.charge_per_mp_all[j]
                 else:
                     if i == 0:
@@ -411,7 +411,7 @@ class CavityResonator():
                         profile = self.profile_save[j, :]
                         center = self.center_save[j, :]
 
-                    bin_length = center[1] - center[0]
+                    bin_length = bins[1] - bins[0]
                     charge_per_mp = bunch.charge_per_mp
 
                 self.phasor_decay(center[0] - bin_length/2, ref_frame="rf")
@@ -511,13 +511,14 @@ class CavityResonator():
         if self.tracking is False:
             self.init_tracking(beam)
 
-        N = self.n_bin - 1
+        N = self.n_bin
         delta = (self.wr - self.m * self.ring.omega1)
         n_turn = int(self.filling_time / self.ring.T0 * 10)
 
         T = np.ones(self.ring.h) * self.ring.T1
         bin_length = np.zeros(self.ring.h)
         charge_per_mp = np.zeros(self.ring.h)
+        bins = np.zeros((N + 1, self.ring.h))
         profile = np.zeros((N, self.ring.h))
         center = np.zeros((N, self.ring.h))
 
@@ -528,12 +529,12 @@ class CavityResonator():
                 beam.mpi.share_distributions(beam, n_bin=self.n_bin)
                 center[:, index] = beam.mpi.tau_center[j]
                 profile[:, index] = beam.mpi.tau_profile[j]
-                bin_length[index] = center[1, index] - center[0, index]
+                bin_length[index] = beam.mpi.bin_length[j]
                 charge_per_mp[index] = beam.mpi.charge_per_mp_all[j]
             else:
-                (bins, sorted_index, profile[:, index],
+                (bins[:, index], sorted_index, profile[:, index],
                  center[:, index]) = bunch.binning(n_bin=self.n_bin)
-                bin_length[index] = center[1, index] - center[0, index]
+                bin_length[index] = bins[1, index] - bins[0, index]
                 charge_per_mp[index] = bunch.charge_per_mp
             T[index] -= (center[-1, index] + bin_length[index] / 2)
             if index != 0:
@@ -1221,22 +1222,29 @@ class TunerLoop():
     gain : float
         Proportional gain of the tuner loop.
         If not specified, 0.01 is used.
-    avering_period:
+    avering_period : int, optional
         Period during which the phase difference is monitored and averaged.
         Then the feedback correction is applied every avering_period turn.
         Unit is turn number.
         A value longer than one synchrotron period (1/fs) is recommended.
-        If not specified, 2-synchrotron period (2/fs) is used, although it is
+        If None, 2-synchrotron period (2/fs) is used, although it is
         too fast compared to the actual situation.
-    offset : float
+        Default is None.
+    offset : float, optional
         Tuning offset in [rad].
+        Default is 0.
 
     """
 
-    def __init__(self, ring, cav_res, gain=0.01, avering_period=0, offset=0):
+    def __init__(self,
+                 ring,
+                 cav_res,
+                 gain=0.01,
+                 avering_period=None,
+                 offset=0):
         self.ring = ring
         self.cav_res = cav_res
-        if avering_period == 0:
+        if avering_period is None:
             fs = self.ring.synchrotron_tune(
                 self.cav_res.Vc) * self.ring.f1 / self.ring.h
             avering_period = 2 / fs / self.ring.T0
diff --git a/mbtrack2/tracking/spacecharge.py b/mbtrack2/tracking/spacecharge.py
index 661bc10..b421c6a 100644
--- a/mbtrack2/tracking/spacecharge.py
+++ b/mbtrack2/tracking/spacecharge.py
@@ -60,6 +60,7 @@ class TransverseSpaceCharge(Element):
         self.efieldn = _efieldn_mit
 
     @Element.parallel
+    @Element.track_bunch_if_non_empty
     def track(self, bunch):
         """
         Perform the tracking of the bunch through the space charge element.
@@ -73,26 +74,29 @@ class TransverseSpaceCharge(Element):
         prefactor = self.interaction_length / (self.ring.E0 *
                                                self.ring.gamma**2)
         (bins, sorted_index, profile,
-         center) = bunch.binning(n_bin=self.n_bins)
+         center) = bunch.binning(n_bin=self.n_bins, return_full_length=True)
         dz = (bins[1] - bins[0]) * c
         charge_density = bunch.charge_per_mp * profile / dz
         for bin_index in range(self.n_bins - 1):
             particle_ids = (bin_index == sorted_index)
+            if bunch.track_alive:
+                particle_ids = particle_ids & bunch.alive
             if len(particle_ids) == 0:
                 continue
-            x = bunch['x'][particle_ids]
-            y = bunch['y'][particle_ids]
+            x = bunch.particles['x'][particle_ids]
+            y = bunch.particles['y'][particle_ids]
 
             if len(x) != 0 and len(y) != 0:
                 mean_x, std_x = x.mean(), x.std()
                 mean_y, std_y = y.mean(), y.std()
 
-                en_x, en_y = get_displaced_efield(self.efieldn,
-                                                  bunch['x'][particle_ids],
-                                                  bunch['y'][particle_ids],
-                                                  std_x, std_y, mean_x, mean_y)
+                en_x, en_y = get_displaced_efield(
+                    self.efieldn, bunch.particles['x'][particle_ids],
+                    bunch.particles['y'][particle_ids], std_x, std_y, mean_x,
+                    mean_y)
 
                 kicks_x = prefactor * en_x * charge_density[bin_index]
                 kicks_y = prefactor * en_y * charge_density[bin_index]
-                bunch['xp'][particle_ids] += kicks_x
-                bunch['yp'][particle_ids] += kicks_y
+
+                bunch.particles['xp'][particle_ids] += kicks_x
+                bunch.particles['yp'][particle_ids] += kicks_y
diff --git a/mbtrack2/tracking/wakepotential.py b/mbtrack2/tracking/wakepotential.py
index 3fdb19b..47581c4 100644
--- a/mbtrack2/tracking/wakepotential.py
+++ b/mbtrack2/tracking/wakepotential.py
@@ -83,6 +83,8 @@ class WakePotential(Element):
         self.n_types = len(self.wakefield.wake_components)
         self.ring = ring
         self.n_bin = n_bin
+        if self.n_bin < 2:
+            raise ValueError("n_bin must be >= 2.")
         self.check_sampling()
         self.interp_on_postion = interp_on_postion
 
@@ -149,8 +151,8 @@ class WakePotential(Element):
             Dipole moment of the bunch.
 
         """
-        dipole = np.empty((self.n_bin - 1, ))
-        for i in range(self.n_bin - 1):
+        dipole = np.empty((self.n_bin, ))
+        for i in range(self.n_bin):
             dipole[i] = bunch[plane][self.sorted_index == i].sum()
         dipole = dipole / self.profile
         dipole[np.isnan(dipole)] = 0
@@ -287,6 +289,7 @@ class WakePotential(Element):
         return tau0, Wp
 
     @Element.parallel
+    @Element.track_bunch_if_non_empty
     def track(self, bunch):
         """
         Tracking method for the element.
@@ -298,34 +301,32 @@ class WakePotential(Element):
         bunch : Bunch or Beam object.
         
         """
-
-        if len(bunch) != 0:
-            self.charge_density(bunch)
-            for wake_type in self.types:
-                tau0, Wp = self.get_wakepotential(bunch, wake_type)
-                if self.interp_on_postion:
-                    Wp_interp = np.interp(bunch["tau"], tau0 + self.tau_mean,
-                                          Wp, 0, 0)
-                else:
-                    Wp_interp = np.interp(self.center, tau0 + self.tau_mean,
-                                          Wp, 0, 0)
-                    Wp_interp = Wp_interp[self.sorted_index]
-                if wake_type == "Wlong":
-                    bunch["delta"] += Wp_interp * bunch.charge / self.ring.E0
-                elif wake_type == "Wxdip":
-                    bunch["xp"] += Wp_interp * bunch.charge / self.ring.E0
-                elif wake_type == "Wydip":
-                    bunch["yp"] += Wp_interp * bunch.charge / self.ring.E0
-                elif wake_type == "Wxquad":
-                    bunch["xp"] += (bunch["x"] * Wp_interp * bunch.charge /
-                                    self.ring.E0)
-                elif wake_type == "Wyquad":
-                    bunch["yp"] += (bunch["y"] * Wp_interp * bunch.charge /
-                                    self.ring.E0)
-                elif wake_type == "Wxcst":
-                    bunch["xp"] += Wp_interp * bunch.charge / self.ring.E0
-                elif wake_type == "Wycst":
-                    bunch["yp"] += Wp_interp * bunch.charge / self.ring.E0
+        self.charge_density(bunch)
+        for wake_type in self.types:
+            tau0, Wp = self.get_wakepotential(bunch, wake_type)
+            if self.interp_on_postion:
+                Wp_interp = np.interp(bunch["tau"], tau0 + self.tau_mean, Wp,
+                                      0, 0)
+            else:
+                Wp_interp = np.interp(self.center, tau0 + self.tau_mean, Wp, 0,
+                                      0)
+                Wp_interp = Wp_interp[self.sorted_index]
+            if wake_type == "Wlong":
+                bunch["delta"] += Wp_interp * bunch.charge / self.ring.E0
+            elif wake_type == "Wxdip":
+                bunch["xp"] += Wp_interp * bunch.charge / self.ring.E0
+            elif wake_type == "Wydip":
+                bunch["yp"] += Wp_interp * bunch.charge / self.ring.E0
+            elif wake_type == "Wxquad":
+                bunch["xp"] += (bunch["x"] * Wp_interp * bunch.charge /
+                                self.ring.E0)
+            elif wake_type == "Wyquad":
+                bunch["yp"] += (bunch["y"] * Wp_interp * bunch.charge /
+                                self.ring.E0)
+            elif wake_type == "Wxcst":
+                bunch["xp"] += Wp_interp * bunch.charge / self.ring.E0
+            elif wake_type == "Wycst":
+                bunch["yp"] += Wp_interp * bunch.charge / self.ring.E0
 
     def plot_last_wake(self,
                        wake_type,
@@ -435,7 +436,9 @@ class WakePotential(Element):
         profile0 = gaussian_bunch(tau0, sigma)
         dipole0 = np.ones_like(profile0) * dipole
 
-        if wake_type == "Wlong" or wake_type == "Wxquad" or wake_type == "Wyquad":
+        if (wake_type == "Wlong" or wake_type == "Wxquad"
+                or wake_type == "Wyquad" or wake_type == "Wxcst"
+                or wake_type == "Wycst"):
             Wp = signal.convolve(profile0, W0 * -1, mode='same') * dtau0
         elif wake_type == "Wxdip":
             Wp = signal.convolve(profile0 * dipole0, W0, mode='same') * dtau0
@@ -581,7 +584,7 @@ class WakePotential(Element):
             idx = getattr(self.wakefield, wake_type).data.index
             diff = idx[1:] - idx[:-1]
             result = np.all(np.isclose(diff, diff[0], atol=1e-15))
-            if result is False:
+            if result == False:
                 raise ValueError(
                     "The wake function must be uniformly sampled.")
 
diff --git a/mbtrack2/utilities/optics.py b/mbtrack2/utilities/optics.py
index 4e2f58b..2aba395 100644
--- a/mbtrack2/utilities/optics.py
+++ b/mbtrack2/utilities/optics.py
@@ -657,6 +657,15 @@ class PhysicalModel:
         sym : bool, optional
             If True, right/left and top/bottum symmetry is applied.
         """
+        if start_position < self.position[0]:
+            raise ValueError(f"Wrong start_position value: {start_position}")
+        if end_position > self.position[-1]:
+            raise ValueError(f"Wrong end_position value: {end_position}")
+        if start_position > end_position:
+            raise ValueError(
+                f"start_position > end_position: {start_position} > {end_position}"
+            )
+
         ind = (self.position > start_position) & (self.position < end_position)
         if x_right is not None:
             self.x_right[ind] = x_right
@@ -723,6 +732,15 @@ class PhysicalModel:
         sym : bool, optional
             If True, right/left and top/bottum symmetry is applied.
         """
+        if start_position < self.position[0]:
+            raise ValueError(f"Wrong start_position value: {start_position}")
+        if end_position > self.position[-1]:
+            raise ValueError(f"Wrong end_position value: {end_position}")
+        if start_position > end_position:
+            raise ValueError(
+                f"start_position > end_position: {start_position} > {end_position}"
+            )
+
         ind = (self.position > start_position) & (self.position < end_position)
         if (x_right_start is not None) and (x_right_end is not None):
             self.x_right[ind] = np.linspace(x_right_start, x_right_end,
diff --git a/mbtrack2/utilities/spectrum.py b/mbtrack2/utilities/spectrum.py
index 7f8fb88..449cbde 100644
--- a/mbtrack2/utilities/spectrum.py
+++ b/mbtrack2/utilities/spectrum.py
@@ -3,6 +3,8 @@
 Module where bunch and beam spectrums and profile are defined.
 """
 
+from math import factorial
+
 import numpy as np
 from scipy.special import jv, spherical_jn
 
@@ -41,9 +43,8 @@ def spectral_density(frequency, sigma, m=1, k=0, mode="Hermite"):
     """
 
     if mode == "Hermite":
-        return 1 / (np.math.factorial(m) *
-                    2**m) * (2 * np.pi * frequency * sigma)**(
-                        2 * m) * np.exp(-(2 * np.pi * frequency * sigma)**2)
+        return 1 / (factorial(m) * 2**m) * (2 * np.pi * frequency * sigma)**(
+            2 * m) * np.exp(-(2 * np.pi * frequency * sigma)**2)
     elif mode == "Chebyshev":
         tau_l = 4 * sigma
         return (jv(m, 2 * np.pi * frequency * tau_l))**2
diff --git a/poetry.lock b/poetry.lock
index 584ca37..f137972 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -2,60 +2,65 @@
 
 [[package]]
 name = "accelerator-toolbox"
-version = "0.5.0"
+version = "0.6.1"
 description = "Accelerator Toolbox"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "accelerator-toolbox-0.5.0.tar.gz", hash = "sha256:519de56f87a71513d7eac93a5670fd493ec12799c019c213bc5db686e6413637"},
-    {file = "accelerator_toolbox-0.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6a4696192782db3b419b1dd39f6f84fb10bfbf05e74dc8746d521ee7d43f0577"},
-    {file = "accelerator_toolbox-0.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:06eca2ade40c0ec940d145ac2a57ca6e38ad9c7b3a418b71d705475ef64b5302"},
-    {file = "accelerator_toolbox-0.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59fa4dc6d3cfddc6a11fae1600dae93c0206b8be0ca26bdd3a507972cf259b5d"},
-    {file = "accelerator_toolbox-0.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811d0f107bdd1d652aa4c75861351224ff2df5aebbbe335e987c9df170e0d144"},
-    {file = "accelerator_toolbox-0.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f008b09c414158c7dfa1fcb725eeb3ec6a7a7ffc20d94c041ea65ac71b4a8de6"},
-    {file = "accelerator_toolbox-0.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:77f01fadd8620c2977e8034125fb5d672a7752ae8f07150f5d9cefffce22d6b3"},
-    {file = "accelerator_toolbox-0.5.0-cp310-cp310-win32.whl", hash = "sha256:74868c75e3c503a90d16d0f218cf4fab07744a49a854d3dea303b84ced681ed5"},
-    {file = "accelerator_toolbox-0.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6105e6f5421f3514216ed649cfcd11ab8fb0d808816f0983ba516e4148e4c6c"},
-    {file = "accelerator_toolbox-0.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2def21702adb6bc6841681f060aadb24ed8e708ee84afb25a7226af4a4bb955c"},
-    {file = "accelerator_toolbox-0.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:388119d32c948f74f2f8eb12ba28b82bb744b895eb64c3d57f1e8ea629aea5ab"},
-    {file = "accelerator_toolbox-0.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9607528289ea8d5aa74ef4a8ddf8b1dd2dfd81ffda32d011d66567349b6445b6"},
-    {file = "accelerator_toolbox-0.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:520721a348d69c59d766e28b7c482505b175dc6448696f49f0bc55386a3bb1c8"},
-    {file = "accelerator_toolbox-0.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:78b1fa8e4a141fb5655977fd03bd54ecced34c12c5b676dae2763e59bec65159"},
-    {file = "accelerator_toolbox-0.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:5b4e230f434e4839ccf56e13b285685f2d5163d089ea4dba488c4e58dd0d4bc2"},
-    {file = "accelerator_toolbox-0.5.0-cp311-cp311-win32.whl", hash = "sha256:4168f12e1c007c89df5967518d8343179ae66c0890e1817f6702096d4182659d"},
-    {file = "accelerator_toolbox-0.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:8fd0802b8728373aad936a48f599dbe886b18c060b6283da1e548676ffcabf67"},
-    {file = "accelerator_toolbox-0.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:dddf954cade3c60451be8ff3fbee4daa2e27a41d06608a7c425c8dad894b8830"},
-    {file = "accelerator_toolbox-0.5.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:144110ede61a6b5947e99f54e10d2205a165946290817fdc37a6ffa57e19c7a2"},
-    {file = "accelerator_toolbox-0.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f43002154b9b14576bd746a2f3c4684926dc1d65191e6923e01263a76a6595a5"},
-    {file = "accelerator_toolbox-0.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:1fcbafb2e85556933461335e1df035dee20e6aaf93fc9b5d516ce08b57ea2d82"},
-    {file = "accelerator_toolbox-0.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4da4c1ffa3b063447d1ee9ed4a4e2579af958c04a9aa2eecdb37b0984094f5f7"},
-    {file = "accelerator_toolbox-0.5.0-cp37-cp37m-win32.whl", hash = "sha256:4d46f7c518cbc6d2fbfaf6af157cea1a6437b4503b5fb0db5cee851768c48e27"},
-    {file = "accelerator_toolbox-0.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:1b065c09879e7f5d0e7978fe564462bd2da8b9a3574a36069b7ca86f618adca5"},
-    {file = "accelerator_toolbox-0.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:14f06d9606ec55f689ac2ab38b43e3c8e6403f4f253e6019adb8f512a7210dfd"},
-    {file = "accelerator_toolbox-0.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2bce394ab8715b6186fdad9a6fb273fb08a492d4d194a8ab44903450fce77c64"},
-    {file = "accelerator_toolbox-0.5.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3ec8a7c1a1ecca3223cfeba408ef20a9b8ca68b1cbefde6f854c84703fde272"},
-    {file = "accelerator_toolbox-0.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18e8136ca8f46fbcdbb7ae2aacc84473d7db07c155f2afcb4c7d289164bea38a"},
-    {file = "accelerator_toolbox-0.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:cef92eb3726a4084520f80f7a291461b800ab79dc40b261cd8cb25887a1ec5a2"},
-    {file = "accelerator_toolbox-0.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c4b02cf8628cc352099d0819a474d4d4e4f2997dd04c8854f231040e73253b00"},
-    {file = "accelerator_toolbox-0.5.0-cp38-cp38-win32.whl", hash = "sha256:3c628105026ef7949e00c5e71d056e28253381fc447c31188587c75f0b899d9b"},
-    {file = "accelerator_toolbox-0.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:3992890702d11c3fb74a7c5f098f2f81daf5664eed13f58e43b791fdc2e354f6"},
-    {file = "accelerator_toolbox-0.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801c467d4f514a2d07aef83d1570f29cc856526d1c1d1a661ee7aa2fbca8e110"},
-    {file = "accelerator_toolbox-0.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:eb88778a9d6a379f41c79a49e0379a95a2172af613fbd4fadde736e811bb9bf1"},
-    {file = "accelerator_toolbox-0.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8623e6086e3253232bf666562500e992f4f6ee20bfd3e645f03c08ed741c3b7"},
-    {file = "accelerator_toolbox-0.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fddbcbef404cc7f0e9e0994a54d20273600b3db540359c2b0d38edbe17a7ba12"},
-    {file = "accelerator_toolbox-0.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:49de8d3f8b5d3e27ac7899edf45c65fe98f302f42f2bb5d2fa16db278b97ae90"},
-    {file = "accelerator_toolbox-0.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fcd3329b7e52839a6a7d5edba3b2cb0f0326390297776162b634e0ca88d81ce7"},
-    {file = "accelerator_toolbox-0.5.0-cp39-cp39-win32.whl", hash = "sha256:de14d5205905278cdd91c5c3900174240f2e15566e9c777197d27e1addbe1eb2"},
-    {file = "accelerator_toolbox-0.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:c6f2a65bb5e7bd9d63b9780fbc1b1a5b0648bf143f1e8f721d4fc7bb77cd3396"},
+    {file = "accelerator_toolbox-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3ac5152cb0a120f35e1578e2881ce10d276d650c06be895bbde65967b9905b40"},
+    {file = "accelerator_toolbox-0.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d76bf18eb905d12dd11586e5955ac8ec32763a520176d666656cc0d51818dd55"},
+    {file = "accelerator_toolbox-0.6.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:672d86d95c6a8c3c4e2ac07b5a606070e53fc94d476a0652ec470c0c7a2ac72e"},
+    {file = "accelerator_toolbox-0.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:340bcf315d1b86bd34255ac04b44158a23433cb2b972f23856cd6013c5eaf262"},
+    {file = "accelerator_toolbox-0.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0fef4748a472a6e2e58d24678e04179386d5fafbec6a193fcebffea88b8b704c"},
+    {file = "accelerator_toolbox-0.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:89ffd9c45b8dd0d4e1518a74beb31aa6040511aa6f096dd0123010e8d65dec85"},
+    {file = "accelerator_toolbox-0.6.1-cp310-cp310-win32.whl", hash = "sha256:06e6a18fa5c7cb082f14be666d83811911aa2ec9124fbd56b8b4a8086218be7c"},
+    {file = "accelerator_toolbox-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:86e5ee9449c1dbc034916cfed174d525b115252b779d1379c1ed35152f40fbc3"},
+    {file = "accelerator_toolbox-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79dc93e26395266725dc652bbda46f9f45eca1e869dcfe5bda169c2823cea998"},
+    {file = "accelerator_toolbox-0.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bb4cc9958cf082b338900cf51e4805d0111f4b4952b3527ae8c667847fa594d1"},
+    {file = "accelerator_toolbox-0.6.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9caea973e753d97ce86315f7d2c08234a1d9081b66791a85639eeb03b5f59a7"},
+    {file = "accelerator_toolbox-0.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4dffd19a623a9c83374920ba969ccb20d82c4bef85e26c699fca2356277b1236"},
+    {file = "accelerator_toolbox-0.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:24761db8fd74d9feb4de25b3034012779c3da2cb8880fce3ff64c4bfa8ae23fb"},
+    {file = "accelerator_toolbox-0.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b60e95ce65a818ca3c3e987995f764039d47bd81abf181b6d300e6d5f7cee146"},
+    {file = "accelerator_toolbox-0.6.1-cp311-cp311-win32.whl", hash = "sha256:63d97e642b44fdc1df4dbfa5d12b853e9e04252708a4ee9580ddb2a998251896"},
+    {file = "accelerator_toolbox-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:154004613a8e3ab0d32a53956519e686c98449b22fdef43860f874948eaa9f63"},
+    {file = "accelerator_toolbox-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:0a1cfbce362b707dfe1278c817e0caee9e953ca071fc4c2fb9c2e64d670d2d0f"},
+    {file = "accelerator_toolbox-0.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:245ddd37377b1f27380bee7dfe5a333249a258670ad1a2d76c0a04d3452c214b"},
+    {file = "accelerator_toolbox-0.6.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:089ae3abd03d9d8a385bf1c402e03f4e74d8c08f8200951ae78c177afe020a00"},
+    {file = "accelerator_toolbox-0.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e53895f4eaacb9cc7eeecb60f0ae7c27435794c7d3874eaca4b66499257034f9"},
+    {file = "accelerator_toolbox-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:da4527f7607d11ac4fc6b844dedc21cf1dc666d30cd852e80eb68bb45530aaeb"},
+    {file = "accelerator_toolbox-0.6.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:03339a7cfdce54709e3e2946f1d073d9461d4826151384d6e7b99219b81a840b"},
+    {file = "accelerator_toolbox-0.6.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcb79ec9ea3403c8b863cd32fa9a7628f1a5a274ceecea17a4ec70257de11c74"},
+    {file = "accelerator_toolbox-0.6.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e9c2506a122f23fe0e8432c3f78d81217151c9fd4b925f41c2414983ba617cf"},
+    {file = "accelerator_toolbox-0.6.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:c9f5340617f7dfb602e2591039b4ac841a8aa7e854aae2b9ab030582caaa4d4a"},
+    {file = "accelerator_toolbox-0.6.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ff3e7a5dee31e866ad8d44300c1311148e1190b2f930b971fe2724ceb398a452"},
+    {file = "accelerator_toolbox-0.6.1-cp37-cp37m-win32.whl", hash = "sha256:96fbd1db4955d5709e7e84e9b00ab410e6381cb6118f2aabe2ab01c49dbd83df"},
+    {file = "accelerator_toolbox-0.6.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4c3d3ab862f908641ace70b523d657d51d38acaf316dde2734dfb54fda623b3f"},
+    {file = "accelerator_toolbox-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:415397692eac5f463cc34befe6bf620c5526b2e1c19ff91b6db80cfc721c2cd1"},
+    {file = "accelerator_toolbox-0.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:692d46673cbcba35667f3dcbf2b55d849b3905baafd45a4481d4bbca0f339edd"},
+    {file = "accelerator_toolbox-0.6.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d48226cfa0253412f1a57a360087a9c810f64491eb4696fa0501b5563032845"},
+    {file = "accelerator_toolbox-0.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed296d7d31595b9e68987b3be702e0c19227f3c649e5940c54d6f040c1c46fc5"},
+    {file = "accelerator_toolbox-0.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:3a5d17534d0d979f1aa3e258405c0f001148dd74938ae60f9311834d89e710bf"},
+    {file = "accelerator_toolbox-0.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dd9ced17e05112bfb8ed24377fbffefdc38e2ad4fc65495e48558a663085b45b"},
+    {file = "accelerator_toolbox-0.6.1-cp38-cp38-win32.whl", hash = "sha256:c82c5640142593d9251bcd89bb7dddd76dd9b408c7ae489ade3acb2a769975c3"},
+    {file = "accelerator_toolbox-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:986d1752e193681fb5082747c7cfe32c91cbe2588aa1d94957a8cf2b2aba1c8b"},
+    {file = "accelerator_toolbox-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4ab176e692f3b37805f11a9366677fbb5e5eac40f3ad959170b5485234916ae6"},
+    {file = "accelerator_toolbox-0.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13a5772298b73c591a8f84029ea79f9255ae8ae4983d33728d0cf5bfdabafe82"},
+    {file = "accelerator_toolbox-0.6.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6b9e6fa1becaa831dc88c786537559324ae551f162968ed512a04a22c56e771"},
+    {file = "accelerator_toolbox-0.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3e01953df2e2f38c480a9b7d4f0ff737d71dce6e9b5f40888c878b5bce1113cc"},
+    {file = "accelerator_toolbox-0.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:43f072fe45a922b23800acb16f2624739670b93d62efe586f588ad71c2ef219a"},
+    {file = "accelerator_toolbox-0.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9e0ebedf6a3b0941b8b77f6f800cc9aa1bfaa226973bc6de355edceaa735c209"},
+    {file = "accelerator_toolbox-0.6.1-cp39-cp39-win32.whl", hash = "sha256:eac7beac945ad1451f749c9eb96a1bc45b270dc894691ad5465fd19f7b0b752a"},
+    {file = "accelerator_toolbox-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:12dee22b6f7ea04e224afee519d82e32a2c0c8c28ee9317ddba76c9555093416"},
+    {file = "accelerator_toolbox-0.6.1.tar.gz", hash = "sha256:9f4c67216634d20b07f8105ead471b7440c80151dfe81ad27538ea4adefdaa89"},
 ]
 
 [package.dependencies]
-numpy = ">=1.16.6"
+numpy = {version = ">=1.23.5", markers = "python_version >= \"3.9\""}
 scipy = ">=1.4.0"
 
 [package.extras]
-dev = ["flake8", "pytest (>=2.9)", "pytest-cov", "pytest-lazy-fixture"]
-doc = ["Sphinx (>=5.3,<6.0)", "myst-parser", "pydata-sphinx-theme (>=0.11.0,<0.12.0)", "sphinx-copybutton"]
+dev = ["flake8", "pytest (>=2.9)", "pytest-cov"]
+doc = ["Sphinx (>=7.2,<8.0)", "myst-nb", "myst-parser", "pydata-sphinx-theme (>=0.14,<1.0)", "sphinx-copybutton", "sphinx_design"]
 mpi = ["mpi4py"]
 plot = ["matplotlib"]
 
@@ -72,66 +77,87 @@ files = [
 
 [[package]]
 name = "contourpy"
-version = "1.2.0"
+version = "1.3.0"
 description = "Python library for calculating contours of 2D quadrilateral grids"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "contourpy-1.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0274c1cb63625972c0c007ab14dd9ba9e199c36ae1a231ce45d725cbcbfd10a8"},
-    {file = "contourpy-1.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ab459a1cbbf18e8698399c595a01f6dcc5c138220ca3ea9e7e6126232d102bb4"},
-    {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdd887f17c2f4572ce548461e4f96396681212d858cae7bd52ba3310bc6f00f"},
-    {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5d16edfc3fc09968e09ddffada434b3bf989bf4911535e04eada58469873e28e"},
-    {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c203f617abc0dde5792beb586f827021069fb6d403d7f4d5c2b543d87edceb9"},
-    {file = "contourpy-1.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b69303ceb2e4d4f146bf82fda78891ef7bcd80c41bf16bfca3d0d7eb545448aa"},
-    {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:884c3f9d42d7218304bc74a8a7693d172685c84bd7ab2bab1ee567b769696df9"},
-    {file = "contourpy-1.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4a1b1208102be6e851f20066bf0e7a96b7d48a07c9b0cfe6d0d4545c2f6cadab"},
-    {file = "contourpy-1.2.0-cp310-cp310-win32.whl", hash = "sha256:34b9071c040d6fe45d9826cbbe3727d20d83f1b6110d219b83eb0e2a01d79488"},
-    {file = "contourpy-1.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:bd2f1ae63998da104f16a8b788f685e55d65760cd1929518fd94cd682bf03e41"},
-    {file = "contourpy-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dd10c26b4eadae44783c45ad6655220426f971c61d9b239e6f7b16d5cdaaa727"},
-    {file = "contourpy-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5c6b28956b7b232ae801406e529ad7b350d3f09a4fde958dfdf3c0520cdde0dd"},
-    {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebeac59e9e1eb4b84940d076d9f9a6cec0064e241818bcb6e32124cc5c3e377a"},
-    {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:139d8d2e1c1dd52d78682f505e980f592ba53c9f73bd6be102233e358b401063"},
-    {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1e9dc350fb4c58adc64df3e0703ab076f60aac06e67d48b3848c23647ae4310e"},
-    {file = "contourpy-1.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18fc2b4ed8e4a8fe849d18dce4bd3c7ea637758c6343a1f2bae1e9bd4c9f4686"},
-    {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:16a7380e943a6d52472096cb7ad5264ecee36ed60888e2a3d3814991a0107286"},
-    {file = "contourpy-1.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8d8faf05be5ec8e02a4d86f616fc2a0322ff4a4ce26c0f09d9f7fb5330a35c95"},
-    {file = "contourpy-1.2.0-cp311-cp311-win32.whl", hash = "sha256:67b7f17679fa62ec82b7e3e611c43a016b887bd64fb933b3ae8638583006c6d6"},
-    {file = "contourpy-1.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:99ad97258985328b4f207a5e777c1b44a83bfe7cf1f87b99f9c11d4ee477c4de"},
-    {file = "contourpy-1.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:575bcaf957a25d1194903a10bc9f316c136c19f24e0985a2b9b5608bdf5dbfe0"},
-    {file = "contourpy-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9e6c93b5b2dbcedad20a2f18ec22cae47da0d705d454308063421a3b290d9ea4"},
-    {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:464b423bc2a009088f19bdf1f232299e8b6917963e2b7e1d277da5041f33a779"},
-    {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:68ce4788b7d93e47f84edd3f1f95acdcd142ae60bc0e5493bfd120683d2d4316"},
-    {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d7d1f8871998cdff5d2ff6a087e5e1780139abe2838e85b0b46b7ae6cc25399"},
-    {file = "contourpy-1.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e739530c662a8d6d42c37c2ed52a6f0932c2d4a3e8c1f90692ad0ce1274abe0"},
-    {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:247b9d16535acaa766d03037d8e8fb20866d054d3c7fbf6fd1f993f11fc60ca0"},
-    {file = "contourpy-1.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:461e3ae84cd90b30f8d533f07d87c00379644205b1d33a5ea03381edc4b69431"},
-    {file = "contourpy-1.2.0-cp312-cp312-win32.whl", hash = "sha256:1c2559d6cffc94890b0529ea7eeecc20d6fadc1539273aa27faf503eb4656d8f"},
-    {file = "contourpy-1.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:491b1917afdd8638a05b611a56d46587d5a632cabead889a5440f7c638bc6ed9"},
-    {file = "contourpy-1.2.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5fd1810973a375ca0e097dee059c407913ba35723b111df75671a1976efa04bc"},
-    {file = "contourpy-1.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:999c71939aad2780f003979b25ac5b8f2df651dac7b38fb8ce6c46ba5abe6ae9"},
-    {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7caf9b241464c404613512d5594a6e2ff0cc9cb5615c9475cc1d9b514218ae8"},
-    {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:266270c6f6608340f6c9836a0fb9b367be61dde0c9a9a18d5ece97774105ff3e"},
-    {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbd50d0a0539ae2e96e537553aff6d02c10ed165ef40c65b0e27e744a0f10af8"},
-    {file = "contourpy-1.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11f8d2554e52f459918f7b8e6aa20ec2a3bce35ce95c1f0ef4ba36fbda306df5"},
-    {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ce96dd400486e80ac7d195b2d800b03e3e6a787e2a522bfb83755938465a819e"},
-    {file = "contourpy-1.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6d3364b999c62f539cd403f8123ae426da946e142312a514162adb2addd8d808"},
-    {file = "contourpy-1.2.0-cp39-cp39-win32.whl", hash = "sha256:1c88dfb9e0c77612febebb6ac69d44a8d81e3dc60f993215425b62c1161353f4"},
-    {file = "contourpy-1.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:78e6ad33cf2e2e80c5dfaaa0beec3d61face0fb650557100ee36db808bfa6843"},
-    {file = "contourpy-1.2.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:be16975d94c320432657ad2402f6760990cb640c161ae6da1363051805fa8108"},
-    {file = "contourpy-1.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b95a225d4948b26a28c08307a60ac00fb8671b14f2047fc5476613252a129776"},
-    {file = "contourpy-1.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0d7e03c0f9a4f90dc18d4e77e9ef4ec7b7bbb437f7f675be8e530d65ae6ef956"},
-    {file = "contourpy-1.2.0.tar.gz", hash = "sha256:171f311cb758de7da13fc53af221ae47a5877be5a0843a9fe150818c51ed276a"},
+    {file = "contourpy-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:880ea32e5c774634f9fcd46504bf9f080a41ad855f4fef54f5380f5133d343c7"},
+    {file = "contourpy-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:76c905ef940a4474a6289c71d53122a4f77766eef23c03cd57016ce19d0f7b42"},
+    {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92f8557cbb07415a4d6fa191f20fd9d2d9eb9c0b61d1b2f52a8926e43c6e9af7"},
+    {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36f965570cff02b874773c49bfe85562b47030805d7d8360748f3eca570f4cab"},
+    {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cacd81e2d4b6f89c9f8a5b69b86490152ff39afc58a95af002a398273e5ce589"},
+    {file = "contourpy-1.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69375194457ad0fad3a839b9e29aa0b0ed53bb54db1bfb6c3ae43d111c31ce41"},
+    {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a52040312b1a858b5e31ef28c2e865376a386c60c0e248370bbea2d3f3b760d"},
+    {file = "contourpy-1.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3faeb2998e4fcb256542e8a926d08da08977f7f5e62cf733f3c211c2a5586223"},
+    {file = "contourpy-1.3.0-cp310-cp310-win32.whl", hash = "sha256:36e0cff201bcb17a0a8ecc7f454fe078437fa6bda730e695a92f2d9932bd507f"},
+    {file = "contourpy-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:87ddffef1dbe5e669b5c2440b643d3fdd8622a348fe1983fad7a0f0ccb1cd67b"},
+    {file = "contourpy-1.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fa4c02abe6c446ba70d96ece336e621efa4aecae43eaa9b030ae5fb92b309ad"},
+    {file = "contourpy-1.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:834e0cfe17ba12f79963861e0f908556b2cedd52e1f75e6578801febcc6a9f49"},
+    {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dbc4c3217eee163fa3984fd1567632b48d6dfd29216da3ded3d7b844a8014a66"},
+    {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4865cd1d419e0c7a7bf6de1777b185eebdc51470800a9f42b9e9decf17762081"},
+    {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:303c252947ab4b14c08afeb52375b26781ccd6a5ccd81abcdfc1fafd14cf93c1"},
+    {file = "contourpy-1.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637f674226be46f6ba372fd29d9523dd977a291f66ab2a74fbeb5530bb3f445d"},
+    {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76a896b2f195b57db25d6b44e7e03f221d32fe318d03ede41f8b4d9ba1bff53c"},
+    {file = "contourpy-1.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e1fd23e9d01591bab45546c089ae89d926917a66dceb3abcf01f6105d927e2cb"},
+    {file = "contourpy-1.3.0-cp311-cp311-win32.whl", hash = "sha256:d402880b84df3bec6eab53cd0cf802cae6a2ef9537e70cf75e91618a3801c20c"},
+    {file = "contourpy-1.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:6cb6cc968059db9c62cb35fbf70248f40994dfcd7aa10444bbf8b3faeb7c2d67"},
+    {file = "contourpy-1.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:570ef7cf892f0afbe5b2ee410c507ce12e15a5fa91017a0009f79f7d93a1268f"},
+    {file = "contourpy-1.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:da84c537cb8b97d153e9fb208c221c45605f73147bd4cadd23bdae915042aad6"},
+    {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0be4d8425bfa755e0fd76ee1e019636ccc7c29f77a7c86b4328a9eb6a26d0639"},
+    {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c0da700bf58f6e0b65312d0a5e695179a71d0163957fa381bb3c1f72972537c"},
+    {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb8b141bb00fa977d9122636b16aa67d37fd40a3d8b52dd837e536d64b9a4d06"},
+    {file = "contourpy-1.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3634b5385c6716c258d0419c46d05c8aa7dc8cb70326c9a4fb66b69ad2b52e09"},
+    {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0dce35502151b6bd35027ac39ba6e5a44be13a68f55735c3612c568cac3805fd"},
+    {file = "contourpy-1.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea348f053c645100612b333adc5983d87be69acdc6d77d3169c090d3b01dc35"},
+    {file = "contourpy-1.3.0-cp312-cp312-win32.whl", hash = "sha256:90f73a5116ad1ba7174341ef3ea5c3150ddf20b024b98fb0c3b29034752c8aeb"},
+    {file = "contourpy-1.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:b11b39aea6be6764f84360fce6c82211a9db32a7c7de8fa6dd5397cf1d079c3b"},
+    {file = "contourpy-1.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3e1c7fa44aaae40a2247e2e8e0627f4bea3dd257014764aa644f319a5f8600e3"},
+    {file = "contourpy-1.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:364174c2a76057feef647c802652f00953b575723062560498dc7930fc9b1cb7"},
+    {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32b238b3b3b649e09ce9aaf51f0c261d38644bdfa35cbaf7b263457850957a84"},
+    {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d51fca85f9f7ad0b65b4b9fe800406d0d77017d7270d31ec3fb1cc07358fdea0"},
+    {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:732896af21716b29ab3e988d4ce14bc5133733b85956316fb0c56355f398099b"},
+    {file = "contourpy-1.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d73f659398a0904e125280836ae6f88ba9b178b2fed6884f3b1f95b989d2c8da"},
+    {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c6c7c2408b7048082932cf4e641fa3b8ca848259212f51c8c59c45aa7ac18f14"},
+    {file = "contourpy-1.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f317576606de89da6b7e0861cf6061f6146ead3528acabff9236458a6ba467f8"},
+    {file = "contourpy-1.3.0-cp313-cp313-win32.whl", hash = "sha256:31cd3a85dbdf1fc002280c65caa7e2b5f65e4a973fcdf70dd2fdcb9868069294"},
+    {file = "contourpy-1.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:4553c421929ec95fb07b3aaca0fae668b2eb5a5203d1217ca7c34c063c53d087"},
+    {file = "contourpy-1.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:345af746d7766821d05d72cb8f3845dfd08dd137101a2cb9b24de277d716def8"},
+    {file = "contourpy-1.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3bb3808858a9dc68f6f03d319acd5f1b8a337e6cdda197f02f4b8ff67ad2057b"},
+    {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:420d39daa61aab1221567b42eecb01112908b2cab7f1b4106a52caaec8d36973"},
+    {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4d63ee447261e963af02642ffcb864e5a2ee4cbfd78080657a9880b8b1868e18"},
+    {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:167d6c890815e1dac9536dca00828b445d5d0df4d6a8c6adb4a7ec3166812fa8"},
+    {file = "contourpy-1.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:710a26b3dc80c0e4febf04555de66f5fd17e9cf7170a7b08000601a10570bda6"},
+    {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:75ee7cb1a14c617f34a51d11fa7524173e56551646828353c4af859c56b766e2"},
+    {file = "contourpy-1.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:33c92cdae89ec5135d036e7218e69b0bb2851206077251f04a6c4e0e21f03927"},
+    {file = "contourpy-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a11077e395f67ffc2c44ec2418cfebed032cd6da3022a94fc227b6faf8e2acb8"},
+    {file = "contourpy-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e8134301d7e204c88ed7ab50028ba06c683000040ede1d617298611f9dc6240c"},
+    {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e12968fdfd5bb45ffdf6192a590bd8ddd3ba9e58360b29683c6bb71a7b41edca"},
+    {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fd2a0fc506eccaaa7595b7e1418951f213cf8255be2600f1ea1b61e46a60c55f"},
+    {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4cfb5c62ce023dfc410d6059c936dcf96442ba40814aefbfa575425a3a7f19dc"},
+    {file = "contourpy-1.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68a32389b06b82c2fdd68276148d7b9275b5f5cf13e5417e4252f6d1a34f72a2"},
+    {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:94e848a6b83da10898cbf1311a815f770acc9b6a3f2d646f330d57eb4e87592e"},
+    {file = "contourpy-1.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d78ab28a03c854a873787a0a42254a0ccb3cb133c672f645c9f9c8f3ae9d0800"},
+    {file = "contourpy-1.3.0-cp39-cp39-win32.whl", hash = "sha256:81cb5ed4952aae6014bc9d0421dec7c5835c9c8c31cdf51910b708f548cf58e5"},
+    {file = "contourpy-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:14e262f67bd7e6eb6880bc564dcda30b15e351a594657e55b7eec94b6ef72843"},
+    {file = "contourpy-1.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe41b41505a5a33aeaed2a613dccaeaa74e0e3ead6dd6fd3a118fb471644fd6c"},
+    {file = "contourpy-1.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca7e17a65f72a5133bdbec9ecf22401c62bcf4821361ef7811faee695799779"},
+    {file = "contourpy-1.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:1ec4dc6bf570f5b22ed0d7efba0dfa9c5b9e0431aeea7581aa217542d9e809a4"},
+    {file = "contourpy-1.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:00ccd0dbaad6d804ab259820fa7cb0b8036bda0686ef844d24125d8287178ce0"},
+    {file = "contourpy-1.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8ca947601224119117f7c19c9cdf6b3ab54c5726ef1d906aa4a69dfb6dd58102"},
+    {file = "contourpy-1.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:c6ec93afeb848a0845a18989da3beca3eec2c0f852322efe21af1931147d12cb"},
+    {file = "contourpy-1.3.0.tar.gz", hash = "sha256:7ffa0db17717a8ffb127efd0c95a4362d996b892c2904db72428d5b52e1938a4"},
 ]
 
 [package.dependencies]
-numpy = ">=1.20,<2.0"
+numpy = ">=1.23"
 
 [package.extras]
 bokeh = ["bokeh", "selenium"]
 docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"]
-mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.6.1)", "types-Pillow"]
+mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.11.1)", "types-Pillow"]
 test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
-test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"]
+test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"]
 
 [[package]]
 name = "cycler"
@@ -150,13 +176,13 @@ tests = ["pytest", "pytest-cov", "pytest-xdist"]
 
 [[package]]
 name = "exceptiongroup"
-version = "1.2.0"
+version = "1.2.2"
 description = "Backport of PEP 654 (exception groups)"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"},
-    {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"},
+    {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
+    {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
 ]
 
 [package.extras]
@@ -164,53 +190,59 @@ test = ["pytest (>=6)"]
 
 [[package]]
 name = "fonttools"
-version = "4.50.0"
+version = "4.54.1"
 description = "Tools to manipulate font files"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "fonttools-4.50.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:effd303fb422f8ce06543a36ca69148471144c534cc25f30e5be752bc4f46736"},
-    {file = "fonttools-4.50.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7913992ab836f621d06aabac118fc258b9947a775a607e1a737eb3a91c360335"},
-    {file = "fonttools-4.50.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e0a1c5bd2f63da4043b63888534b52c5a1fd7ae187c8ffc64cbb7ae475b9dab"},
-    {file = "fonttools-4.50.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d40fc98540fa5360e7ecf2c56ddf3c6e7dd04929543618fd7b5cc76e66390562"},
-    {file = "fonttools-4.50.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fff65fbb7afe137bac3113827855e0204482727bddd00a806034ab0d3951d0d"},
-    {file = "fonttools-4.50.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b1aeae3dd2ee719074a9372c89ad94f7c581903306d76befdaca2a559f802472"},
-    {file = "fonttools-4.50.0-cp310-cp310-win32.whl", hash = "sha256:e9623afa319405da33b43c85cceb0585a6f5d3a1d7c604daf4f7e1dd55c03d1f"},
-    {file = "fonttools-4.50.0-cp310-cp310-win_amd64.whl", hash = "sha256:778c5f43e7e654ef7fe0605e80894930bc3a7772e2f496238e57218610140f54"},
-    {file = "fonttools-4.50.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3dfb102e7f63b78c832e4539969167ffcc0375b013080e6472350965a5fe8048"},
-    {file = "fonttools-4.50.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9e58fe34cb379ba3d01d5d319d67dd3ce7ca9a47ad044ea2b22635cd2d1247fc"},
-    {file = "fonttools-4.50.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c673ab40d15a442a4e6eb09bf007c1dda47c84ac1e2eecbdf359adacb799c24"},
-    {file = "fonttools-4.50.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b3ac35cdcd1a4c90c23a5200212c1bb74fa05833cc7c14291d7043a52ca2aaa"},
-    {file = "fonttools-4.50.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:8844e7a2c5f7ecf977e82eb6b3014f025c8b454e046d941ece05b768be5847ae"},
-    {file = "fonttools-4.50.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:f849bd3c5c2249b49c98eca5aaebb920d2bfd92b3c69e84ca9bddf133e9f83f0"},
-    {file = "fonttools-4.50.0-cp311-cp311-win32.whl", hash = "sha256:39293ff231b36b035575e81c14626dfc14407a20de5262f9596c2cbb199c3625"},
-    {file = "fonttools-4.50.0-cp311-cp311-win_amd64.whl", hash = "sha256:c33d5023523b44d3481624f840c8646656a1def7630ca562f222eb3ead16c438"},
-    {file = "fonttools-4.50.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b4a886a6dbe60100ba1cd24de962f8cd18139bd32808da80de1fa9f9f27bf1dc"},
-    {file = "fonttools-4.50.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b2ca1837bfbe5eafa11313dbc7edada79052709a1fffa10cea691210af4aa1fa"},
-    {file = "fonttools-4.50.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0493dd97ac8977e48ffc1476b932b37c847cbb87fd68673dee5182004906828"},
-    {file = "fonttools-4.50.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:77844e2f1b0889120b6c222fc49b2b75c3d88b930615e98893b899b9352a27ea"},
-    {file = "fonttools-4.50.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3566bfb8c55ed9100afe1ba6f0f12265cd63a1387b9661eb6031a1578a28bad1"},
-    {file = "fonttools-4.50.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:35e10ddbc129cf61775d58a14f2d44121178d89874d32cae1eac722e687d9019"},
-    {file = "fonttools-4.50.0-cp312-cp312-win32.whl", hash = "sha256:cc8140baf9fa8f9b903f2b393a6c413a220fa990264b215bf48484f3d0bf8710"},
-    {file = "fonttools-4.50.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ccc85fd96373ab73c59833b824d7a73846670a0cb1f3afbaee2b2c426a8f931"},
-    {file = "fonttools-4.50.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e270a406219af37581d96c810172001ec536e29e5593aa40d4c01cca3e145aa6"},
-    {file = "fonttools-4.50.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ac2463de667233372e9e1c7e9de3d914b708437ef52a3199fdbf5a60184f190c"},
-    {file = "fonttools-4.50.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47abd6669195abe87c22750dbcd366dc3a0648f1b7c93c2baa97429c4dc1506e"},
-    {file = "fonttools-4.50.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:074841375e2e3d559aecc86e1224caf78e8b8417bb391e7d2506412538f21adc"},
-    {file = "fonttools-4.50.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0743fd2191ad7ab43d78cd747215b12033ddee24fa1e088605a3efe80d6984de"},
-    {file = "fonttools-4.50.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:3d7080cce7be5ed65bee3496f09f79a82865a514863197ff4d4d177389e981b0"},
-    {file = "fonttools-4.50.0-cp38-cp38-win32.whl", hash = "sha256:a467ba4e2eadc1d5cc1a11d355abb945f680473fbe30d15617e104c81f483045"},
-    {file = "fonttools-4.50.0-cp38-cp38-win_amd64.whl", hash = "sha256:f77e048f805e00870659d6318fd89ef28ca4ee16a22b4c5e1905b735495fc422"},
-    {file = "fonttools-4.50.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b6245eafd553c4e9a0708e93be51392bd2288c773523892fbd616d33fd2fda59"},
-    {file = "fonttools-4.50.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a4062cc7e8de26f1603323ef3ae2171c9d29c8a9f5e067d555a2813cd5c7a7e0"},
-    {file = "fonttools-4.50.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34692850dfd64ba06af61e5791a441f664cb7d21e7b544e8f385718430e8f8e4"},
-    {file = "fonttools-4.50.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678dd95f26a67e02c50dcb5bf250f95231d455642afbc65a3b0bcdacd4e4dd38"},
-    {file = "fonttools-4.50.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4f2ce7b0b295fe64ac0a85aef46a0f2614995774bd7bc643b85679c0283287f9"},
-    {file = "fonttools-4.50.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d346f4dc2221bfb7ab652d1e37d327578434ce559baf7113b0f55768437fe6a0"},
-    {file = "fonttools-4.50.0-cp39-cp39-win32.whl", hash = "sha256:a51eeaf52ba3afd70bf489be20e52fdfafe6c03d652b02477c6ce23c995222f4"},
-    {file = "fonttools-4.50.0-cp39-cp39-win_amd64.whl", hash = "sha256:8639be40d583e5d9da67795aa3eeeda0488fb577a1d42ae11a5036f18fb16d93"},
-    {file = "fonttools-4.50.0-py3-none-any.whl", hash = "sha256:48fa36da06247aa8282766cfd63efff1bb24e55f020f29a335939ed3844d20d3"},
-    {file = "fonttools-4.50.0.tar.gz", hash = "sha256:fa5cf61058c7dbb104c2ac4e782bf1b2016a8cf2f69de6e4dd6a865d2c969bb5"},
+    {file = "fonttools-4.54.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ed7ee041ff7b34cc62f07545e55e1468808691dddfd315d51dd82a6b37ddef2"},
+    {file = "fonttools-4.54.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41bb0b250c8132b2fcac148e2e9198e62ff06f3cc472065dff839327945c5882"},
+    {file = "fonttools-4.54.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7965af9b67dd546e52afcf2e38641b5be956d68c425bef2158e95af11d229f10"},
+    {file = "fonttools-4.54.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:278913a168f90d53378c20c23b80f4e599dca62fbffae4cc620c8eed476b723e"},
+    {file = "fonttools-4.54.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0e88e3018ac809b9662615072dcd6b84dca4c2d991c6d66e1970a112503bba7e"},
+    {file = "fonttools-4.54.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4aa4817f0031206e637d1e685251ac61be64d1adef111060df84fdcbc6ab6c44"},
+    {file = "fonttools-4.54.1-cp310-cp310-win32.whl", hash = "sha256:7e3b7d44e18c085fd8c16dcc6f1ad6c61b71ff463636fcb13df7b1b818bd0c02"},
+    {file = "fonttools-4.54.1-cp310-cp310-win_amd64.whl", hash = "sha256:dd9cc95b8d6e27d01e1e1f1fae8559ef3c02c76317da650a19047f249acd519d"},
+    {file = "fonttools-4.54.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5419771b64248484299fa77689d4f3aeed643ea6630b2ea750eeab219588ba20"},
+    {file = "fonttools-4.54.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:301540e89cf4ce89d462eb23a89464fef50915255ece765d10eee8b2bf9d75b2"},
+    {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ae5091547e74e7efecc3cbf8e75200bc92daaeb88e5433c5e3e95ea8ce5aa7"},
+    {file = "fonttools-4.54.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82834962b3d7c5ca98cb56001c33cf20eb110ecf442725dc5fdf36d16ed1ab07"},
+    {file = "fonttools-4.54.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d26732ae002cc3d2ecab04897bb02ae3f11f06dd7575d1df46acd2f7c012a8d8"},
+    {file = "fonttools-4.54.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:58974b4987b2a71ee08ade1e7f47f410c367cdfc5a94fabd599c88165f56213a"},
+    {file = "fonttools-4.54.1-cp311-cp311-win32.whl", hash = "sha256:ab774fa225238986218a463f3fe151e04d8c25d7de09df7f0f5fce27b1243dbc"},
+    {file = "fonttools-4.54.1-cp311-cp311-win_amd64.whl", hash = "sha256:07e005dc454eee1cc60105d6a29593459a06321c21897f769a281ff2d08939f6"},
+    {file = "fonttools-4.54.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:54471032f7cb5fca694b5f1a0aaeba4af6e10ae989df408e0216f7fd6cdc405d"},
+    {file = "fonttools-4.54.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8fa92cb248e573daab8d032919623cc309c005086d743afb014c836636166f08"},
+    {file = "fonttools-4.54.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a911591200114969befa7f2cb74ac148bce5a91df5645443371aba6d222e263"},
+    {file = "fonttools-4.54.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93d458c8a6a354dc8b48fc78d66d2a8a90b941f7fec30e94c7ad9982b1fa6bab"},
+    {file = "fonttools-4.54.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5eb2474a7c5be8a5331146758debb2669bf5635c021aee00fd7c353558fc659d"},
+    {file = "fonttools-4.54.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9c563351ddc230725c4bdf7d9e1e92cbe6ae8553942bd1fb2b2ff0884e8b714"},
+    {file = "fonttools-4.54.1-cp312-cp312-win32.whl", hash = "sha256:fdb062893fd6d47b527d39346e0c5578b7957dcea6d6a3b6794569370013d9ac"},
+    {file = "fonttools-4.54.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4564cf40cebcb53f3dc825e85910bf54835e8a8b6880d59e5159f0f325e637e"},
+    {file = "fonttools-4.54.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6e37561751b017cf5c40fce0d90fd9e8274716de327ec4ffb0df957160be3bff"},
+    {file = "fonttools-4.54.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:357cacb988a18aace66e5e55fe1247f2ee706e01debc4b1a20d77400354cddeb"},
+    {file = "fonttools-4.54.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e953cc0bddc2beaf3a3c3b5dd9ab7554677da72dfaf46951e193c9653e515a"},
+    {file = "fonttools-4.54.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58d29b9a294573d8319f16f2f79e42428ba9b6480442fa1836e4eb89c4d9d61c"},
+    {file = "fonttools-4.54.1-cp313-cp313-win32.whl", hash = "sha256:9ef1b167e22709b46bf8168368b7b5d3efeaaa746c6d39661c1b4405b6352e58"},
+    {file = "fonttools-4.54.1-cp313-cp313-win_amd64.whl", hash = "sha256:262705b1663f18c04250bd1242b0515d3bbae177bee7752be67c979b7d47f43d"},
+    {file = "fonttools-4.54.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ed2f80ca07025551636c555dec2b755dd005e2ea8fbeb99fc5cdff319b70b23b"},
+    {file = "fonttools-4.54.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9dc080e5a1c3b2656caff2ac2633d009b3a9ff7b5e93d0452f40cd76d3da3b3c"},
+    {file = "fonttools-4.54.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d152d1be65652fc65e695e5619e0aa0982295a95a9b29b52b85775243c06556"},
+    {file = "fonttools-4.54.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8583e563df41fdecef31b793b4dd3af8a9caa03397be648945ad32717a92885b"},
+    {file = "fonttools-4.54.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:0d1d353ef198c422515a3e974a1e8d5b304cd54a4c2eebcae708e37cd9eeffb1"},
+    {file = "fonttools-4.54.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:fda582236fee135d4daeca056c8c88ec5f6f6d88a004a79b84a02547c8f57386"},
+    {file = "fonttools-4.54.1-cp38-cp38-win32.whl", hash = "sha256:e7d82b9e56716ed32574ee106cabca80992e6bbdcf25a88d97d21f73a0aae664"},
+    {file = "fonttools-4.54.1-cp38-cp38-win_amd64.whl", hash = "sha256:ada215fd079e23e060157aab12eba0d66704316547f334eee9ff26f8c0d7b8ab"},
+    {file = "fonttools-4.54.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5b8a096e649768c2f4233f947cf9737f8dbf8728b90e2771e2497c6e3d21d13"},
+    {file = "fonttools-4.54.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e10d2e0a12e18f4e2dd031e1bf7c3d7017be5c8dbe524d07706179f355c5dac"},
+    {file = "fonttools-4.54.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:31c32d7d4b0958600eac75eaf524b7b7cb68d3a8c196635252b7a2c30d80e986"},
+    {file = "fonttools-4.54.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c39287f5c8f4a0c5a55daf9eaf9ccd223ea59eed3f6d467133cc727d7b943a55"},
+    {file = "fonttools-4.54.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a7a310c6e0471602fe3bf8efaf193d396ea561486aeaa7adc1f132e02d30c4b9"},
+    {file = "fonttools-4.54.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d3b659d1029946f4ff9b6183984578041b520ce0f8fb7078bb37ec7445806b33"},
+    {file = "fonttools-4.54.1-cp39-cp39-win32.whl", hash = "sha256:e96bc94c8cda58f577277d4a71f51c8e2129b8b36fd05adece6320dd3d57de8a"},
+    {file = "fonttools-4.54.1-cp39-cp39-win_amd64.whl", hash = "sha256:e8a4b261c1ef91e7188a30571be6ad98d1c6d9fa2427244c545e2fa0a2494dd7"},
+    {file = "fonttools-4.54.1-py3-none-any.whl", hash = "sha256:37cddd62d83dc4f72f7c3f3c2bcf2697e89a30efb152079896544a93907733bd"},
+    {file = "fonttools-4.54.1.tar.gz", hash = "sha256:957f669d4922f92c171ba01bef7f29410668db09f6c02111e22b2bce446f3285"},
 ]
 
 [package.extras]
@@ -229,77 +261,86 @@ woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
 
 [[package]]
 name = "h5py"
-version = "3.10.0"
+version = "3.12.1"
 description = "Read and write HDF5 files from Python"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "h5py-3.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b963fb772964fc1d1563c57e4e2e874022ce11f75ddc6df1a626f42bd49ab99f"},
-    {file = "h5py-3.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:012ab448590e3c4f5a8dd0f3533255bc57f80629bf7c5054cf4c87b30085063c"},
-    {file = "h5py-3.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:781a24263c1270a62cd67be59f293e62b76acfcc207afa6384961762bb88ea03"},
-    {file = "h5py-3.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f42e6c30698b520f0295d70157c4e202a9e402406f50dc08f5a7bc416b24e52d"},
-    {file = "h5py-3.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:93dd840bd675787fc0b016f7a05fc6efe37312a08849d9dd4053fd0377b1357f"},
-    {file = "h5py-3.10.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2381e98af081b6df7f6db300cd88f88e740649d77736e4b53db522d8874bf2dc"},
-    {file = "h5py-3.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:667fe23ab33d5a8a6b77970b229e14ae3bb84e4ea3382cc08567a02e1499eedd"},
-    {file = "h5py-3.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90286b79abd085e4e65e07c1bd7ee65a0f15818ea107f44b175d2dfe1a4674b7"},
-    {file = "h5py-3.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c013d2e79c00f28ffd0cc24e68665ea03ae9069e167087b2adb5727d2736a52"},
-    {file = "h5py-3.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:92273ce69ae4983dadb898fd4d3bea5eb90820df953b401282ee69ad648df684"},
-    {file = "h5py-3.10.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c97d03f87f215e7759a354460fb4b0d0f27001450b18b23e556e7856a0b21c3"},
-    {file = "h5py-3.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86df4c2de68257b8539a18646ceccdcf2c1ce6b1768ada16c8dcfb489eafae20"},
-    {file = "h5py-3.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba9ab36be991119a3ff32d0c7cbe5faf9b8d2375b5278b2aea64effbeba66039"},
-    {file = "h5py-3.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c8e4fda19eb769e9a678592e67eaec3a2f069f7570c82d2da909c077aa94339"},
-    {file = "h5py-3.10.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:492305a074327e8d2513011fa9fffeb54ecb28a04ca4c4227d7e1e9616d35641"},
-    {file = "h5py-3.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9450464b458cca2c86252b624279115dcaa7260a40d3cb1594bf2b410a2bd1a3"},
-    {file = "h5py-3.10.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd6f6d1384a9f491732cee233b99cd4bfd6e838a8815cc86722f9d2ee64032af"},
-    {file = "h5py-3.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3074ec45d3dc6e178c6f96834cf8108bf4a60ccb5ab044e16909580352010a97"},
-    {file = "h5py-3.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:212bb997a91e6a895ce5e2f365ba764debeaef5d2dca5c6fb7098d66607adf99"},
-    {file = "h5py-3.10.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5dfc65ac21fa2f630323c92453cadbe8d4f504726ec42f6a56cf80c2f90d6c52"},
-    {file = "h5py-3.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d4682b94fd36ab217352be438abd44c8f357c5449b8995e63886b431d260f3d3"},
-    {file = "h5py-3.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aece0e2e1ed2aab076c41802e50a0c3e5ef8816d60ece39107d68717d4559824"},
-    {file = "h5py-3.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43a61b2c2ad65b1fabc28802d133eed34debcc2c8b420cb213d3d4ef4d3e2229"},
-    {file = "h5py-3.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:ae2f0201c950059676455daf92700eeb57dcf5caaf71b9e1328e6e6593601770"},
-    {file = "h5py-3.10.0.tar.gz", hash = "sha256:d93adc48ceeb33347eb24a634fb787efc7ae4644e6ea4ba733d099605045c049"},
+    {file = "h5py-3.12.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f0f1a382cbf494679c07b4371f90c70391dedb027d517ac94fa2c05299dacda"},
+    {file = "h5py-3.12.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cb65f619dfbdd15e662423e8d257780f9a66677eae5b4b3fc9dca70b5fd2d2a3"},
+    {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b15d8dbd912c97541312c0e07438864d27dbca857c5ad634de68110c6beb1c2"},
+    {file = "h5py-3.12.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59685fe40d8c1fbbee088c88cd4da415a2f8bee5c270337dc5a1c4aa634e3307"},
+    {file = "h5py-3.12.1-cp310-cp310-win_amd64.whl", hash = "sha256:577d618d6b6dea3da07d13cc903ef9634cde5596b13e832476dd861aaf651f3e"},
+    {file = "h5py-3.12.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ccd9006d92232727d23f784795191bfd02294a4f2ba68708825cb1da39511a93"},
+    {file = "h5py-3.12.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ad8a76557880aed5234cfe7279805f4ab5ce16b17954606cca90d578d3e713ef"},
+    {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1473348139b885393125126258ae2d70753ef7e9cec8e7848434f385ae72069e"},
+    {file = "h5py-3.12.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:018a4597f35092ae3fb28ee851fdc756d2b88c96336b8480e124ce1ac6fb9166"},
+    {file = "h5py-3.12.1-cp311-cp311-win_amd64.whl", hash = "sha256:3fdf95092d60e8130ba6ae0ef7a9bd4ade8edbe3569c13ebbaf39baefffc5ba4"},
+    {file = "h5py-3.12.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:06a903a4e4e9e3ebbc8b548959c3c2552ca2d70dac14fcfa650d9261c66939ed"},
+    {file = "h5py-3.12.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b3b8f3b48717e46c6a790e3128d39c61ab595ae0a7237f06dfad6a3b51d5351"},
+    {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:050a4f2c9126054515169c49cb900949814987f0c7ae74c341b0c9f9b5056834"},
+    {file = "h5py-3.12.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c4b41d1019322a5afc5082864dfd6359f8935ecd37c11ac0029be78c5d112c9"},
+    {file = "h5py-3.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:e4d51919110a030913201422fb07987db4338eba5ec8c5a15d6fab8e03d443fc"},
+    {file = "h5py-3.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:513171e90ed92236fc2ca363ce7a2fc6f2827375efcbb0cc7fbdd7fe11fecafc"},
+    {file = "h5py-3.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:59400f88343b79655a242068a9c900001a34b63e3afb040bd7cdf717e440f653"},
+    {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3e465aee0ec353949f0f46bf6c6f9790a2006af896cee7c178a8c3e5090aa32"},
+    {file = "h5py-3.12.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba51c0c5e029bb5420a343586ff79d56e7455d496d18a30309616fdbeed1068f"},
+    {file = "h5py-3.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:52ab036c6c97055b85b2a242cb540ff9590bacfda0c03dd0cf0661b311f522f8"},
+    {file = "h5py-3.12.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2b8dd64f127d8b324f5d2cd1c0fd6f68af69084e9e47d27efeb9e28e685af3e"},
+    {file = "h5py-3.12.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4532c7e97fbef3d029735db8b6f5bf01222d9ece41e309b20d63cfaae2fb5c4d"},
+    {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fdf6d7936fa824acfa27305fe2d9f39968e539d831c5bae0e0d83ed521ad1ac"},
+    {file = "h5py-3.12.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84342bffd1f82d4f036433e7039e241a243531a1d3acd7341b35ae58cdab05bf"},
+    {file = "h5py-3.12.1-cp39-cp39-win_amd64.whl", hash = "sha256:62be1fc0ef195891949b2c627ec06bc8e837ff62d5b911b6e42e38e0f20a897d"},
+    {file = "h5py-3.12.1.tar.gz", hash = "sha256:326d70b53d31baa61f00b8aa5f95c2fcb9621a3ee8365d770c551a13dbbcbfdf"},
 ]
 
 [package.dependencies]
-numpy = ">=1.17.3"
+numpy = ">=1.19.3"
 
 [[package]]
 name = "importlib-metadata"
-version = "7.1.0"
+version = "8.5.0"
 description = "Read metadata from Python packages"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"},
-    {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"},
+    {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"},
+    {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"},
 ]
 
 [package.dependencies]
-zipp = ">=0.5"
+zipp = ">=3.20"
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
+cover = ["pytest-cov"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+enabler = ["pytest-enabler (>=2.2)"]
 perf = ["ipython"]
-testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"]
+test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"]
+type = ["pytest-mypy"]
 
 [[package]]
 name = "importlib-resources"
-version = "6.4.0"
+version = "6.4.5"
 description = "Read resources from Python packages"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"},
-    {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"},
+    {file = "importlib_resources-6.4.5-py3-none-any.whl", hash = "sha256:ac29d5f956f01d5e4bb63102a5a19957f1b9175e45649977264a1416783bb717"},
+    {file = "importlib_resources-6.4.5.tar.gz", hash = "sha256:980862a1d16c9e147a59603677fa2aa5fd82b87f223b6cb870695bcfce830065"},
 ]
 
 [package.dependencies]
 zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""}
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
+cover = ["pytest-cov"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+enabler = ["pytest-enabler (>=2.2)"]
+test = ["jaraco.test (>=5.4)", "pytest (>=6,!=8.1.*)", "zipp (>=3.17)"]
+type = ["pytest-mypy"]
 
 [[package]]
 name = "iniconfig"
@@ -328,152 +369,174 @@ colors = ["colorama (>=0.4.6)"]
 
 [[package]]
 name = "kiwisolver"
-version = "1.4.5"
+version = "1.4.7"
 description = "A fast implementation of the Cassowary constraint solver"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-win32.whl", hash = "sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238"},
-    {file = "kiwisolver-1.4.5-cp310-cp310-win_amd64.whl", hash = "sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-win32.whl", hash = "sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac"},
-    {file = "kiwisolver-1.4.5-cp311-cp311-win_amd64.whl", hash = "sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-win32.whl", hash = "sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20"},
-    {file = "kiwisolver-1.4.5-cp312-cp312-win_amd64.whl", hash = "sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9"},
-    {file = "kiwisolver-1.4.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130"},
-    {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898"},
-    {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709"},
-    {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b"},
-    {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89"},
-    {file = "kiwisolver-1.4.5-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985"},
-    {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265"},
-    {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a"},
-    {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205"},
-    {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb"},
-    {file = "kiwisolver-1.4.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f"},
-    {file = "kiwisolver-1.4.5-cp37-cp37m-win32.whl", hash = "sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3"},
-    {file = "kiwisolver-1.4.5-cp37-cp37m-win_amd64.whl", hash = "sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-win32.whl", hash = "sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e"},
-    {file = "kiwisolver-1.4.5-cp38-cp38-win_amd64.whl", hash = "sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-win32.whl", hash = "sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f"},
-    {file = "kiwisolver-1.4.5-cp39-cp39-win_amd64.whl", hash = "sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635"},
-    {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920"},
-    {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390"},
-    {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d"},
-    {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523"},
-    {file = "kiwisolver-1.4.5-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4"},
-    {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892"},
-    {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544"},
-    {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126"},
-    {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd"},
-    {file = "kiwisolver-1.4.5-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929"},
-    {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09"},
-    {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7"},
-    {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad"},
-    {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea"},
-    {file = "kiwisolver-1.4.5-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee"},
-    {file = "kiwisolver-1.4.5.tar.gz", hash = "sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:8a9c83f75223d5e48b0bc9cb1bf2776cf01563e00ade8775ffe13b0b6e1af3a6"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:58370b1ffbd35407444d57057b57da5d6549d2d854fa30249771775c63b5fe17"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:aa0abdf853e09aff551db11fce173e2177d00786c688203f52c87ad7fcd91ef9"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:8d53103597a252fb3ab8b5845af04c7a26d5e7ea8122303dd7a021176a87e8b9"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:88f17c5ffa8e9462fb79f62746428dd57b46eb931698e42e990ad63103f35e6c"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88a9ca9c710d598fd75ee5de59d5bda2684d9db36a9f50b6125eaea3969c2599"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f4d742cb7af1c28303a51b7a27aaee540e71bb8e24f68c736f6f2ffc82f2bf05"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28c7fea2196bf4c2f8d46a0415c77a1c480cc0724722f23d7410ffe9842c407"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e968b84db54f9d42046cf154e02911e39c0435c9801681e3fc9ce8a3c4130278"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0c18ec74c0472de033e1bebb2911c3c310eef5649133dd0bedf2a169a1b269e5"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8f0ea6da6d393d8b2e187e6a5e3fb81f5862010a40c3945e2c6d12ae45cfb2ad"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:f106407dda69ae456dd1227966bf445b157ccc80ba0dff3802bb63f30b74e895"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:84ec80df401cfee1457063732d90022f93951944b5b58975d34ab56bb150dfb3"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-win32.whl", hash = "sha256:71bb308552200fb2c195e35ef05de12f0c878c07fc91c270eb3d6e41698c3bcc"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-win_amd64.whl", hash = "sha256:44756f9fd339de0fb6ee4f8c1696cfd19b2422e0d70b4cefc1cc7f1f64045a8c"},
+    {file = "kiwisolver-1.4.7-cp310-cp310-win_arm64.whl", hash = "sha256:78a42513018c41c2ffd262eb676442315cbfe3c44eed82385c2ed043bc63210a"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d2b0e12a42fb4e72d509fc994713d099cbb15ebf1103545e8a45f14da2dfca54"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2a8781ac3edc42ea4b90bc23e7d37b665d89423818e26eb6df90698aa2287c95"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:46707a10836894b559e04b0fd143e343945c97fd170d69a2d26d640b4e297935"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ef97b8df011141c9b0f6caf23b29379f87dd13183c978a30a3c546d2c47314cb"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ab58c12a2cd0fc769089e6d38466c46d7f76aced0a1f54c77652446733d2d02"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:803b8e1459341c1bb56d1c5c010406d5edec8a0713a0945851290a7930679b51"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f9a9e8a507420fe35992ee9ecb302dab68550dedc0da9e2880dd88071c5fb052"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18077b53dc3bb490e330669a99920c5e6a496889ae8c63b58fbc57c3d7f33a18"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6af936f79086a89b3680a280c47ea90b4df7047b5bdf3aa5c524bbedddb9e545"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3abc5b19d24af4b77d1598a585b8a719beb8569a71568b66f4ebe1fb0449460b"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:933d4de052939d90afbe6e9d5273ae05fb836cc86c15b686edd4b3560cc0ee36"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:65e720d2ab2b53f1f72fb5da5fb477455905ce2c88aaa671ff0a447c2c80e8e3"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3bf1ed55088f214ba6427484c59553123fdd9b218a42bbc8c6496d6754b1e523"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-win32.whl", hash = "sha256:4c00336b9dd5ad96d0a558fd18a8b6f711b7449acce4c157e7343ba92dd0cf3d"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-win_amd64.whl", hash = "sha256:929e294c1ac1e9f615c62a4e4313ca1823ba37326c164ec720a803287c4c499b"},
+    {file = "kiwisolver-1.4.7-cp311-cp311-win_arm64.whl", hash = "sha256:e33e8fbd440c917106b237ef1a2f1449dfbb9b6f6e1ce17c94cd6a1e0d438376"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:5360cc32706dab3931f738d3079652d20982511f7c0ac5711483e6eab08efff2"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942216596dc64ddb25adb215c3c783215b23626f8d84e8eff8d6d45c3f29f75a"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:48b571ecd8bae15702e4f22d3ff6a0f13e54d3d00cd25216d5e7f658242065ee"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ad42ba922c67c5f219097b28fae965e10045ddf145d2928bfac2eb2e17673640"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:612a10bdae23404a72941a0fc8fa2660c6ea1217c4ce0dbcab8a8f6543ea9e7f"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e838bba3a3bac0fe06d849d29772eb1afb9745a59710762e4ba3f4cb8424483"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22f499f6157236c19f4bbbd472fa55b063db77a16cd74d49afe28992dff8c258"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:693902d433cf585133699972b6d7c42a8b9f8f826ebcaf0132ff55200afc599e"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4e77f2126c3e0b0d055f44513ed349038ac180371ed9b52fe96a32aa071a5107"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:657a05857bda581c3656bfc3b20e353c232e9193eb167766ad2dc58b56504948"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4bfa75a048c056a411f9705856abfc872558e33c055d80af6a380e3658766038"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:34ea1de54beef1c104422d210c47c7d2a4999bdecf42c7b5718fbe59a4cac383"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:90da3b5f694b85231cf93586dad5e90e2d71b9428f9aad96952c99055582f520"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-win32.whl", hash = "sha256:18e0cca3e008e17fe9b164b55735a325140a5a35faad8de92dd80265cd5eb80b"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-win_amd64.whl", hash = "sha256:58cb20602b18f86f83a5c87d3ee1c766a79c0d452f8def86d925e6c60fbf7bfb"},
+    {file = "kiwisolver-1.4.7-cp312-cp312-win_arm64.whl", hash = "sha256:f5a8b53bdc0b3961f8b6125e198617c40aeed638b387913bf1ce78afb1b0be2a"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2e6039dcbe79a8e0f044f1c39db1986a1b8071051efba3ee4d74f5b365f5226e"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a1ecf0ac1c518487d9d23b1cd7139a6a65bc460cd101ab01f1be82ecf09794b6"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7ab9ccab2b5bd5702ab0803676a580fffa2aa178c2badc5557a84cc943fcf750"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f816dd2277f8d63d79f9c8473a79fe54047bc0467754962840782c575522224d"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cf8bcc23ceb5a1b624572a1623b9f79d2c3b337c8c455405ef231933a10da379"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dea0bf229319828467d7fca8c7c189780aa9ff679c94539eed7532ebe33ed37c"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c06a4c7cf15ec739ce0e5971b26c93638730090add60e183530d70848ebdd34"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:913983ad2deb14e66d83c28b632fd35ba2b825031f2fa4ca29675e665dfecbe1"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5337ec7809bcd0f424c6b705ecf97941c46279cf5ed92311782c7c9c2026f07f"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c26ed10c4f6fa6ddb329a5120ba3b6db349ca192ae211e882970bfc9d91420b"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c619b101e6de2222c1fcb0531e1b17bbffbe54294bfba43ea0d411d428618c27"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:073a36c8273647592ea332e816e75ef8da5c303236ec0167196793eb1e34657a"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:3ce6b2b0231bda412463e152fc18335ba32faf4e8c23a754ad50ffa70e4091ee"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-win32.whl", hash = "sha256:f4c9aee212bc89d4e13f58be11a56cc8036cabad119259d12ace14b34476fd07"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-win_amd64.whl", hash = "sha256:8a3ec5aa8e38fc4c8af308917ce12c536f1c88452ce554027e55b22cbbfbff76"},
+    {file = "kiwisolver-1.4.7-cp313-cp313-win_arm64.whl", hash = "sha256:76c8094ac20ec259471ac53e774623eb62e6e1f56cd8690c67ce6ce4fcb05650"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5d5abf8f8ec1f4e22882273c423e16cae834c36856cac348cfbfa68e01c40f3a"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:aeb3531b196ef6f11776c21674dba836aeea9d5bd1cf630f869e3d90b16cfade"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7d755065e4e866a8086c9bdada157133ff466476a2ad7861828e17b6026e22c"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08471d4d86cbaec61f86b217dd938a83d85e03785f51121e791a6e6689a3be95"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7bbfcb7165ce3d54a3dfbe731e470f65739c4c1f85bb1018ee912bae139e263b"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d34eb8494bea691a1a450141ebb5385e4b69d38bb8403b5146ad279f4b30fa3"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9242795d174daa40105c1d86aba618e8eab7bf96ba8c3ee614da8302a9f95503"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a0f64a48bb81af7450e641e3fe0b0394d7381e342805479178b3d335d60ca7cf"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:8e045731a5416357638d1700927529e2b8ab304811671f665b225f8bf8d8f933"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4322872d5772cae7369f8351da1edf255a604ea7087fe295411397d0cfd9655e"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e1631290ee9271dffe3062d2634c3ecac02c83890ada077d225e081aca8aab89"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:edcfc407e4eb17e037bca59be0e85a2031a2ac87e4fed26d3e9df88b4165f92d"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:4d05d81ecb47d11e7f8932bd8b61b720bf0b41199358f3f5e36d38e28f0532c5"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-win32.whl", hash = "sha256:b38ac83d5f04b15e515fd86f312479d950d05ce2368d5413d46c088dda7de90a"},
+    {file = "kiwisolver-1.4.7-cp38-cp38-win_amd64.whl", hash = "sha256:d83db7cde68459fc803052a55ace60bea2bae361fc3b7a6d5da07e11954e4b09"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:3f9362ecfca44c863569d3d3c033dbe8ba452ff8eed6f6b5806382741a1334bd"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e8df2eb9b2bac43ef8b082e06f750350fbbaf2887534a5be97f6cf07b19d9583"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f32d6edbc638cde7652bd690c3e728b25332acbadd7cad670cc4a02558d9c417"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e2e6c39bd7b9372b0be21456caab138e8e69cc0fc1190a9dfa92bd45a1e6e904"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:dda56c24d869b1193fcc763f1284b9126550eaf84b88bbc7256e15028f19188a"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79849239c39b5e1fd906556c474d9b0439ea6792b637511f3fe3a41158d89ca8"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5e3bc157fed2a4c02ec468de4ecd12a6e22818d4f09cde2c31ee3226ffbefab2"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3da53da805b71e41053dc670f9a820d1157aae77b6b944e08024d17bcd51ef88"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8705f17dfeb43139a692298cb6637ee2e59c0194538153e83e9ee0c75c2eddde"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:82a5c2f4b87c26bb1a0ef3d16b5c4753434633b83d365cc0ddf2770c93829e3c"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ce8be0466f4c0d585cdb6c1e2ed07232221df101a4c6f28821d2aa754ca2d9e2"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:409afdfe1e2e90e6ee7fc896f3df9a7fec8e793e58bfa0d052c8a82f99c37abb"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5b9c3f4ee0b9a439d2415012bd1b1cc2df59e4d6a9939f4d669241d30b414327"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-win32.whl", hash = "sha256:a79ae34384df2b615eefca647a2873842ac3b596418032bef9a7283675962644"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-win_amd64.whl", hash = "sha256:cf0438b42121a66a3a667de17e779330fc0f20b0d97d59d2f2121e182b0505e4"},
+    {file = "kiwisolver-1.4.7-cp39-cp39-win_arm64.whl", hash = "sha256:764202cc7e70f767dab49e8df52c7455e8de0df5d858fa801a11aa0d882ccf3f"},
+    {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:94252291e3fe68001b1dd747b4c0b3be12582839b95ad4d1b641924d68fd4643"},
+    {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5b7dfa3b546da08a9f622bb6becdb14b3e24aaa30adba66749d38f3cc7ea9706"},
+    {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd3de6481f4ed8b734da5df134cd5a6a64fe32124fe83dde1e5b5f29fe30b1e6"},
+    {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a91b5f9f1205845d488c928e8570dcb62b893372f63b8b6e98b863ebd2368ff2"},
+    {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40fa14dbd66b8b8f470d5fc79c089a66185619d31645f9b0773b88b19f7223c4"},
+    {file = "kiwisolver-1.4.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:eb542fe7933aa09d8d8f9d9097ef37532a7df6497819d16efe4359890a2f417a"},
+    {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bfa1acfa0c54932d5607e19a2c24646fb4c1ae2694437789129cf099789a3b00"},
+    {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:eee3ea935c3d227d49b4eb85660ff631556841f6e567f0f7bda972df6c2c9935"},
+    {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f3160309af4396e0ed04db259c3ccbfdc3621b5559b5453075e5de555e1f3a1b"},
+    {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a17f6a29cf8935e587cc8a4dbfc8368c55edc645283db0ce9801016f83526c2d"},
+    {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10849fb2c1ecbfae45a693c070e0320a91b35dd4bcf58172c023b994283a124d"},
+    {file = "kiwisolver-1.4.7-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:ac542bf38a8a4be2dc6b15248d36315ccc65f0743f7b1a76688ffb6b5129a5c2"},
+    {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8b01aac285f91ca889c800042c35ad3b239e704b150cfd3382adfc9dcc780e39"},
+    {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:48be928f59a1f5c8207154f935334d374e79f2b5d212826307d072595ad76a2e"},
+    {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f37cfe618a117e50d8c240555331160d73d0411422b59b5ee217843d7b693608"},
+    {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599b5c873c63a1f6ed7eead644a8a380cfbdf5db91dcb6f85707aaab213b1674"},
+    {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:801fa7802e5cfabe3ab0c81a34c323a319b097dfb5004be950482d882f3d7225"},
+    {file = "kiwisolver-1.4.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:0c6c43471bc764fad4bc99c5c2d6d16a676b1abf844ca7c8702bdae92df01ee0"},
+    {file = "kiwisolver-1.4.7.tar.gz", hash = "sha256:9893ff81bd7107f7b685d3017cc6583daadb4fc26e4a888350df530e41980a60"},
 ]
 
 [[package]]
 name = "matplotlib"
-version = "3.8.3"
+version = "3.9.2"
 description = "Python plotting package"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "matplotlib-3.8.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cf60138ccc8004f117ab2a2bad513cc4d122e55864b4fe7adf4db20ca68a078f"},
-    {file = "matplotlib-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5f557156f7116be3340cdeef7f128fa99b0d5d287d5f41a16e169819dcf22357"},
-    {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f386cf162b059809ecfac3bcc491a9ea17da69fa35c8ded8ad154cd4b933d5ec"},
-    {file = "matplotlib-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3c5f96f57b0369c288bf6f9b5274ba45787f7e0589a34d24bdbaf6d3344632f"},
-    {file = "matplotlib-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:83e0f72e2c116ca7e571c57aa29b0fe697d4c6425c4e87c6e994159e0c008635"},
-    {file = "matplotlib-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:1c5c8290074ba31a41db1dc332dc2b62def469ff33766cbe325d32a3ee291aea"},
-    {file = "matplotlib-3.8.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5184e07c7e1d6d1481862ee361905b7059f7fe065fc837f7c3dc11eeb3f2f900"},
-    {file = "matplotlib-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d7e7e0993d0758933b1a241a432b42c2db22dfa37d4108342ab4afb9557cbe3e"},
-    {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:04b36ad07eac9740fc76c2aa16edf94e50b297d6eb4c081e3add863de4bb19a7"},
-    {file = "matplotlib-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c42dae72a62f14982f1474f7e5c9959fc4bc70c9de11cc5244c6e766200ba65"},
-    {file = "matplotlib-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:bf5932eee0d428192c40b7eac1399d608f5d995f975cdb9d1e6b48539a5ad8d0"},
-    {file = "matplotlib-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:40321634e3a05ed02abf7c7b47a50be50b53ef3eaa3a573847431a545585b407"},
-    {file = "matplotlib-3.8.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:09074f8057917d17ab52c242fdf4916f30e99959c1908958b1fc6032e2d0f6d4"},
-    {file = "matplotlib-3.8.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5745f6d0fb5acfabbb2790318db03809a253096e98c91b9a31969df28ee604aa"},
-    {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97653d869a71721b639714b42d87cda4cfee0ee74b47c569e4874c7590c55c5"},
-    {file = "matplotlib-3.8.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:242489efdb75b690c9c2e70bb5c6550727058c8a614e4c7716f363c27e10bba1"},
-    {file = "matplotlib-3.8.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:83c0653c64b73926730bd9ea14aa0f50f202ba187c307a881673bad4985967b7"},
-    {file = "matplotlib-3.8.3-cp312-cp312-win_amd64.whl", hash = "sha256:ef6c1025a570354297d6c15f7d0f296d95f88bd3850066b7f1e7b4f2f4c13a39"},
-    {file = "matplotlib-3.8.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c4af3f7317f8a1009bbb2d0bf23dfaba859eb7dd4ccbd604eba146dccaaaf0a4"},
-    {file = "matplotlib-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c6e00a65d017d26009bac6808f637b75ceade3e1ff91a138576f6b3065eeeba"},
-    {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7b49ab49a3bea17802df6872f8d44f664ba8f9be0632a60c99b20b6db2165b7"},
-    {file = "matplotlib-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6728dde0a3997396b053602dbd907a9bd64ec7d5cf99e728b404083698d3ca01"},
-    {file = "matplotlib-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:813925d08fb86aba139f2d31864928d67511f64e5945ca909ad5bc09a96189bb"},
-    {file = "matplotlib-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:cd3a0c2be76f4e7be03d34a14d49ded6acf22ef61f88da600a18a5cd8b3c5f3c"},
-    {file = "matplotlib-3.8.3-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:fa93695d5c08544f4a0dfd0965f378e7afc410d8672816aff1e81be1f45dbf2e"},
-    {file = "matplotlib-3.8.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9764df0e8778f06414b9d281a75235c1e85071f64bb5d71564b97c1306a2afc"},
-    {file = "matplotlib-3.8.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:5e431a09e6fab4012b01fc155db0ce6dccacdbabe8198197f523a4ef4805eb26"},
-    {file = "matplotlib-3.8.3.tar.gz", hash = "sha256:7b416239e9ae38be54b028abbf9048aff5054a9aba5416bef0bd17f9162ce161"},
+    {file = "matplotlib-3.9.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:9d78bbc0cbc891ad55b4f39a48c22182e9bdaea7fc0e5dbd364f49f729ca1bbb"},
+    {file = "matplotlib-3.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c375cc72229614632c87355366bdf2570c2dac01ac66b8ad048d2dabadf2d0d4"},
+    {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d94ff717eb2bd0b58fe66380bd8b14ac35f48a98e7c6765117fe67fb7684e64"},
+    {file = "matplotlib-3.9.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ab68d50c06938ef28681073327795c5db99bb4666214d2d5f880ed11aeaded66"},
+    {file = "matplotlib-3.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:65aacf95b62272d568044531e41de26285d54aec8cb859031f511f84bd8b495a"},
+    {file = "matplotlib-3.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:3fd595f34aa8a55b7fc8bf9ebea8aa665a84c82d275190a61118d33fbc82ccae"},
+    {file = "matplotlib-3.9.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d8dd059447824eec055e829258ab092b56bb0579fc3164fa09c64f3acd478772"},
+    {file = "matplotlib-3.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c797dac8bb9c7a3fd3382b16fe8f215b4cf0f22adccea36f1545a6d7be310b41"},
+    {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d719465db13267bcef19ea8954a971db03b9f48b4647e3860e4bc8e6ed86610f"},
+    {file = "matplotlib-3.9.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8912ef7c2362f7193b5819d17dae8629b34a95c58603d781329712ada83f9447"},
+    {file = "matplotlib-3.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7741f26a58a240f43bee74965c4882b6c93df3e7eb3de160126d8c8f53a6ae6e"},
+    {file = "matplotlib-3.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:ae82a14dab96fbfad7965403c643cafe6515e386de723e498cf3eeb1e0b70cc7"},
+    {file = "matplotlib-3.9.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ac43031375a65c3196bee99f6001e7fa5bdfb00ddf43379d3c0609bdca042df9"},
+    {file = "matplotlib-3.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:be0fc24a5e4531ae4d8e858a1a548c1fe33b176bb13eff7f9d0d38ce5112a27d"},
+    {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bf81de2926c2db243c9b2cbc3917619a0fc85796c6ba4e58f541df814bbf83c7"},
+    {file = "matplotlib-3.9.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6ee45bc4245533111ced13f1f2cace1e7f89d1c793390392a80c139d6cf0e6c"},
+    {file = "matplotlib-3.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:306c8dfc73239f0e72ac50e5a9cf19cc4e8e331dd0c54f5e69ca8758550f1e1e"},
+    {file = "matplotlib-3.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:5413401594cfaff0052f9d8b1aafc6d305b4bd7c4331dccd18f561ff7e1d3bd3"},
+    {file = "matplotlib-3.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:18128cc08f0d3cfff10b76baa2f296fc28c4607368a8402de61bb3f2eb33c7d9"},
+    {file = "matplotlib-3.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4876d7d40219e8ae8bb70f9263bcbe5714415acfdf781086601211335e24f8aa"},
+    {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6d9f07a80deab4bb0b82858a9e9ad53d1382fd122be8cde11080f4e7dfedb38b"},
+    {file = "matplotlib-3.9.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7c0410f181a531ec4e93bbc27692f2c71a15c2da16766f5ba9761e7ae518413"},
+    {file = "matplotlib-3.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:909645cce2dc28b735674ce0931a4ac94e12f5b13f6bb0b5a5e65e7cea2c192b"},
+    {file = "matplotlib-3.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:f32c7410c7f246838a77d6d1eff0c0f87f3cb0e7c4247aebea71a6d5a68cab49"},
+    {file = "matplotlib-3.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:37e51dd1c2db16ede9cfd7b5cabdfc818b2c6397c83f8b10e0e797501c963a03"},
+    {file = "matplotlib-3.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b82c5045cebcecd8496a4d694d43f9cc84aeeb49fe2133e036b207abe73f4d30"},
+    {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f053c40f94bc51bc03832a41b4f153d83f2062d88c72b5e79997072594e97e51"},
+    {file = "matplotlib-3.9.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbe196377a8248972f5cede786d4c5508ed5f5ca4a1e09b44bda889958b33f8c"},
+    {file = "matplotlib-3.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5816b1e1fe8c192cbc013f8f3e3368ac56fbecf02fb41b8f8559303f24c5015e"},
+    {file = "matplotlib-3.9.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:cef2a73d06601437be399908cf13aee74e86932a5ccc6ccdf173408ebc5f6bb2"},
+    {file = "matplotlib-3.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e0830e188029c14e891fadd99702fd90d317df294c3298aad682739c5533721a"},
+    {file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:03ba9c1299c920964e8d3857ba27173b4dbb51ca4bab47ffc2c2ba0eb5e2cbc5"},
+    {file = "matplotlib-3.9.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1cd93b91ab47a3616b4d3c42b52f8363b88ca021e340804c6ab2536344fad9ca"},
+    {file = "matplotlib-3.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:6d1ce5ed2aefcdce11904fc5bbea7d9c21fff3d5f543841edf3dea84451a09ea"},
+    {file = "matplotlib-3.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:b2696efdc08648536efd4e1601b5fd491fd47f4db97a5fbfd175549a7365c1b2"},
+    {file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:d52a3b618cb1cbb769ce2ee1dcdb333c3ab6e823944e9a2d36e37253815f9556"},
+    {file = "matplotlib-3.9.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:039082812cacd6c6bec8e17a9c1e6baca230d4116d522e81e1f63a74d01d2e21"},
+    {file = "matplotlib-3.9.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6758baae2ed64f2331d4fd19be38b7b4eae3ecec210049a26b6a4f3ae1c85dcc"},
+    {file = "matplotlib-3.9.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:050598c2b29e0b9832cde72bcf97627bf00262adbc4a54e2b856426bb2ef0697"},
+    {file = "matplotlib-3.9.2.tar.gz", hash = "sha256:96ab43906269ca64a6366934106fa01534454a69e471b7bf3d79083981aaab92"},
 ]
 
 [package.dependencies]
@@ -482,38 +545,41 @@ cycler = ">=0.10"
 fonttools = ">=4.22.0"
 importlib-resources = {version = ">=3.2.0", markers = "python_version < \"3.10\""}
 kiwisolver = ">=1.3.1"
-numpy = ">=1.21,<2"
+numpy = ">=1.23"
 packaging = ">=20.0"
 pillow = ">=8"
 pyparsing = ">=2.3.1"
 python-dateutil = ">=2.7"
 
+[package.extras]
+dev = ["meson-python (>=0.13.1)", "numpy (>=1.25)", "pybind11 (>=2.6)", "setuptools (>=64)", "setuptools_scm (>=7)"]
+
 [[package]]
 name = "mpi4py"
-version = "3.1.5"
+version = "3.1.6"
 description = "Python bindings for MPI"
 optional = false
-python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
-files = [
-    {file = "mpi4py-3.1.5-cp27-cp27m-win32.whl", hash = "sha256:dbc47296a560d2a5aa50d716cb9bf90552349687ee068a493d646e3f6c20a9a4"},
-    {file = "mpi4py-3.1.5-cp27-cp27m-win_amd64.whl", hash = "sha256:b3e06c9ef1d0a2da3d5c7431bbf4af50a01ac9915fb4ab33409a21cd9e67be9c"},
-    {file = "mpi4py-3.1.5-cp310-cp310-win32.whl", hash = "sha256:f39df0d985cb6fb342ee6c6902cadf21b2d828d7df00b182573da0242646b715"},
-    {file = "mpi4py-3.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:aec0e6238ed76c930c07df7dcea19f3be5ca958fb76353e668b19511ed4c86d7"},
-    {file = "mpi4py-3.1.5-cp311-cp311-win32.whl", hash = "sha256:f73686e3ff8f76bacb9ecacba0515f84392ad4c561b76603f9680f0fe64ef0ed"},
-    {file = "mpi4py-3.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:d854dae2e62042a0355fa24ef7bea50b5380414806319240a57e654be1e59d9c"},
-    {file = "mpi4py-3.1.5-cp312-cp312-win32.whl", hash = "sha256:17cc793bf2fe3921f2c3cda59a2a708d2e0c68ce07c8b9d2b6ee1a9adc28fe3d"},
-    {file = "mpi4py-3.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:8b38ad45a843bcbd7d11e23fd901016bb8069f35a9d4500666090465a2f734f2"},
-    {file = "mpi4py-3.1.5-cp35-cp35m-win32.whl", hash = "sha256:61d6c5df1803002cf6c61523417d48f9ecf64b55808e3d9d47815c174d7125dd"},
-    {file = "mpi4py-3.1.5-cp35-cp35m-win_amd64.whl", hash = "sha256:de6291eb7587e09637bc56f0e00d94863a1253f9d06b2ee97937bb9d49b53615"},
-    {file = "mpi4py-3.1.5-cp36-cp36m-win32.whl", hash = "sha256:dd7ebe2d9c52330670e2424ba3a535df999e57bdaf3a93a8967fede1d2d5927d"},
-    {file = "mpi4py-3.1.5-cp36-cp36m-win_amd64.whl", hash = "sha256:0de523428e15b453539da14208489a19f9fc7570cc8f9d1a3365175e441bba8e"},
-    {file = "mpi4py-3.1.5-cp37-cp37m-win32.whl", hash = "sha256:9cc87b70ce7164fbc521c45bbc4f5fb9acce8ea70d3503da0768ef67ba52186a"},
-    {file = "mpi4py-3.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:8574fe09d34d89531be6e640d18b54f7b7a046514de069f18c181bafeff51108"},
-    {file = "mpi4py-3.1.5-cp38-cp38-win32.whl", hash = "sha256:265f4c0a9cfdd606701fa36e0b373afae5930bedbf03c8360fd62f8c38639bf6"},
-    {file = "mpi4py-3.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:edf2224fa9c4416f891ac87f8e5b8a754ab45bc31dcfc4fbc8e29d6c643084c9"},
-    {file = "mpi4py-3.1.5-cp39-cp39-win32.whl", hash = "sha256:c4e6e776e183dbf3aa0945679303120716bb3f0826faeeb740dc5a055fcff3a8"},
-    {file = "mpi4py-3.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:3a2853b7b41899a17c3f1e6ad27e48a30e98d49d12830b345672fef3a6dcc8d6"},
-    {file = "mpi4py-3.1.5.tar.gz", hash = "sha256:a706e76db9255135c2fb5d1ef54cb4f7b0e4ad9e33cbada7de27626205f2a153"},
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+files = [
+    {file = "mpi4py-3.1.6-cp27-cp27m-win32.whl", hash = "sha256:95f27e00f3951f9c1533cd99ffeae2f384f7ba53cc3870ee06c3c88f9e5bd6c3"},
+    {file = "mpi4py-3.1.6-cp27-cp27m-win_amd64.whl", hash = "sha256:ea8a65f74707e1be5a0125ae3f4f6c0c475c3d845d623b9f5686a919c1119439"},
+    {file = "mpi4py-3.1.6-cp310-cp310-win32.whl", hash = "sha256:57cb8cc5e4f5730ca2116b4918fddb158bdac3852b57b0fb799278bcd2f9df03"},
+    {file = "mpi4py-3.1.6-cp310-cp310-win_amd64.whl", hash = "sha256:242459fcfd18c50b2ad6cd15f433efbaad1f12f4a343bdff5fa1720f4797317d"},
+    {file = "mpi4py-3.1.6-cp311-cp311-win32.whl", hash = "sha256:33d16f85d827417fd451ae61c304a26f5a1c3e881d9555c58fc8d5c7ac171034"},
+    {file = "mpi4py-3.1.6-cp311-cp311-win_amd64.whl", hash = "sha256:542fa0b3caa69fbb8f5e8ea60fea3414eef1444d93af28b4ee6486a3f8f27640"},
+    {file = "mpi4py-3.1.6-cp312-cp312-win32.whl", hash = "sha256:6662ffa622ee21041dcfd95bffd4b81906349e22d993239cc0abd17ebca6bed0"},
+    {file = "mpi4py-3.1.6-cp312-cp312-win_amd64.whl", hash = "sha256:606264e22c315de6250745fa84267c18a7370a16bd6c51acf226cbb977d0a10b"},
+    {file = "mpi4py-3.1.6-cp35-cp35m-win32.whl", hash = "sha256:1de7f6bd22ea6c9b1d6cb42e9d8092217552ffc8267f81df884b61f46aef557c"},
+    {file = "mpi4py-3.1.6-cp35-cp35m-win_amd64.whl", hash = "sha256:8dfadb2d7b50bda0f5c8538f8b5af5b2f830babc7c43a267102407a29616c38f"},
+    {file = "mpi4py-3.1.6-cp36-cp36m-win32.whl", hash = "sha256:757e324084c41f84b257b6d03850e73803bafea1b5e94dd6f9ebb3a149764299"},
+    {file = "mpi4py-3.1.6-cp36-cp36m-win_amd64.whl", hash = "sha256:76ecc87605193ef906597cadef571cabc8731ed1848a8c92320385c10ed41168"},
+    {file = "mpi4py-3.1.6-cp37-cp37m-win32.whl", hash = "sha256:67d28b5e5102a9d44eac6bc2765c2e28966b6b79cd54e0ea432403e054c70946"},
+    {file = "mpi4py-3.1.6-cp37-cp37m-win_amd64.whl", hash = "sha256:c0dfd9dbbcfa2fe61031eaba714f1f814e59439188b0a5ac063b2edc42daa234"},
+    {file = "mpi4py-3.1.6-cp38-cp38-win32.whl", hash = "sha256:8f9810decd319110b7dcf7a210a76d7db6a39d4c2b33c750ac4dc4638d7ce012"},
+    {file = "mpi4py-3.1.6-cp38-cp38-win_amd64.whl", hash = "sha256:ac671aa8c512fff432e0c1670c94eabd5571f5085c61579fee534b5b9e41bcf4"},
+    {file = "mpi4py-3.1.6-cp39-cp39-win32.whl", hash = "sha256:e8bd3fd0056580b1aaa4966ab9e54566bf6d6b35ff94c0ee7ceba83d55d039ac"},
+    {file = "mpi4py-3.1.6-cp39-cp39-win_amd64.whl", hash = "sha256:f9a35e23deadf7de9063523f19863957f379d0f13afc2b9787eafc9d570ab868"},
+    {file = "mpi4py-3.1.6.tar.gz", hash = "sha256:c8fa625e0f92b082ef955bfb52f19fa6691d29273d7d71135d295aa143dee6cb"},
 ]
 
 [[package]]
@@ -580,58 +646,71 @@ files = [
 
 [[package]]
 name = "packaging"
-version = "24.0"
+version = "24.2"
 description = "Core utilities for Python packages"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
-    {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
+    {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"},
+    {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"},
 ]
 
 [[package]]
 name = "pandas"
-version = "2.2.1"
+version = "2.2.3"
 description = "Powerful data structures for data analysis, time series, and statistics"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "pandas-2.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8df8612be9cd1c7797c93e1c5df861b2ddda0b48b08f2c3eaa0702cf88fb5f88"},
-    {file = "pandas-2.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0f573ab277252ed9aaf38240f3b54cfc90fff8e5cab70411ee1d03f5d51f3944"},
-    {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f02a3a6c83df4026e55b63c1f06476c9aa3ed6af3d89b4f04ea656ccdaaaa359"},
-    {file = "pandas-2.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c38ce92cb22a4bea4e3929429aa1067a454dcc9c335799af93ba9be21b6beb51"},
-    {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c2ce852e1cf2509a69e98358e8458775f89599566ac3775e70419b98615f4b06"},
-    {file = "pandas-2.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:53680dc9b2519cbf609c62db3ed7c0b499077c7fefda564e330286e619ff0dd9"},
-    {file = "pandas-2.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:94e714a1cca63e4f5939cdce5f29ba8d415d85166be3441165edd427dc9f6bc0"},
-    {file = "pandas-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f821213d48f4ab353d20ebc24e4faf94ba40d76680642fb7ce2ea31a3ad94f9b"},
-    {file = "pandas-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c70e00c2d894cb230e5c15e4b1e1e6b2b478e09cf27cc593a11ef955b9ecc81a"},
-    {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e97fbb5387c69209f134893abc788a6486dbf2f9e511070ca05eed4b930b1b02"},
-    {file = "pandas-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:101d0eb9c5361aa0146f500773395a03839a5e6ecde4d4b6ced88b7e5a1a6403"},
-    {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7d2ed41c319c9fb4fd454fe25372028dfa417aacb9790f68171b2e3f06eae8cd"},
-    {file = "pandas-2.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:af5d3c00557d657c8773ef9ee702c61dd13b9d7426794c9dfeb1dc4a0bf0ebc7"},
-    {file = "pandas-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:06cf591dbaefb6da9de8472535b185cba556d0ce2e6ed28e21d919704fef1a9e"},
-    {file = "pandas-2.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:88ecb5c01bb9ca927ebc4098136038519aa5d66b44671861ffab754cae75102c"},
-    {file = "pandas-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:04f6ec3baec203c13e3f8b139fb0f9f86cd8c0b94603ae3ae8ce9a422e9f5bee"},
-    {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a935a90a76c44fe170d01e90a3594beef9e9a6220021acfb26053d01426f7dc2"},
-    {file = "pandas-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c391f594aae2fd9f679d419e9a4d5ba4bce5bb13f6a989195656e7dc4b95c8f0"},
-    {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9d1265545f579edf3f8f0cb6f89f234f5e44ba725a34d86535b1a1d38decbccc"},
-    {file = "pandas-2.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:11940e9e3056576ac3244baef2fedade891977bcc1cb7e5cc8f8cc7d603edc89"},
-    {file = "pandas-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4acf681325ee1c7f950d058b05a820441075b0dd9a2adf5c4835b9bc056bf4fb"},
-    {file = "pandas-2.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9bd8a40f47080825af4317d0340c656744f2bfdb6819f818e6ba3cd24c0e1397"},
-    {file = "pandas-2.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:df0c37ebd19e11d089ceba66eba59a168242fc6b7155cba4ffffa6eccdfb8f16"},
-    {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:739cc70eaf17d57608639e74d63387b0d8594ce02f69e7a0b046f117974b3019"},
-    {file = "pandas-2.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9d3558d263073ed95e46f4650becff0c5e1ffe0fc3a015de3c79283dfbdb3df"},
-    {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4aa1d8707812a658debf03824016bf5ea0d516afdea29b7dc14cf687bc4d4ec6"},
-    {file = "pandas-2.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:76f27a809cda87e07f192f001d11adc2b930e93a2b0c4a236fde5429527423be"},
-    {file = "pandas-2.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:1ba21b1d5c0e43416218db63037dbe1a01fc101dc6e6024bcad08123e48004ab"},
-    {file = "pandas-2.2.1.tar.gz", hash = "sha256:0ab90f87093c13f3e8fa45b48ba9f39181046e8f3317d3aadb2fffbb1b978572"},
+    {file = "pandas-2.2.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5"},
+    {file = "pandas-2.2.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348"},
+    {file = "pandas-2.2.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed"},
+    {file = "pandas-2.2.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57"},
+    {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42"},
+    {file = "pandas-2.2.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f"},
+    {file = "pandas-2.2.3-cp310-cp310-win_amd64.whl", hash = "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645"},
+    {file = "pandas-2.2.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039"},
+    {file = "pandas-2.2.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd"},
+    {file = "pandas-2.2.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698"},
+    {file = "pandas-2.2.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc"},
+    {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3"},
+    {file = "pandas-2.2.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32"},
+    {file = "pandas-2.2.3-cp311-cp311-win_amd64.whl", hash = "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5"},
+    {file = "pandas-2.2.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9"},
+    {file = "pandas-2.2.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4"},
+    {file = "pandas-2.2.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3"},
+    {file = "pandas-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"},
+    {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8"},
+    {file = "pandas-2.2.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a"},
+    {file = "pandas-2.2.3-cp312-cp312-win_amd64.whl", hash = "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13"},
+    {file = "pandas-2.2.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015"},
+    {file = "pandas-2.2.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28"},
+    {file = "pandas-2.2.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0"},
+    {file = "pandas-2.2.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24"},
+    {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659"},
+    {file = "pandas-2.2.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb"},
+    {file = "pandas-2.2.3-cp313-cp313-win_amd64.whl", hash = "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d"},
+    {file = "pandas-2.2.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468"},
+    {file = "pandas-2.2.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18"},
+    {file = "pandas-2.2.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2"},
+    {file = "pandas-2.2.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4"},
+    {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d"},
+    {file = "pandas-2.2.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a"},
+    {file = "pandas-2.2.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39"},
+    {file = "pandas-2.2.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30"},
+    {file = "pandas-2.2.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c"},
+    {file = "pandas-2.2.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c"},
+    {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea"},
+    {file = "pandas-2.2.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761"},
+    {file = "pandas-2.2.3-cp39-cp39-win_amd64.whl", hash = "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e"},
+    {file = "pandas-2.2.3.tar.gz", hash = "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667"},
 ]
 
 [package.dependencies]
 numpy = [
-    {version = ">=1.22.4,<2", markers = "python_version < \"3.11\""},
-    {version = ">=1.23.2,<2", markers = "python_version == \"3.11\""},
-    {version = ">=1.26.0,<2", markers = "python_version >= \"3.12\""},
+    {version = ">=1.22.4", markers = "python_version < \"3.11\""},
+    {version = ">=1.23.2", markers = "python_version == \"3.11\""},
+    {version = ">=1.26.0", markers = "python_version >= \"3.12\""},
 ]
 python-dateutil = ">=2.8.2"
 pytz = ">=2020.1"
@@ -664,83 +743,90 @@ xml = ["lxml (>=4.9.2)"]
 
 [[package]]
 name = "pillow"
-version = "10.2.0"
+version = "11.0.0"
 description = "Python Imaging Library (Fork)"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "pillow-10.2.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:7823bdd049099efa16e4246bdf15e5a13dbb18a51b68fa06d6c1d4d8b99a796e"},
-    {file = "pillow-10.2.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:83b2021f2ade7d1ed556bc50a399127d7fb245e725aa0113ebd05cfe88aaf588"},
-    {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6fad5ff2f13d69b7e74ce5b4ecd12cc0ec530fcee76356cac6742785ff71c452"},
-    {file = "pillow-10.2.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da2b52b37dad6d9ec64e653637a096905b258d2fc2b984c41ae7d08b938a67e4"},
-    {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:47c0995fc4e7f79b5cfcab1fc437ff2890b770440f7696a3ba065ee0fd496563"},
-    {file = "pillow-10.2.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:322bdf3c9b556e9ffb18f93462e5f749d3444ce081290352c6070d014c93feb2"},
-    {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:51f1a1bffc50e2e9492e87d8e09a17c5eea8409cda8d3f277eb6edc82813c17c"},
-    {file = "pillow-10.2.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:69ffdd6120a4737710a9eee73e1d2e37db89b620f702754b8f6e62594471dee0"},
-    {file = "pillow-10.2.0-cp310-cp310-win32.whl", hash = "sha256:c6dafac9e0f2b3c78df97e79af707cdc5ef8e88208d686a4847bab8266870023"},
-    {file = "pillow-10.2.0-cp310-cp310-win_amd64.whl", hash = "sha256:aebb6044806f2e16ecc07b2a2637ee1ef67a11840a66752751714a0d924adf72"},
-    {file = "pillow-10.2.0-cp310-cp310-win_arm64.whl", hash = "sha256:7049e301399273a0136ff39b84c3678e314f2158f50f517bc50285fb5ec847ad"},
-    {file = "pillow-10.2.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35bb52c37f256f662abdfa49d2dfa6ce5d93281d323a9af377a120e89a9eafb5"},
-    {file = "pillow-10.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c23f307202661071d94b5e384e1e1dc7dfb972a28a2310e4ee16103e66ddb67"},
-    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:773efe0603db30c281521a7c0214cad7836c03b8ccff897beae9b47c0b657d61"},
-    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11fa2e5984b949b0dd6d7a94d967743d87c577ff0b83392f17cb3990d0d2fd6e"},
-    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:716d30ed977be8b37d3ef185fecb9e5a1d62d110dfbdcd1e2a122ab46fddb03f"},
-    {file = "pillow-10.2.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a086c2af425c5f62a65e12fbf385f7c9fcb8f107d0849dba5839461a129cf311"},
-    {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:c8de2789052ed501dd829e9cae8d3dcce7acb4777ea4a479c14521c942d395b1"},
-    {file = "pillow-10.2.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:609448742444d9290fd687940ac0b57fb35e6fd92bdb65386e08e99af60bf757"},
-    {file = "pillow-10.2.0-cp311-cp311-win32.whl", hash = "sha256:823ef7a27cf86df6597fa0671066c1b596f69eba53efa3d1e1cb8b30f3533068"},
-    {file = "pillow-10.2.0-cp311-cp311-win_amd64.whl", hash = "sha256:1da3b2703afd040cf65ec97efea81cfba59cdbed9c11d8efc5ab09df9509fc56"},
-    {file = "pillow-10.2.0-cp311-cp311-win_arm64.whl", hash = "sha256:edca80cbfb2b68d7b56930b84a0e45ae1694aeba0541f798e908a49d66b837f1"},
-    {file = "pillow-10.2.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:1b5e1b74d1bd1b78bc3477528919414874748dd363e6272efd5abf7654e68bef"},
-    {file = "pillow-10.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0eae2073305f451d8ecacb5474997c08569fb4eb4ac231ffa4ad7d342fdc25ac"},
-    {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7c2286c23cd350b80d2fc9d424fc797575fb16f854b831d16fd47ceec078f2c"},
-    {file = "pillow-10.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e23412b5c41e58cec602f1135c57dfcf15482013ce6e5f093a86db69646a5aa"},
-    {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:52a50aa3fb3acb9cf7213573ef55d31d6eca37f5709c69e6858fe3bc04a5c2a2"},
-    {file = "pillow-10.2.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:127cee571038f252a552760076407f9cff79761c3d436a12af6000cd182a9d04"},
-    {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:8d12251f02d69d8310b046e82572ed486685c38f02176bd08baf216746eb947f"},
-    {file = "pillow-10.2.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:54f1852cd531aa981bc0965b7d609f5f6cc8ce8c41b1139f6ed6b3c54ab82bfb"},
-    {file = "pillow-10.2.0-cp312-cp312-win32.whl", hash = "sha256:257d8788df5ca62c980314053197f4d46eefedf4e6175bc9412f14412ec4ea2f"},
-    {file = "pillow-10.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:154e939c5f0053a383de4fd3d3da48d9427a7e985f58af8e94d0b3c9fcfcf4f9"},
-    {file = "pillow-10.2.0-cp312-cp312-win_arm64.whl", hash = "sha256:f379abd2f1e3dddb2b61bc67977a6b5a0a3f7485538bcc6f39ec76163891ee48"},
-    {file = "pillow-10.2.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:8373c6c251f7ef8bda6675dd6d2b3a0fcc31edf1201266b5cf608b62a37407f9"},
-    {file = "pillow-10.2.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:870ea1ada0899fd0b79643990809323b389d4d1d46c192f97342eeb6ee0b8483"},
-    {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4b6b1e20608493548b1f32bce8cca185bf0480983890403d3b8753e44077129"},
-    {file = "pillow-10.2.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3031709084b6e7852d00479fd1d310b07d0ba82765f973b543c8af5061cf990e"},
-    {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:3ff074fc97dd4e80543a3e91f69d58889baf2002b6be64347ea8cf5533188213"},
-    {file = "pillow-10.2.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:cb4c38abeef13c61d6916f264d4845fab99d7b711be96c326b84df9e3e0ff62d"},
-    {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b1b3020d90c2d8e1dae29cf3ce54f8094f7938460fb5ce8bc5c01450b01fbaf6"},
-    {file = "pillow-10.2.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:170aeb00224ab3dc54230c797f8404507240dd868cf52066f66a41b33169bdbe"},
-    {file = "pillow-10.2.0-cp38-cp38-win32.whl", hash = "sha256:c4225f5220f46b2fde568c74fca27ae9771536c2e29d7c04f4fb62c83275ac4e"},
-    {file = "pillow-10.2.0-cp38-cp38-win_amd64.whl", hash = "sha256:0689b5a8c5288bc0504d9fcee48f61a6a586b9b98514d7d29b840143d6734f39"},
-    {file = "pillow-10.2.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:b792a349405fbc0163190fde0dc7b3fef3c9268292586cf5645598b48e63dc67"},
-    {file = "pillow-10.2.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c570f24be1e468e3f0ce7ef56a89a60f0e05b30a3669a459e419c6eac2c35364"},
-    {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8ecd059fdaf60c1963c58ceb8997b32e9dc1b911f5da5307aab614f1ce5c2fb"},
-    {file = "pillow-10.2.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c365fd1703040de1ec284b176d6af5abe21b427cb3a5ff68e0759e1e313a5e7e"},
-    {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:70c61d4c475835a19b3a5aa42492409878bbca7438554a1f89d20d58a7c75c01"},
-    {file = "pillow-10.2.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:b6f491cdf80ae540738859d9766783e3b3c8e5bd37f5dfa0b76abdecc5081f13"},
-    {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9d189550615b4948f45252d7f005e53c2040cea1af5b60d6f79491a6e147eef7"},
-    {file = "pillow-10.2.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:49d9ba1ed0ef3e061088cd1e7538a0759aab559e2e0a80a36f9fd9d8c0c21591"},
-    {file = "pillow-10.2.0-cp39-cp39-win32.whl", hash = "sha256:babf5acfede515f176833ed6028754cbcd0d206f7f614ea3447d67c33be12516"},
-    {file = "pillow-10.2.0-cp39-cp39-win_amd64.whl", hash = "sha256:0304004f8067386b477d20a518b50f3fa658a28d44e4116970abfcd94fac34a8"},
-    {file = "pillow-10.2.0-cp39-cp39-win_arm64.whl", hash = "sha256:0fb3e7fc88a14eacd303e90481ad983fd5b69c761e9e6ef94c983f91025da869"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:322209c642aabdd6207517e9739c704dc9f9db943015535783239022002f054a"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3eedd52442c0a5ff4f887fab0c1c0bb164d8635b32c894bc1faf4c618dd89df2"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb28c753fd5eb3dd859b4ee95de66cc62af91bcff5db5f2571d32a520baf1f04"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:33870dc4653c5017bf4c8873e5488d8f8d5f8935e2f1fb9a2208c47cdd66efd2"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:3c31822339516fb3c82d03f30e22b1d038da87ef27b6a78c9549888f8ceda39a"},
-    {file = "pillow-10.2.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a2b56ba36e05f973d450582fb015594aaa78834fefe8dfb8fcd79b93e64ba4c6"},
-    {file = "pillow-10.2.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:d8e6aeb9201e655354b3ad049cb77d19813ad4ece0df1249d3c793de3774f8c7"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:2247178effb34a77c11c0e8ac355c7a741ceca0a732b27bf11e747bbc950722f"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:15587643b9e5eb26c48e49a7b33659790d28f190fc514a322d55da2fb5c2950e"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753cd8f2086b2b80180d9b3010dd4ed147efc167c90d3bf593fe2af21265e5a5"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:7c8f97e8e7a9009bcacbe3766a36175056c12f9a44e6e6f2d5caad06dcfbf03b"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d1b35bcd6c5543b9cb547dee3150c93008f8dd0f1fef78fc0cd2b141c5baf58a"},
-    {file = "pillow-10.2.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fe4c15f6c9285dc54ce6553a3ce908ed37c8f3825b5a51a15c91442bb955b868"},
-    {file = "pillow-10.2.0.tar.gz", hash = "sha256:e87f0b2c78157e12d7686b27d63c070fd65d994e8ddae6f328e0dcf4a0cd007e"},
+    {file = "pillow-11.0.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:6619654954dc4936fcff82db8eb6401d3159ec6be81e33c6000dfd76ae189947"},
+    {file = "pillow-11.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b3c5ac4bed7519088103d9450a1107f76308ecf91d6dabc8a33a2fcfb18d0fba"},
+    {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a65149d8ada1055029fcb665452b2814fe7d7082fcb0c5bed6db851cb69b2086"},
+    {file = "pillow-11.0.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88a58d8ac0cc0e7f3a014509f0455248a76629ca9b604eca7dc5927cc593c5e9"},
+    {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:c26845094b1af3c91852745ae78e3ea47abf3dbcd1cf962f16b9a5fbe3ee8488"},
+    {file = "pillow-11.0.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:1a61b54f87ab5786b8479f81c4b11f4d61702830354520837f8cc791ebba0f5f"},
+    {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:674629ff60030d144b7bca2b8330225a9b11c482ed408813924619c6f302fdbb"},
+    {file = "pillow-11.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:598b4e238f13276e0008299bd2482003f48158e2b11826862b1eb2ad7c768b97"},
+    {file = "pillow-11.0.0-cp310-cp310-win32.whl", hash = "sha256:9a0f748eaa434a41fccf8e1ee7a3eed68af1b690e75328fd7a60af123c193b50"},
+    {file = "pillow-11.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:a5629742881bcbc1f42e840af185fd4d83a5edeb96475a575f4da50d6ede337c"},
+    {file = "pillow-11.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:ee217c198f2e41f184f3869f3e485557296d505b5195c513b2bfe0062dc537f1"},
+    {file = "pillow-11.0.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1c1d72714f429a521d8d2d018badc42414c3077eb187a59579f28e4270b4b0fc"},
+    {file = "pillow-11.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:499c3a1b0d6fc8213519e193796eb1a86a1be4b1877d678b30f83fd979811d1a"},
+    {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b2351c85d855293a299038e1f89db92a2f35e8d2f783489c6f0b2b5f3fe8a3"},
+    {file = "pillow-11.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f4dba50cfa56f910241eb7f883c20f1e7b1d8f7d91c750cd0b318bad443f4d5"},
+    {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:5ddbfd761ee00c12ee1be86c9c0683ecf5bb14c9772ddbd782085779a63dd55b"},
+    {file = "pillow-11.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:45c566eb10b8967d71bf1ab8e4a525e5a93519e29ea071459ce517f6b903d7fa"},
+    {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b4fd7bd29610a83a8c9b564d457cf5bd92b4e11e79a4ee4716a63c959699b306"},
+    {file = "pillow-11.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cb929ca942d0ec4fac404cbf520ee6cac37bf35be479b970c4ffadf2b6a1cad9"},
+    {file = "pillow-11.0.0-cp311-cp311-win32.whl", hash = "sha256:006bcdd307cc47ba43e924099a038cbf9591062e6c50e570819743f5607404f5"},
+    {file = "pillow-11.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:52a2d8323a465f84faaba5236567d212c3668f2ab53e1c74c15583cf507a0291"},
+    {file = "pillow-11.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:16095692a253047fe3ec028e951fa4221a1f3ed3d80c397e83541a3037ff67c9"},
+    {file = "pillow-11.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d2c0a187a92a1cb5ef2c8ed5412dd8d4334272617f532d4ad4de31e0495bd923"},
+    {file = "pillow-11.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:084a07ef0821cfe4858fe86652fffac8e187b6ae677e9906e192aafcc1b69903"},
+    {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8069c5179902dcdce0be9bfc8235347fdbac249d23bd90514b7a47a72d9fecf4"},
+    {file = "pillow-11.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f02541ef64077f22bf4924f225c0fd1248c168f86e4b7abdedd87d6ebaceab0f"},
+    {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:fcb4621042ac4b7865c179bb972ed0da0218a076dc1820ffc48b1d74c1e37fe9"},
+    {file = "pillow-11.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:00177a63030d612148e659b55ba99527803288cea7c75fb05766ab7981a8c1b7"},
+    {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8853a3bf12afddfdf15f57c4b02d7ded92c7a75a5d7331d19f4f9572a89c17e6"},
+    {file = "pillow-11.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3107c66e43bda25359d5ef446f59c497de2b5ed4c7fdba0894f8d6cf3822dafc"},
+    {file = "pillow-11.0.0-cp312-cp312-win32.whl", hash = "sha256:86510e3f5eca0ab87429dd77fafc04693195eec7fd6a137c389c3eeb4cfb77c6"},
+    {file = "pillow-11.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:8ec4a89295cd6cd4d1058a5e6aec6bf51e0eaaf9714774e1bfac7cfc9051db47"},
+    {file = "pillow-11.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:27a7860107500d813fcd203b4ea19b04babe79448268403172782754870dac25"},
+    {file = "pillow-11.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcd1fb5bb7b07f64c15618c89efcc2cfa3e95f0e3bcdbaf4642509de1942a699"},
+    {file = "pillow-11.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0e038b0745997c7dcaae350d35859c9715c71e92ffb7e0f4a8e8a16732150f38"},
+    {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ae08bd8ffc41aebf578c2af2f9d8749d91f448b3bfd41d7d9ff573d74f2a6b2"},
+    {file = "pillow-11.0.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d69bfd8ec3219ae71bcde1f942b728903cad25fafe3100ba2258b973bd2bc1b2"},
+    {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:61b887f9ddba63ddf62fd02a3ba7add935d053b6dd7d58998c630e6dbade8527"},
+    {file = "pillow-11.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:c6a660307ca9d4867caa8d9ca2c2658ab685de83792d1876274991adec7b93fa"},
+    {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:73e3a0200cdda995c7e43dd47436c1548f87a30bb27fb871f352a22ab8dcf45f"},
+    {file = "pillow-11.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fba162b8872d30fea8c52b258a542c5dfd7b235fb5cb352240c8d63b414013eb"},
+    {file = "pillow-11.0.0-cp313-cp313-win32.whl", hash = "sha256:f1b82c27e89fffc6da125d5eb0ca6e68017faf5efc078128cfaa42cf5cb38798"},
+    {file = "pillow-11.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:8ba470552b48e5835f1d23ecb936bb7f71d206f9dfeee64245f30c3270b994de"},
+    {file = "pillow-11.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:846e193e103b41e984ac921b335df59195356ce3f71dcfd155aa79c603873b84"},
+    {file = "pillow-11.0.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4ad70c4214f67d7466bea6a08061eba35c01b1b89eaa098040a35272a8efb22b"},
+    {file = "pillow-11.0.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6ec0d5af64f2e3d64a165f490d96368bb5dea8b8f9ad04487f9ab60dc4bb6003"},
+    {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c809a70e43c7977c4a42aefd62f0131823ebf7dd73556fa5d5950f5b354087e2"},
+    {file = "pillow-11.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:4b60c9520f7207aaf2e1d94de026682fc227806c6e1f55bba7606d1c94dd623a"},
+    {file = "pillow-11.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1e2688958a840c822279fda0086fec1fdab2f95bf2b717b66871c4ad9859d7e8"},
+    {file = "pillow-11.0.0-cp313-cp313t-win32.whl", hash = "sha256:607bbe123c74e272e381a8d1957083a9463401f7bd01287f50521ecb05a313f8"},
+    {file = "pillow-11.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c39ed17edea3bc69c743a8dd3e9853b7509625c2462532e62baa0732163a904"},
+    {file = "pillow-11.0.0-cp313-cp313t-win_arm64.whl", hash = "sha256:75acbbeb05b86bc53cbe7b7e6fe00fbcf82ad7c684b3ad82e3d711da9ba287d3"},
+    {file = "pillow-11.0.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2e46773dc9f35a1dd28bd6981332fd7f27bec001a918a72a79b4133cf5291dba"},
+    {file = "pillow-11.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:2679d2258b7f1192b378e2893a8a0a0ca472234d4c2c0e6bdd3380e8dfa21b6a"},
+    {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eda2616eb2313cbb3eebbe51f19362eb434b18e3bb599466a1ffa76a033fb916"},
+    {file = "pillow-11.0.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20ec184af98a121fb2da42642dea8a29ec80fc3efbaefb86d8fdd2606619045d"},
+    {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:8594f42df584e5b4bb9281799698403f7af489fba84c34d53d1c4bfb71b7c4e7"},
+    {file = "pillow-11.0.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:c12b5ae868897c7338519c03049a806af85b9b8c237b7d675b8c5e089e4a618e"},
+    {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:70fbbdacd1d271b77b7721fe3cdd2d537bbbd75d29e6300c672ec6bb38d9672f"},
+    {file = "pillow-11.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:5178952973e588b3f1360868847334e9e3bf49d19e169bbbdfaf8398002419ae"},
+    {file = "pillow-11.0.0-cp39-cp39-win32.whl", hash = "sha256:8c676b587da5673d3c75bd67dd2a8cdfeb282ca38a30f37950511766b26858c4"},
+    {file = "pillow-11.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:94f3e1780abb45062287b4614a5bc0874519c86a777d4a7ad34978e86428b8dd"},
+    {file = "pillow-11.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:290f2cc809f9da7d6d622550bbf4c1e57518212da51b6a30fe8e0a270a5b78bd"},
+    {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1187739620f2b365de756ce086fdb3604573337cc28a0d3ac4a01ab6b2d2a6d2"},
+    {file = "pillow-11.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:fbbcb7b57dc9c794843e3d1258c0fbf0f48656d46ffe9e09b63bbd6e8cd5d0a2"},
+    {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d203af30149ae339ad1b4f710d9844ed8796e97fda23ffbc4cc472968a47d0b"},
+    {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21a0d3b115009ebb8ac3d2ebec5c2982cc693da935f4ab7bb5c8ebe2f47d36f2"},
+    {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:73853108f56df97baf2bb8b522f3578221e56f646ba345a372c78326710d3830"},
+    {file = "pillow-11.0.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e58876c91f97b0952eb766123bfef372792ab3f4e3e1f1a2267834c2ab131734"},
+    {file = "pillow-11.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:224aaa38177597bb179f3ec87eeefcce8e4f85e608025e9cfac60de237ba6316"},
+    {file = "pillow-11.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:5bd2d3bdb846d757055910f0a59792d33b555800813c3b39ada1829c372ccb06"},
+    {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:375b8dd15a1f5d2feafff536d47e22f69625c1aa92f12b339ec0b2ca40263273"},
+    {file = "pillow-11.0.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:daffdf51ee5db69a82dd127eabecce20729e21f7a3680cf7cbb23f0829189790"},
+    {file = "pillow-11.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7326a1787e3c7b0429659e0a944725e1b03eeaa10edd945a86dead1913383944"},
+    {file = "pillow-11.0.0.tar.gz", hash = "sha256:72bacbaf24ac003fea9bff9837d1eedb6088758d41e100c1552930151f677739"},
 ]
 
 [package.extras]
-docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
+docs = ["furo", "olefile", "sphinx (>=8.1)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"]
 fpx = ["olefile"]
 mic = ["olefile"]
 tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
@@ -749,28 +835,29 @@ xmp = ["defusedxml"]
 
 [[package]]
 name = "platformdirs"
-version = "4.2.0"
-description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+version = "4.3.6"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"},
-    {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"},
+    {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"},
+    {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"},
 ]
 
 [package.extras]
-docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
-test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
+docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"]
+type = ["mypy (>=1.11.2)"]
 
 [[package]]
 name = "pluggy"
-version = "1.4.0"
+version = "1.5.0"
 description = "plugin and hook calling mechanisms for python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"},
-    {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"},
+    {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
+    {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
 ]
 
 [package.extras]
@@ -779,13 +866,13 @@ testing = ["pytest", "pytest-benchmark"]
 
 [[package]]
 name = "pyparsing"
-version = "3.1.2"
+version = "3.2.0"
 description = "pyparsing module - Classes and methods to define and execute parsing grammars"
 optional = false
-python-versions = ">=3.6.8"
+python-versions = ">=3.9"
 files = [
-    {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"},
-    {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"},
+    {file = "pyparsing-3.2.0-py3-none-any.whl", hash = "sha256:93d9577b88da0bbea8cc8334ee8b918ed014968fd2ec383e868fb8afb1ccef84"},
+    {file = "pyparsing-3.2.0.tar.gz", hash = "sha256:cbf74e27246d595d9a74b186b810f6fbb86726dbf3b9532efb343f6d7294fe9c"},
 ]
 
 [package.extras]
@@ -813,6 +900,23 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
 [package.extras]
 testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
 
+[[package]]
+name = "pytest-mock"
+version = "3.14.0"
+description = "Thin-wrapper around the mock package for easier use with pytest"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"},
+    {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"},
+]
+
+[package.dependencies]
+pytest = ">=6.2.5"
+
+[package.extras]
+dev = ["pre-commit", "pytest-asyncio", "tox"]
+
 [[package]]
 name = "python-dateutil"
 version = "2.9.0.post0"
@@ -829,56 +933,56 @@ six = ">=1.5"
 
 [[package]]
 name = "pytz"
-version = "2024.1"
+version = "2024.2"
 description = "World timezone definitions, modern and historical"
 optional = false
 python-versions = "*"
 files = [
-    {file = "pytz-2024.1-py2.py3-none-any.whl", hash = "sha256:328171f4e3623139da4983451950b28e95ac706e13f3f2630a879749e7a8b319"},
-    {file = "pytz-2024.1.tar.gz", hash = "sha256:2a29735ea9c18baf14b448846bde5a48030ed267578472d8955cd0e7443a9812"},
+    {file = "pytz-2024.2-py2.py3-none-any.whl", hash = "sha256:31c7c1817eb7fae7ca4b8c7ee50c72f93aa2dd863de768e1ef4245d426aa0725"},
+    {file = "pytz-2024.2.tar.gz", hash = "sha256:2aa355083c50a0f93fa581709deac0c9ad65cca8a9e9beac660adcbd493c798a"},
 ]
 
 [[package]]
 name = "scipy"
-version = "1.12.0"
+version = "1.13.1"
 description = "Fundamental algorithms for scientific computing in Python"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "scipy-1.12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:78e4402e140879387187f7f25d91cc592b3501a2e51dfb320f48dfb73565f10b"},
-    {file = "scipy-1.12.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:f5f00ebaf8de24d14b8449981a2842d404152774c1a1d880c901bf454cb8e2a1"},
-    {file = "scipy-1.12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e53958531a7c695ff66c2e7bb7b79560ffdc562e2051644c5576c39ff8efb563"},
-    {file = "scipy-1.12.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5e32847e08da8d895ce09d108a494d9eb78974cf6de23063f93306a3e419960c"},
-    {file = "scipy-1.12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4c1020cad92772bf44b8e4cdabc1df5d87376cb219742549ef69fc9fd86282dd"},
-    {file = "scipy-1.12.0-cp310-cp310-win_amd64.whl", hash = "sha256:75ea2a144096b5e39402e2ff53a36fecfd3b960d786b7efd3c180e29c39e53f2"},
-    {file = "scipy-1.12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:408c68423f9de16cb9e602528be4ce0d6312b05001f3de61fe9ec8b1263cad08"},
-    {file = "scipy-1.12.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5adfad5dbf0163397beb4aca679187d24aec085343755fcdbdeb32b3679f254c"},
-    {file = "scipy-1.12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3003652496f6e7c387b1cf63f4bb720951cfa18907e998ea551e6de51a04467"},
-    {file = "scipy-1.12.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b8066bce124ee5531d12a74b617d9ac0ea59245246410e19bca549656d9a40a"},
-    {file = "scipy-1.12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8bee4993817e204d761dba10dbab0774ba5a8612e57e81319ea04d84945375ba"},
-    {file = "scipy-1.12.0-cp311-cp311-win_amd64.whl", hash = "sha256:a24024d45ce9a675c1fb8494e8e5244efea1c7a09c60beb1eeb80373d0fecc70"},
-    {file = "scipy-1.12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e7e76cc48638228212c747ada851ef355c2bb5e7f939e10952bc504c11f4e372"},
-    {file = "scipy-1.12.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:f7ce148dffcd64ade37b2df9315541f9adad6efcaa86866ee7dd5db0c8f041c3"},
-    {file = "scipy-1.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c39f92041f490422924dfdb782527a4abddf4707616e07b021de33467f917bc"},
-    {file = "scipy-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a7ebda398f86e56178c2fa94cad15bf457a218a54a35c2a7b4490b9f9cb2676c"},
-    {file = "scipy-1.12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:95e5c750d55cf518c398a8240571b0e0782c2d5a703250872f36eaf737751338"},
-    {file = "scipy-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:e646d8571804a304e1da01040d21577685ce8e2db08ac58e543eaca063453e1c"},
-    {file = "scipy-1.12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:913d6e7956c3a671de3b05ccb66b11bc293f56bfdef040583a7221d9e22a2e35"},
-    {file = "scipy-1.12.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:bba1b0c7256ad75401c73e4b3cf09d1f176e9bd4248f0d3112170fb2ec4db067"},
-    {file = "scipy-1.12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:730badef9b827b368f351eacae2e82da414e13cf8bd5051b4bdfd720271a5371"},
-    {file = "scipy-1.12.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6546dc2c11a9df6926afcbdd8a3edec28566e4e785b915e849348c6dd9f3f490"},
-    {file = "scipy-1.12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:196ebad3a4882081f62a5bf4aeb7326aa34b110e533aab23e4374fcccb0890dc"},
-    {file = "scipy-1.12.0-cp39-cp39-win_amd64.whl", hash = "sha256:b360f1b6b2f742781299514e99ff560d1fe9bd1bff2712894b52abe528d1fd1e"},
-    {file = "scipy-1.12.0.tar.gz", hash = "sha256:4bf5abab8a36d20193c698b0f1fc282c1d083c94723902c447e5d2f1780936a3"},
+    {file = "scipy-1.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca"},
+    {file = "scipy-1.13.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f"},
+    {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989"},
+    {file = "scipy-1.13.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f"},
+    {file = "scipy-1.13.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94"},
+    {file = "scipy-1.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54"},
+    {file = "scipy-1.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9"},
+    {file = "scipy-1.13.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326"},
+    {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299"},
+    {file = "scipy-1.13.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa"},
+    {file = "scipy-1.13.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59"},
+    {file = "scipy-1.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b"},
+    {file = "scipy-1.13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1"},
+    {file = "scipy-1.13.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d"},
+    {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627"},
+    {file = "scipy-1.13.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884"},
+    {file = "scipy-1.13.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16"},
+    {file = "scipy-1.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949"},
+    {file = "scipy-1.13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5"},
+    {file = "scipy-1.13.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24"},
+    {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004"},
+    {file = "scipy-1.13.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d"},
+    {file = "scipy-1.13.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c"},
+    {file = "scipy-1.13.1-cp39-cp39-win_amd64.whl", hash = "sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2"},
+    {file = "scipy-1.13.1.tar.gz", hash = "sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c"},
 ]
 
 [package.dependencies]
-numpy = ">=1.22.4,<1.29.0"
+numpy = ">=1.22.4,<2.3"
 
 [package.extras]
-dev = ["click", "cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"]
-doc = ["jupytext", "matplotlib (>2)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (==0.9.0)", "sphinx (!=4.1.0)", "sphinx-design (>=0.2.0)"]
-test = ["asv", "gmpy2", "hypothesis", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
+dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pydevtool", "rich-click", "ruff", "types-psutil", "typing_extensions"]
+doc = ["jupyterlite-pyodide-kernel", "jupyterlite-sphinx (>=0.12.0)", "jupytext", "matplotlib (>=3.5)", "myst-nb", "numpydoc", "pooch", "pydata-sphinx-theme (>=0.15.2)", "sphinx (>=5.0.0)", "sphinx-design (>=0.4.0)"]
+test = ["array-api-strict", "asv", "gmpy2", "hypothesis (>=6.30)", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "scikit-umfpack", "threadpoolctl"]
 
 [[package]]
 name = "seaborn"
@@ -914,24 +1018,24 @@ files = [
 
 [[package]]
 name = "tomli"
-version = "2.0.1"
+version = "2.0.2"
 description = "A lil' TOML parser"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"},
-    {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
+    {file = "tomli-2.0.2-py3-none-any.whl", hash = "sha256:2ebe24485c53d303f690b0ec092806a085f07af5a5aa1464f3931eec36caaa38"},
+    {file = "tomli-2.0.2.tar.gz", hash = "sha256:d46d457a85337051c36524bc5349dd91b1877838e2979ac5ced3e710ed8a60ed"},
 ]
 
 [[package]]
 name = "tzdata"
-version = "2024.1"
+version = "2024.2"
 description = "Provider of IANA time zone data"
 optional = false
 python-versions = ">=2"
 files = [
-    {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
-    {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
+    {file = "tzdata-2024.2-py2.py3-none-any.whl", hash = "sha256:a48093786cdcde33cad18c2555e8532f34422074448fbc874186f0abd79565cd"},
+    {file = "tzdata-2024.2.tar.gz", hash = "sha256:7d85cc416e9382e69095b7bdf4afd9e3880418a2413feec7069d533d6b4e31cc"},
 ]
 
 [[package]]
@@ -952,20 +1056,24 @@ tomli = ">=2.0.1"
 
 [[package]]
 name = "zipp"
-version = "3.18.1"
+version = "3.21.0"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 optional = false
-python-versions = ">=3.8"
+python-versions = ">=3.9"
 files = [
-    {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"},
-    {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"},
+    {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"},
+    {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"},
 ]
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
-testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
+cover = ["pytest-cov"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+enabler = ["pytest-enabler (>=2.2)"]
+test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"]
+type = ["pytest-mypy"]
 
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.9"
-content-hash = "e3e024139bcf83ce86307d978adf2be8fdd3965a1df096691c3c2da0c935053c"
+content-hash = "7a8f30aaac9e77dfcd3cd051472f779e9a66d28ac08e094038ac28ae58a1a804"
diff --git a/pyproject.toml b/pyproject.toml
index 5e2b884..f4f25eb 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -23,6 +23,7 @@ seaborn = "^0.12"
 yapf = ">= 0.29"
 isort = "5.*"
 pytest = "^7.1"
+pytest-mock = ">= 3.14.0"
 
 [build-system]
 requires = ["poetry-core"]
diff --git a/tests/conftest.py b/tests/conftest.py
index cf83d88..6c1a25e 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,101 +1,8 @@
-import numpy as np
-import pytest
-
-from mbtrack2 import Bunch, Electron, Optics, Synchrotron
-
-
-@pytest.fixture
-def model_ring():
-    h = 416  # Harmonic number of the accelerator.
-    L = 353.97  # Ring circumference in [m].
-    E0 = 2.75e9  # Nominal (total) energy of the ring in [eV].
-    particle = Electron()  # Particle considered.
-    ac = 1.0695e-5  #1.0695e-4
-    U0 = 452.6e3  # Energy loss per turn in [eV].
-    tau = np.array([
-        7.68e-3, 14.14e-3, 12.18e-3
-    ])  #horizontal, vertical and longitudinal damping times in [s].
-    tune = np.array([54.2, 18.3])
-    emit = np.array([84.4e-12, 84.4e-13])
-    sigma_0 = 9e-12
-    sigma_delta = 9.07649e-4
-    chro = np.array([1.8, 1.3])
-    beta = np.array([3.288, 4.003])
-    alpha = np.array([0.004, 0.02])
-    dispersion = np.array([0.008, 0.01, 0.003, 0.001])
-    optics = Optics(local_beta=beta,
-                    local_alpha=alpha,
-                    local_dispersion=dispersion)
-    ring = Synchrotron(h=h,
-                       optics=optics,
-                       particle=particle,
-                       L=L,
-                       E0=E0,
-                       ac=ac,
-                       U0=U0,
-                       tau=tau,
-                       emit=emit,
-                       tune=tune,
-                       sigma_delta=sigma_delta,
-                       sigma_0=sigma_0,
-                       chro=chro)
-    return ring
-
-
-@pytest.fixture
-def local_optics():
-    beta = np.array([1, 1])
-    alpha = np.array([0, 0])
-    dispersion = np.array([0, 0, 0, 0])
-    local_optics = Optics(local_beta=beta,
-                          local_alpha=alpha,
-                          local_dispersion=dispersion)
-    return local_optics
-
-
-@pytest.fixture
-def demo_ring(local_optics):
-
-    h = 20
-    L = 100
-    E0 = 1e9
-    particle = Electron()
-    ac = 1e-3
-    U0 = 250e3
-    tau = np.array([10e-3, 10e-3, 5e-3])
-    tune = np.array([18.2, 10.3])
-    emit = np.array([50e-9, 50e-9 * 0.01])
-    sigma_0 = 30e-12
-    sigma_delta = 1e-3
-    chro = [1.0, 1.0]
-
-    ring = Synchrotron(h,
-                       local_optics,
-                       particle,
-                       L=L,
-                       E0=E0,
-                       ac=ac,
-                       U0=U0,
-                       tau=tau,
-                       emit=emit,
-                       tune=tune,
-                       sigma_delta=sigma_delta,
-                       sigma_0=sigma_0,
-                       chro=chro)
-
-    return ring
-
-
-@pytest.fixture
-def mybunch(demo_ring):
-    mp_number = 10
-    mybunch = Bunch(demo_ring, mp_number=mp_number, track_alive=True)
-    return mybunch
-
-
-@pytest.fixture
-def large_bunch(demo_ring):
-    mp_number = 1e5
-    large_bunch = Bunch(demo_ring, mp_number=mp_number, track_alive=True)
-    large_bunch.init_gaussian()
-    return large_bunch
+# Add modules here to access fixtures from other files
+pytest_plugins = [
+    "tests.unit.utilities.test_optics",
+    "tests.unit.tracking.test_synchrotron",
+    "tests.unit.tracking.test_parallel",
+    "tests.unit.tracking.test_particle",
+    "tests.unit.impedance.test_wakefield"
+]
diff --git a/tests/test_ibs.py b/tests/physics/test_ibs_phys.py
similarity index 67%
rename from tests/test_ibs.py
rename to tests/physics/test_ibs_phys.py
index edc93ca..a8a6689 100644
--- a/tests/test_ibs.py
+++ b/tests/physics/test_ibs_phys.py
@@ -12,6 +12,43 @@ from mbtrack2.tracking import (
 )
 from mbtrack2.utilities import Optics
 
+@pytest.fixture
+def model_ring():
+    h = 416  # Harmonic number of the accelerator.
+    L = 353.97  # Ring circumference in [m].
+    E0 = 2.75e9  # Nominal (total) energy of the ring in [eV].
+    particle = Electron()  # Particle considered.
+    ac = 1.0695e-5  #1.0695e-4
+    U0 = 452.6e3  # Energy loss per turn in [eV].
+    tau = np.array([
+        7.68e-3, 14.14e-3, 12.18e-3
+    ])  #horizontal, vertical and longitudinal damping times in [s].
+    tune = np.array([54.2, 18.3])
+    emit = np.array([84.4e-12, 84.4e-13])
+    sigma_0 = 9e-12
+    sigma_delta = 9.07649e-4
+    chro = np.array([1.8, 1.3])
+    beta = np.array([3.288, 4.003])
+    alpha = np.array([0.004, 0.02])
+    dispersion = np.array([0.008, 0.01, 0.003, 0.001])
+    optics = Optics(local_beta=beta,
+                    local_alpha=alpha,
+                    local_dispersion=dispersion)
+    ring = Synchrotron(h=h,
+                       optics=optics,
+                       particle=particle,
+                       L=L,
+                       E0=E0,
+                       ac=ac,
+                       U0=U0,
+                       tau=tau,
+                       emit=emit,
+                       tune=tune,
+                       sigma_delta=sigma_delta,
+                       sigma_0=sigma_0,
+                       chro=chro)
+    return ring
+
 
 def test_ibs_PS(model_ring):
     modelname = "PS"
@@ -29,11 +66,11 @@ def test_ibs_PS(model_ring):
                   Vc=1.8e6,
                   theta=np.arccos(model_ring.U0 / 1.8e6))
     sr = SynchrotronRadiation(model_ring, switch=[1, 1, 1])
-    ibs = IntrabeamScattering(model_ring,
-                              bunch,
-                              model=modelname,
-                              n_points=1000,
-                              n_bin=1)
+    with pytest.warns(UserWarning):
+        ibs = IntrabeamScattering(model_ring,
+                                  model=modelname,
+                                  n_points=1000,
+                                  n_bin=1)
     long_map.track(bunch)
     trans_map.track(bunch)
     rf.track(bunch)
@@ -66,11 +103,11 @@ def test_ibs_PM(model_ring):
                   Vc=1.8e6,
                   theta=np.arccos(model_ring.U0 / 1.8e6))
     sr = SynchrotronRadiation(model_ring, switch=[1, 1, 1])
-    ibs = IntrabeamScattering(model_ring,
-                              bunch,
-                              model=modelname,
-                              n_points=1000,
-                              n_bin=1)
+    with pytest.warns(UserWarning):
+        ibs = IntrabeamScattering(model_ring,
+                                  model=modelname,
+                                  n_points=1000,
+                                  n_bin=1)
     long_map.track(bunch)
     trans_map.track(bunch)
     rf.track(bunch)
@@ -103,11 +140,11 @@ def test_ibs_Bane(model_ring):
                   Vc=1.8e6,
                   theta=np.arccos(model_ring.U0 / 1.8e6))
     sr = SynchrotronRadiation(model_ring, switch=[1, 1, 1])
-    ibs = IntrabeamScattering(model_ring,
-                              bunch,
-                              model=modelname,
-                              n_points=1000,
-                              n_bin=1)
+    with pytest.warns(UserWarning):
+        ibs = IntrabeamScattering(model_ring,
+                                  model=modelname,
+                                  n_points=1000,
+                                  n_bin=1)
     long_map.track(bunch)
     trans_map.track(bunch)
     rf.track(bunch)
@@ -138,11 +175,11 @@ def test_ibs_CIMP(model_ring):
                   Vc=1.8e6,
                   theta=np.arccos(model_ring.U0 / 1.8e6))
     sr = SynchrotronRadiation(model_ring, switch=[1, 1, 1])
-    ibs = IntrabeamScattering(model_ring,
-                              bunch,
-                              model=modelname,
-                              n_points=1000,
-                              n_bin=1)
+    with pytest.warns(UserWarning):
+        ibs = IntrabeamScattering(model_ring,
+                                  model=modelname,
+                                  n_points=1000,
+                                  n_bin=1)
     long_map.track(bunch)
     trans_map.track(bunch)
     rf.track(bunch)
diff --git a/tests/test_bunch.py b/tests/test_bunch.py
deleted file mode 100644
index 25189d3..0000000
--- a/tests/test_bunch.py
+++ /dev/null
@@ -1,105 +0,0 @@
-import numpy as np
-import pytest
-from scipy.constants import e
-
-from mbtrack2 import Bunch
-
-
-def test_bunch_values(demo_ring):
-    mp_number = 10
-    current = 20e-3
-    mybunch = Bunch(demo_ring, mp_number=mp_number, current=current,
-                    track_alive=True)
-
-    assert mybunch.mp_number == mp_number
-    assert pytest.approx(mybunch.current) == current
-    assert len(mybunch) == mp_number
-    np.testing.assert_allclose(mybunch.alive, np.ones((mp_number,), dtype=bool))
-    assert pytest.approx(mybunch.charge) == current * demo_ring.T0
-    assert pytest.approx(mybunch.charge_per_mp) == current * demo_ring.T0 / mp_number
-    assert pytest.approx(mybunch.particle_number) == current * demo_ring.T0 / e
-    assert mybunch.is_empty == False
-
-def test_bunch_magic(mybunch):
-    for label in mybunch:
-        np.testing.assert_allclose(mybunch[label], np.zeros(len(mybunch)))
-        mybunch[label] = np.ones(len(mybunch))
-        np.testing.assert_allclose(mybunch[label], np.ones(len(mybunch)))
-
-def test_bunch_losses(mybunch):
-    charge_init = mybunch.charge
-    mybunch.alive[0] = False
-    assert len(mybunch) == mybunch.mp_number - 1
-    assert pytest.approx(mybunch.charge) == charge_init * len(mybunch) / mybunch.mp_number
-
-def test_bunch_init_gauss(large_bunch):
-    large_bunch.init_gaussian(mean=np.ones((6,)))
-    np.testing.assert_allclose(large_bunch.mean, np.ones((6,)), rtol=1e-2)
-
-def test_bunch_save_load(mybunch, demo_ring, tmp_path):
-    mybunch["x"] += 1
-    mybunch.save(str(tmp_path / "test"))
-
-    mybunch2 = Bunch(demo_ring, mp_number=1, current=1e-5)
-    mybunch2.load(str(tmp_path / "test.hdf5"))
-
-    assert mybunch.mp_number == mybunch2.mp_number
-    assert pytest.approx(mybunch.charge) == mybunch2.charge
-    for label in mybunch:
-        np.testing.assert_allclose(mybunch[label], mybunch2[label])
-
-def test_bunch_stats(demo_ring, large_bunch):
-    large_bunch.init_gaussian()
-    np.testing.assert_array_almost_equal(large_bunch.mean, np.zeros((6,)), decimal=5)
-    sig = np.concatenate((demo_ring.sigma(), [demo_ring.sigma_0, demo_ring.sigma_delta]))
-    np.testing.assert_allclose(large_bunch.std, sig, rtol=1e-2)
-    np.testing.assert_allclose(large_bunch.emit[:2], demo_ring.emit, rtol=1e-2)
-    np.testing.assert_allclose(large_bunch.cs_invariant[:2], demo_ring.emit*2, rtol=1e-2)
-
-def test_bunch_binning(mybunch):
-    mybunch.init_gaussian()
-    (bins, sorted_index, profile, center) = mybunch.binning()
-    profile0 = np.zeros((len(bins)-1,))
-    for i, val in enumerate(sorted_index):
-        assert bins[val] <= mybunch["tau"][i] <= bins[val+1]
-        profile0[val] += 1
-    np.testing.assert_allclose(profile0, profile)
-
-def test_bunch_plots(mybunch):
-    mybunch.init_gaussian()
-    mybunch.plot_phasespace()
-    mybunch.plot_profile()
-    assert True
-def test_bunch_emittance(demo_ring):
-    mp_number = 1_000_000
-    current = 1.2e-3
-    mybunch = Bunch(demo_ring, mp_number=mp_number, current=current,
-                    track_alive=False)
-    mybunch.init_gaussian()
-    np.testing.assert_allclose(mybunch.emit[0], demo_ring.emit[0], rtol=1e-2, atol=0,
-     err_msg=f'Emittances do not match. {demo_ring.emit[0]} initialised, {mybunch.emit[0]:} calculated')
-    np.testing.assert_allclose(mybunch.emit[1], demo_ring.emit[1], rtol=1e-2, atol=0,
-     err_msg=f'Emittances do not match. {demo_ring.emit[1]} initialised, {mybunch.emit[1]:} calculated')
-
-    np.testing.assert_allclose(mybunch.emit[0], mybunch.cs_invariant[0]/2, rtol=1e-2, atol=0,
-     err_msg=f'Emittances do not match. {mybunch.cs_invariant[0]/2} calculated with optics functions, {mybunch.emit[0]:} calculated with coordinates only')
-    np.testing.assert_allclose(mybunch.emit[1], mybunch.cs_invariant[1]/2, rtol=1e-2, atol=0,
-     err_msg=f'Emittances do not match. {mybunch.cs_invariant[1]/2} calculated with optics functions, {mybunch.emit[1]:} calculated with coordinates only')
-
-
-def test_bunch_emittance_with_dispersion(demo_ring):
-    mp_number = 1_000_000
-    current = 1.2e-3
-    demo_ring.optics.local_dispersion = np.array([1e-2, 1e-3, 1e-2, 1e-3])
-    mybunch = Bunch(demo_ring, mp_number=mp_number, current=current,
-                    track_alive=False)
-    mybunch.init_gaussian()
-    np.testing.assert_allclose(mybunch.emit[0], demo_ring.emit[0], rtol=1e-2, atol=0,
-     err_msg=f'Emittances do not match. {demo_ring.emit[0]} initialised, {mybunch.emit[0]:} calculated')
-    np.testing.assert_allclose(mybunch.emit[1], demo_ring.emit[1], rtol=1e-2, atol=0,
-     err_msg=f'Emittances do not match. {demo_ring.emit[1]} initialised, {mybunch.emit[1]:} calculated')
-
-    np.testing.assert_allclose(mybunch.emit[0], mybunch.cs_invariant[0]/2, rtol=1e-2, atol=0,
-     err_msg=f'Emittances do not match. {mybunch.cs_invariant[0]/2} calculated with optics functions, {mybunch.emit[0]:} calculated with coordinates only')
-    np.testing.assert_allclose(mybunch.emit[1], mybunch.cs_invariant[1]/2, rtol=1e-2, atol=0,
-     err_msg=f'Emittances do not match. {mybunch.cs_invariant[1]/2} calculated with optics functions, {mybunch.emit[1]:} calculated with coordinates only')
diff --git a/tests/test_optics.py b/tests/test_optics.py
deleted file mode 100644
index ceabcba..0000000
--- a/tests/test_optics.py
+++ /dev/null
@@ -1,17 +0,0 @@
-import numpy as np
-import pytest
-
-from mbtrack2 import Optics
-
-
-def test_local_optics():
-    beta = np.array([1, 1])
-    alpha = np.array([0, 0])
-    dispersion = np.array([0, 0, 0, 0])
-    gamma = (1 + alpha**2) / beta
-    optics = Optics(local_beta=beta, local_alpha=alpha, 
-                      local_dispersion=dispersion)
-    np.testing.assert_allclose(optics.local_beta, beta)
-    np.testing.assert_allclose(optics.local_alpha, alpha)
-    np.testing.assert_allclose(optics.local_dispersion, dispersion)
-    np.testing.assert_allclose(optics.local_gamma, gamma)
\ No newline at end of file
diff --git a/tests/test_synchrotron.py b/tests/test_synchrotron.py
deleted file mode 100644
index ad152a3..0000000
--- a/tests/test_synchrotron.py
+++ /dev/null
@@ -1,63 +0,0 @@
-import numpy as np
-import pytest
-from scipy.constants import c, e
-
-from mbtrack2 import Electron, Synchrotron
-
-
-def test_synchrotron_values(local_optics):
-    h = 20
-    L = 100
-    E0 = 1e9
-    particle = Electron()
-    ac = 1e-3
-    U0 = 250e3
-    tau = np.array([10e-3, 10e-3, 5e-3])
-    tune = np.array([18.2, 10.3])
-    emit = np.array([50e-9, 50e-9*0.01])
-    sigma_0 = 30e-12
-    sigma_delta = 1e-3
-    chro = [1.0,1.0]
-    
-    ring = Synchrotron(h, local_optics, particle, L=L, E0=E0, ac=ac, U0=U0, tau=tau,
-                       emit=emit, tune=tune, sigma_delta=sigma_delta, 
-                       sigma_0=sigma_0, chro=chro)
-    assert pytest.approx(ring.h) == h
-    assert pytest.approx(ring.L) == L
-    assert pytest.approx(ring.E0) == E0
-    assert pytest.approx(ring.U0) == U0
-    assert pytest.approx(ring.ac) == ac
-    np.testing.assert_allclose(ring.tau, tau)
-    np.testing.assert_allclose(ring.tune, tune)
-    np.testing.assert_allclose(ring.emit, emit)
-    assert pytest.approx(ring.sigma_0) == sigma_0
-    assert pytest.approx(ring.sigma_delta) == sigma_delta
-    np.testing.assert_allclose(ring.chro, chro)
-    assert pytest.approx(ring.T0) == L/c
-    assert pytest.approx(ring.T1) == L/c/h
-    assert pytest.approx(ring.f0) == c/L
-    assert pytest.approx(ring.f1) == 1/(L/c/h)
-    assert pytest.approx(ring.omega0) == 2 * np.pi * c/L
-    assert pytest.approx(ring.omega1) == 2 * np.pi * 1/(L/c/h)
-    assert pytest.approx(ring.k1) == 2 * np.pi * 1/(L/c/h) / c
-    assert pytest.approx(ring.gamma) == E0 / (particle.mass * c**2 / e)
-    assert pytest.approx(ring.beta) == np.sqrt(1 - (E0 / (particle.mass * c**2 / e))**-2)
-    
-def test_synchrotron_mcf(demo_ring):
-    demo_ring.mcf_order = [5e-4, 1e-4, 1e-3]
-    assert pytest.approx(demo_ring.mcf(0.5)) == 5e-4*(0.5**2) + 1e-4*0.5 + 1e-3
-    assert pytest.approx(demo_ring.eta(0.5)) == demo_ring.mcf(0.5) - 1 / (demo_ring.gamma**2)
-    
-def test_synchrotron_tune(demo_ring):
-    tuneS = demo_ring.synchrotron_tune(1e6)
-    assert pytest.approx(tuneS, rel=1e-4) == 0.0017553
-    
-def test_synchrotron_long_twiss(demo_ring):
-    tuneS, long_alpha, long_beta, long_gamma = demo_ring.get_longitudinal_twiss(1e6, add=False)
-    assert pytest.approx(tuneS, rel=1e-4) == demo_ring.synchrotron_tune(1e6)
-    assert pytest.approx(long_alpha, rel=1e-4) == -0.0055146
-    assert pytest.approx(long_beta, rel=1e-4) == 3.0236e-08
-    assert pytest.approx(long_gamma, rel=1e-4) == 3.30736e7
-    
-def test_synchrotron_sigma(demo_ring):
-    np.testing.assert_allclose(demo_ring.sigma(), np.array([2.23606798e-04, 2.23606798e-04, 2.23606798e-05, 2.23606798e-05]))
\ No newline at end of file
diff --git a/tests/unit/impedance/test_impedance_model.py b/tests/unit/impedance/test_impedance_model.py
new file mode 100644
index 0000000..02e07cf
--- /dev/null
+++ b/tests/unit/impedance/test_impedance_model.py
@@ -0,0 +1,266 @@
+
+from mbtrack2 import ImpedanceModel, ComplexData, WakeField
+import pandas as pd
+import numpy as np
+
+component_list = ComplexData.name_and_coefficients_table().columns.to_list()
+
+
+import pytest
+
+class TestImpedanceModel:
+
+    # Adding WakeField objects with unique names should succeed without errors
+    @pytest.mark.parametrize('pos1,pos2', [([0],[1]), (0,1), (0.5,1.5)])
+    def test_add_wakefield(self, 
+                                        ring_with_at_lattice, 
+                                        generate_wakefield,
+                                        pos1,
+                                        pos2):
+        model = ImpedanceModel(ring_with_at_lattice)
+        wakefield1 = generate_wakefield(name="wake1")
+        wakefield2 = generate_wakefield(name="wake2")
+        model.add(wakefield1, pos1)
+        model.add(wakefield2, pos2)
+        assert "wake1" in model.names
+        assert "wake2" in model.names
+        assert wakefield1 in model.wakefields
+        assert wakefield2 in model.wakefields
+        assert pos1 in model.positions
+        assert pos2 in model.positions
+
+    # Adding global WakeField objects should correctly append them to the globals list
+    def test_add_global_wakefield(self, ring_with_at_lattice, generate_wakefield):
+        model = ImpedanceModel(ring_with_at_lattice)
+        global_wakefield = generate_wakefield(name = "global_wake")
+        model.add_global(global_wakefield)
+        assert "global_wake" in model.globals_names
+        assert global_wakefield in model.globals
+    
+    # Test beta weighting summation
+    @pytest.mark.parametrize('comp,expected',
+                             [('long', np.array([3, 6])),
+                              ('xcst', np.array([(2**0.5 + 4**0.5 + 6**0.5)/2, (2**0.5 + 4**0.5 + 6**0.5)/2*2])),
+                              ('ycst', np.array([(1**0.5 + 2**0.5 + 3**0.5)/1, (1**0.5 + 2**0.5 + 3**0.5)/1*2])),
+                              ('xdip', np.array([6, 12])),
+                              ('ydip', np.array([6, 12])),
+                              ('xquad', np.array([6, 12])),
+                              ('yquad', np.array([6, 12]))])
+    def test_sum_beta(self, demo_ring, generate_wakefield, comp, expected):
+        
+        wake_in = generate_wakefield(
+            wake_function_params=[{'component_type': comp,
+                                   'function': np.array([1, 2])}],
+            impedance_params=[{'component_type': comp,
+                                   'function': np.array([1, 2])}])
+        
+        demo_ring.optics.local_beta = np.array([2,1])
+        model = ImpedanceModel(demo_ring)
+        model.add(wake_in, 0, name="test")
+        beta = np.array([[2,4,6],[1,2,3]])
+        result_wake = model.sum_beta(model.wakefields[0], beta)
+        wf = getattr(result_wake, f'W{comp}')
+        z = getattr(result_wake, f'Z{comp}')
+        assert np.allclose(wf.data["real"], expected)
+        assert np.allclose(z.data["real"], expected)
+        
+    def test_compute_sum_names(self, ring_with_at_lattice, generate_wakefield):
+        wake1 = generate_wakefield(name="wake1")
+        wake2 = generate_wakefield(name="wake2")
+        model = ImpedanceModel(ring_with_at_lattice)
+        model.add(wake1, [1,2,3])
+        model.add(wake2, [5,6,7])
+        model.compute_sum_names()
+        assert hasattr(model, 'sum_wake1')
+        assert hasattr(model, 'sum_wake2')
+        assert 'sum_wake1' in model.sum_names
+        assert 'sum_wake2' in model.sum_names
+
+    # Computing the sum of WakeFields should correctly aggregate all components
+    def test_compute_sum(self, ring_with_at_lattice, wakefield_param):
+        model = ImpedanceModel(ring_with_at_lattice)
+        model.add(wakefield_param, [0], "wake1")
+        model.add(wakefield_param, [1], "wake2")
+        model.compute_sum()
+        assert hasattr(model, 'sum')
+        for comp in wakefield_param.components:
+            assert comp in model.sum.components
+
+    # Saving the ImpedanceModel to a file should serialize all relevant attributes
+    def test_save_impedance_model(self, tmp_path, ring_with_at_lattice, generate_wakefield):
+        model = ImpedanceModel(ring_with_at_lattice)
+        wakefield = generate_wakefield(name="wake")
+        model.add(wakefield, [0])
+        model.compute_sum()
+        file_path = tmp_path / "impedance_model.pkl"
+        model.save(file_path)
+        assert file_path.exists()
+
+    # Loading the ImpedanceModel from a file should restore all attributes accurately
+    def test_load_impedance_model(self, tmp_path, ring_with_at_lattice, generate_wakefield):
+        model = ImpedanceModel(ring_with_at_lattice)
+        wakefield = generate_wakefield(name="wake")
+        global_wake = generate_wakefield(name="global_wake")
+        model.add(wakefield, [0])
+        model.add_global(global_wake)
+        model.compute_sum()
+        file_path = tmp_path / "impedance_model.pkl"
+        model.save(file_path)
+    
+        new_model = ImpedanceModel(ring_with_at_lattice)
+        new_model.load(file_path)
+    
+        assert "wake" in new_model.names
+        assert isinstance(new_model.wakefields[0], WakeField)
+        assert [0] in new_model.positions
+        assert "global_wake" in new_model.globals_names
+        assert isinstance(new_model.globals[0], WakeField)
+
+    # Plotting the contributions of WakeFields should generate a valid plot
+    @pytest.mark.parametrize('comp',[('long'),('xdip'),('ydip'),('xquad'),('yquad')])
+    def test_plot_area(self, ring_with_at_lattice, generate_wakefield, comp):
+        model = ImpedanceModel(ring_with_at_lattice)
+        wake = generate_wakefield(
+            impedance_params=[{'component_type': comp}],
+            name="wake"
+            )
+        model.add(wake, [0])
+        model.compute_sum()
+        fig = model.plot_area(Z_type=f"Z{comp}", sigma=30e-12, zoom=True)
+        assert fig is not None
+
+    # Adding a WakeField with a duplicate name should raise a ValueError
+    def test_add_duplicate_name_raises_valueerror(self, ring_with_at_lattice, generate_wakefield):
+        model = ImpedanceModel(ring_with_at_lattice)
+        wakefield = generate_wakefield()
+        wakefield.name = "duplicate"
+    
+        model.add(wakefield, [0], "duplicate")
+    
+        with pytest.raises(ValueError):
+            model.add(wakefield, [1], "duplicate")
+
+    # Adding a global WakeField with a duplicate name should raise a ValueError
+    def test_add_global_duplicate_name_raises_valueerror(self, ring_with_at_lattice, generate_wakefield):
+        model = ImpedanceModel(ring_with_at_lattice)
+        global_wakefield = generate_wakefield()
+        global_wakefield.name = "global_duplicate"
+    
+        model.add_global(global_wakefield, "global_duplicate")
+    
+        with pytest.raises(ValueError):
+            model.add_global(global_wakefield, "global_duplicate")
+
+    # Renaming an attribute should update the internal dictionary correctly
+    def test_rename_attribute(self, ring_with_at_lattice):
+        model = ImpedanceModel(ring_with_at_lattice)
+    
+        model.some_attribute = 123
+    
+        model.rename_attribute("some_attribute", "new_attribute_name")
+    
+        assert hasattr(model, "new_attribute_name")
+        assert not hasattr(model, "some_attribute")
+
+    # Grouping attributes should correctly combine specified properties
+    def test_group_attributes(self, demo_ring, generate_wakefield):
+        # Setup
+        model = ImpedanceModel(demo_ring)
+    
+        # Create WakeFields with different properties
+        wake1 = generate_wakefield(impedance_params=[{'component_type': 'long'}, 
+                                                     {'component_type': 'xdip'}])
+        wake2 = generate_wakefield(impedance_params=[{'component_type': 'long'}, 
+                                                     {'component_type': 'xdip'}])    
+        # Add WakeFields to the model
+        model.add(wake1, positions=[0.0], name='wake1')
+        model.add(wake2, positions=[1.0], name='wake2')
+    
+        # Compute sum to initialize sum_names
+        model.compute_sum()
+    
+        # Group attributes with a common property
+        model.group_attributes('wake', property_list=['Zlong', 'Zxdip'])
+    
+        # Assertions
+        assert 'wake' in model.sum_names
+        assert not hasattr(model, 'sum_wake1')
+        assert not hasattr(model, 'sum_wake2')
+    
+        # Check if the grouped attribute has combined properties
+        grouped_wake = getattr(model, 'wake')
+        assert hasattr(grouped_wake, 'Zlong')
+        assert hasattr(grouped_wake, 'Zxdip')
+        
+    # Grouping attributes should correctly combine specified properties
+    # def test_group_attributes_combines_properties(self, demo_ring, generate_wakefield):
+    #     # Setup
+    #     model = ImpedanceModel(demo_ring)
+    
+    #     # Create WakeFields with different properties
+    #     wake1 = generate_wakefield(impedance_params=[{'component_type': 'long'}, 
+    #                                                  {'component_type': 'xdip'}])
+    #     wake2 = generate_wakefield(impedance_params=[{'component_type': 'long'}])    
+    #     # Add WakeFields to the model
+    #     model.add(wake1, positions=[0.0], name='wake1')
+    #     model.add(wake2, positions=[1.0], name='wake2')
+    
+    #     # Compute sum to initialize sum_names
+    #     model.compute_sum()
+    
+    #     # Group attributes with a common property
+    #     model.group_attributes('wake', property_list=['Zlong', 'Zxdip'])
+    
+    #     # Assertions
+    #     assert 'wake' in model.sum_names
+    #     assert not hasattr(model, 'sum_wake1')
+    #     assert not hasattr(model, 'sum_wake2')
+    
+    #     # Check if the grouped attribute has combined properties
+    #     grouped_wake = getattr(model, 'wake')
+    #     assert hasattr(grouped_wake, 'Zlong')
+    #     assert hasattr(grouped_wake, 'Zxdip')
+
+    def test_power_loss_spectrum(self, demo_ring, generate_wakefield):
+        # Setup
+        wakefield = generate_wakefield(impedance_params=[{'component_type': 'long'}])
+        model = ImpedanceModel(demo_ring)
+        model.add(wakefield, positions=[0], name='test_wakefield')
+        model.compute_sum()
+
+        # Parameters
+        M = 100
+        bunch_spacing = 1e-9
+        I = 0.5
+        sigma = 1e-9
+
+        # Test without max_overlap
+        pf0, power_loss = model.power_loss_spectrum(M, bunch_spacing, I, sigma=sigma)
+
+        # Assertions
+        assert len(pf0) == len(power_loss)
+        assert np.all(power_loss > 0)
+
+    # The energy_loss method should compute correctly with minimal input data
+    def test_energy_loss(self, demo_ring, generate_wakefield):
+        # Setup
+        wakefield = generate_wakefield(
+            impedance_params=[{'component_type': 'long'}]
+        )
+        model = ImpedanceModel(demo_ring)
+        model.add(wakefield, positions=[0], name='test_wakefield')
+        model.compute_sum()
+
+        # Parameters for energy_loss
+        M = 1  # Minimal number of bunches
+        bunch_spacing = 1.0  # Arbitrary non-zero spacing
+        I = 0.1  # Minimal non-zero current
+        sigma = 1e-12  # Minimal non-zero sigma
+
+        # Execute
+        summary = model.energy_loss(M, bunch_spacing, I, sigma=sigma)
+
+        # Verify
+        assert not summary.empty, "The summary should not be empty"
+        assert "loss factor (beam) [V/pC]" in summary.columns, "Expected column missing"
+        assert "loss factor (bunch) [V/pC]" in summary.columns, "Expected column missing"
\ No newline at end of file
diff --git a/tests/unit/impedance/test_wakefield.py b/tests/unit/impedance/test_wakefield.py
new file mode 100644
index 0000000..3e22aa0
--- /dev/null
+++ b/tests/unit/impedance/test_wakefield.py
@@ -0,0 +1,321 @@
+import numpy as np
+import pytest
+from mbtrack2 import ComplexData, WakeFunction, Impedance, WakeField
+
+component_list = ComplexData.name_and_coefficients_table().columns.to_list()
+
+class TestComplexData:
+    
+    @pytest.fixture
+    def complex_data(self):
+        complex_data = ComplexData(variable=np.array([0, 1]), function=np.array([1+1j, 2+2j]))
+        return complex_data
+
+    # Add two ComplexData objects using 'zero' method and check resulting DataFrame
+    def test_add_zero_method(self, complex_data):
+        complex_data2 = ComplexData(variable=np.array([0, 1]), function=np.array([3+3j, 4+4j]))
+        result = complex_data + complex_data2
+        expected_real = np.array([4, 6])
+        expected_imag = np.array([4, 6])
+        assert np.allclose(result.data['real'], expected_real)
+        assert np.allclose(result.data['imag'], expected_imag)
+
+    # Multiply ComplexData by a scalar and verify the resulting DataFrame
+    def test_multiply_by_scalar(self, complex_data):
+        result = complex_data * 2
+        expected_real = np.array([2, 4])
+        expected_imag = np.array([2, 4])
+        assert np.allclose(result.data['real'], expected_real)
+        assert np.allclose(result.data['imag'], expected_imag)
+
+    # Interpolate ComplexData using __call__ and verify the interpolated values
+    def test_interpolation_call(self):
+        complex_data = ComplexData(variable=np.array([0, 0.25, 0.75, 1]), 
+                                   function=np.array([1+1j, 1.25+1.25j, 1.75+1.75j, 2+2j]))
+        interpolated_values = complex_data(np.array([0.5]))
+        expected_values = np.array([1.5 + 1.5j])
+        assert np.allclose(interpolated_values, expected_values)
+
+    # Set component_type and verify coefficients and plane attributes
+    def test_set_component_type(self):
+        complex_data = ComplexData()
+        complex_data.component_type = 'xdip'
+        assert complex_data.a == 1
+        assert complex_data.b == 0
+        assert complex_data.c == 0
+        assert complex_data.d == 0
+        assert complex_data.plane == 'x'
+
+    # Multiply ComplexData by a non-numeric type and expect a warning
+    def test_multiply_by_non_numeric_warning(self):
+        complex_data = ComplexData()
+        with pytest.warns(UserWarning):
+            result = complex_data.multiply('non-numeric')
+            assert result is complex_data
+
+    # Interpolate ComplexData with values outside the index range
+    def test_interpolation_outside_range(self, complex_data):
+        with pytest.raises(ValueError):
+            complex_data(np.array([-0.5, 1.5]))
+            
+    # Interpolate ComplexData with values outside the index range
+    def test_wrong_component_type(self, complex_data):
+        with pytest.raises(KeyError):
+            complex_data.component_type = "wrong"
+
+@pytest.fixture(params=component_list)
+def wf_param(request):
+    wf = WakeFunction(variable=np.array([0, 1]), function=np.array([1, 2]), component_type=request.param)
+    return wf
+
+@pytest.fixture
+def generate_wf():
+    def generate(variable=np.array([0, 1]),
+                 function=np.array([1, 2]),
+                 component_type='long',
+                 ):
+        wf = WakeFunction(variable=variable,
+                          function=function, 
+                          component_type=component_type)
+        return wf
+    return generate
+
+class TestWakeFunction:
+
+    # Add two WakeFunction objects with the same component type and verify result
+    def test_add_same_component_type(self, generate_wf):
+        wf1 = generate_wf()
+        wf2 = generate_wf(function=np.array([3, 4]))
+        result = wf1 + wf2
+        assert np.allclose(result.data['real'], [4, 6])
+        assert result.component_type == 'long'
+
+    # Multiply WakeFunction by a scalar and verify the result
+    def test_multiply_by_scalar(self, generate_wf):
+        wf = generate_wf()
+        result = wf * 2
+        assert np.allclose(result.data['real'], [2, 4])
+        assert result.component_type == 'long'
+        
+    # Add WakeFunction objects with different component types and check for warnings
+    def test_add_different_component_types_warning(self, generate_wf):
+        wf1 = generate_wf(component_type='long')
+        wf2 = generate_wf(component_type='xdip')
+        with pytest.warns(UserWarning, match="do not have the same coordinates or plane or type"):
+            result = wf1 + wf2
+        assert result.data.equals(wf1.data)
+
+    # Convert WakeFunction to Impedance and verify the transformation
+    def test_convert_to_impedance(self, generate_wf):
+        for component_type in ["long","xdip","ydip"]:
+            wf = generate_wf(component_type=component_type)
+            imp = wf.to_impedance(freq_lim=10)
+            assert imp.component_type == component_type
+            assert isinstance(imp, Impedance)
+        
+    # Test deconvolution
+    def test_deconvolution(self, generate_wf):
+        for component_type in ["long","xdip","ydip"]:
+            wf = generate_wf(component_type=component_type)
+            deconv_wf = wf.deconvolution(freq_lim=1e9, sigma=0.01, mu=0)
+            assert isinstance(deconv_wf, WakeFunction)
+            assert deconv_wf.component_type == component_type
+
+    # Plot the WakeFunction data and verify the plot output
+    def test_plot_output(self, wf_param):
+        wf_param.plot()
+        assert True
+
+    # Plot the WakeFunction.loss_factor
+    def test_loss_factor(self, wf_param):
+        assert wf_param.loss_factor(1) > 0
+        
+@pytest.fixture(params=component_list)
+def imp_param(request):
+    imp = Impedance(variable=np.array([1, 2]), function=np.array([3+4j, 5+6j]), component_type=request.param)
+    return imp
+
+@pytest.fixture
+def generate_imp():
+    def generate(variable=np.array([1, 2]),
+                 function=np.array([3+4j, 5+6j]),
+                 component_type='long',
+                 ):
+        imp = Impedance(variable=variable,
+                        function=function, 
+                        component_type=component_type)
+        return imp
+    return generate
+        
+class TestImpedance:
+
+    # Impedance objects can be added using the add method with matching component types
+    def test_add_matching_component_types(self, generate_imp):
+        imp1 = generate_imp()
+        imp2 = generate_imp(function=np.array([1+1j, 1+1j]))
+        result = imp1 + imp2
+        expected_real = np.array([4, 6])
+        expected_imag = np.array([5, 7])
+        assert np.allclose(result.data['real'], expected_real)
+        assert np.allclose(result.data['imag'], expected_imag)
+        assert result.component_type == 'long'
+
+    # Impedance objects can be multiplied by a scalar using the multiply method
+    def test_multiply_by_scalar(self, generate_imp):
+        imp = generate_imp()
+        result = imp * 2
+        expected_real = np.array([6, 10])
+        expected_imag = np.array([8, 12])
+        assert np.allclose(result.data['real'], expected_real)
+        assert np.allclose(result.data['imag'], expected_imag)
+        assert result.component_type == 'long'
+
+    # The loss_factor method computes the correct loss factor for a given sigma
+    def test_loss_factor_computation(self, generate_imp):
+        for component_type in ["long","xdip","ydip","xquad","yquad"]:
+            imp = generate_imp(component_type=component_type)
+            assert imp.loss_factor(1) > 0
+
+    # The to_wakefunction method correctly transforms impedance data to a WakeFunction object
+    def test_to_wakefunction_transformation(self, generate_imp):
+        for component_type in ["long","xdip","ydip"]:
+            imp = generate_imp(component_type=component_type)
+            wf = imp.to_wakefunction()
+            assert isinstance(wf, WakeFunction)
+            assert wf.component_type == component_type
+
+    # The plot method generates a plot of the impedance data with appropriate labels
+    def test_plot_generation(self, imp_param):
+        imp_param.plot()
+        assert True
+
+@pytest.fixture
+def generate_wakefield(generate_imp, generate_wf):
+    def generate(impedance_params=None, wake_function_params=None, name=None):
+        structure_list = []
+        if impedance_params:
+            for params in impedance_params:
+                structure_list.append(generate_imp(**params))
+        if wake_function_params:
+            for params in wake_function_params:
+                structure_list.append(generate_wf(**params))
+        return WakeField(structure_list=structure_list, name=name)
+    return generate
+
+@pytest.fixture(params=component_list)
+def wakefield_param(generate_wakefield, request):
+    wake = generate_wakefield(
+        impedance_params=[{'component_type': request.param}],
+        wake_function_params=[{'component_type': request.param}]
+        )
+    return wake
+
+class TestWakeField:
+
+    def test_initialize_with_impedance_wakefunction_list(self, generate_wakefield):
+        for component_type in component_list:
+            wakefield = generate_wakefield(
+                impedance_params=[{'component_type': component_type}],
+                wake_function_params=[{'component_type': component_type}]
+            )
+        assert f'Z{component_type}' in wakefield.components
+        assert f'W{component_type}' in wakefield.components
+
+    def test_append_to_model(self, generate_imp, generate_wakefield, generate_wf):
+        wakefield = generate_wakefield()
+        for component_type in component_list:
+            imp = generate_imp(component_type=component_type)
+            wf = generate_wf(component_type=component_type)
+            wakefield.append_to_model(imp)
+            assert f'Z{component_type}' in wakefield.components
+            wakefield.append_to_model(wf)
+            assert f'W{component_type}' in wakefield.components
+
+    def test_save_and_load_wakefield(self, generate_wakefield, tmp_path):
+        wakefield = generate_wakefield(
+            impedance_params=[{'component_type': 'long'}]
+        )
+        file_path = tmp_path / "wakefield.pkl"
+        wakefield.save(file_path)
+        loaded_wakefield = WakeField.load(file_path)
+        assert 'Zlong' in loaded_wakefield.components
+
+    @pytest.mark.parametrize('comp,expected',
+                             [('long', np.array([4, 6])),
+                              ('xcst', np.array([3 + np.sqrt(2), 4 + 2*np.sqrt(2)])),
+                              ('ycst', np.array([3 + np.sqrt(2), 4 + 2*np.sqrt(2)])),
+                              ('xdip', np.array([5, 8])),
+                              ('ydip', np.array([5, 8])),
+                              ('xquad', np.array([5, 8])),
+                              ('yquad', np.array([5, 8]))]
+        )
+    def test_add_wakefields_with_beta_functions(self, 
+                                                generate_wakefield,
+                                                comp,
+                                                expected):
+        wake1 = generate_wakefield(
+            wake_function_params=[{'component_type': comp,
+                                   'function': np.array([1, 2])}]
+        )
+        wake2 = generate_wakefield(
+            wake_function_params=[{'component_type': comp,
+                                   'function': np.array([3, 4])}]
+        )
+        beta1 = [2, 2]
+        beta2 = [1, 1]
+        
+        result_wake = WakeField.add_wakefields(wake1, beta1, wake2, beta2)
+        wf = getattr(result_wake, f'W{comp}')
+        assert np.allclose(wf.data["real"], expected)
+        
+        result_wake = WakeField.add_several_wakefields([wake1, wake2], np.array([beta1, beta2]))
+        wf = getattr(result_wake, f'W{comp}')
+        assert np.allclose(wf.data["real"], expected)
+
+    def test_add_duplicate_components_raises_error(self, generate_wakefield):
+        wakefield = generate_wakefield(
+            impedance_params=[{'component_type': 'long'}]
+        )
+        with pytest.raises(ValueError):
+            wakefield.append_to_model(wakefield.Zlong)
+
+    def test_drop_non_existent_component_raises_error(self, generate_wakefield):
+        wakefield = generate_wakefield()
+        with pytest.raises(AttributeError):
+            wakefield.drop('Znonexistent')
+
+    def test_append_to_model_with_invalid_component(self, generate_wakefield):
+        wakefield = generate_wakefield()
+        invalid_component = "InvalidComponent"
+        with pytest.raises(ValueError, match="is not an Impedance nor a WakeFunction."):
+            wakefield.append_to_model(invalid_component)
+
+    def test_drop_method_input_types(self, generate_wakefield):
+        wakefield = generate_wakefield(
+            impedance_params=[{'component_type': 'long'}],
+            wake_function_params=[{'component_type': 'xdip'}]
+        )
+    
+        wakefield.drop('Zlong')
+        assert 'Zlong' not in wakefield.components
+    
+        wakefield.drop(['Wxdip'])
+        assert 'Wxdip' not in wakefield.components
+    
+        wakefield = generate_wakefield(
+            impedance_params=[{'component_type': 'long'}],
+            wake_function_params=[{'component_type': 'xdip'}]
+        )
+        wakefield.drop('Z')
+        assert 'Zlong' not in wakefield.components
+    
+        wakefield = generate_wakefield(
+            impedance_params=[{'component_type': 'long'}],
+            wake_function_params=[{'component_type': 'xdip'}]
+        )
+        wakefield.drop('W')
+        assert 'Wxdip' not in wakefield.components
+    
+        with pytest.raises(TypeError):
+            wakefield.drop(123)
+        
\ No newline at end of file
diff --git a/tests/unit/tracking/test_aperture.py b/tests/unit/tracking/test_aperture.py
new file mode 100644
index 0000000..df2a54c
--- /dev/null
+++ b/tests/unit/tracking/test_aperture.py
@@ -0,0 +1,181 @@
+import pytest
+from mbtrack2 import (CircularAperture, 
+                      ElipticalAperture,
+                      RectangularAperture,
+                      LongitudinalAperture)
+
+class TestCircularAperture:
+
+    # Initialize CircularAperture with a positive radius and track a bunch with particles inside the radius
+    def test_track_particles_inside_radius(self, small_bunch):
+        aperture = CircularAperture(radius=1.0)
+        small_bunch["x"] = 0.5
+        small_bunch["y"] = -0.5
+        aperture.track(small_bunch)
+        assert all(small_bunch.alive)
+
+    # Track a bunch with particles exactly on the radius boundary
+    @pytest.mark.parametrize("x, y", [(1.0, 0.0),
+                                      (0.0, 1.0),
+                                      (-1.0, 0.0),
+                                      (0.0, -1.0)])                                      
+    def test_track_particles_on_boundary(self, small_bunch, x, y):
+        aperture = CircularAperture(radius=1.0)
+        small_bunch["x"] = x
+        small_bunch["y"] = y
+        aperture.track(small_bunch)
+        assert all(small_bunch.alive)
+
+    # Track a bunch with all particles outside the radius
+    @pytest.mark.parametrize("x, y", [(0.75, 0.75),
+                                      (0.75, -0.75),
+                                      (1.1, 0.0),
+                                      (0.0, 1.1)])
+    def test_track_all_particles_outside_radius(self, small_bunch, x, y):
+        aperture = CircularAperture(radius=1.0)
+        small_bunch["x"] = x
+        small_bunch["y"] = y
+        aperture.track(small_bunch)
+        assert not any(small_bunch.alive)
+
+    # Track a bunch with no particles
+    def test_track_no_particles(self, small_bunch):
+        aperture = CircularAperture(radius=1.0)
+        small_bunch.alive[:] = False
+        aperture.track(small_bunch)
+        assert len(small_bunch) == 0
+
+class TestElipticalAperture:
+
+    # Track a bunch where all particles are within the elliptical aperture
+    def test_track_particles_within_aperture(self, small_bunch):
+        aperture = ElipticalAperture(X_radius=1.0, Y_radius=2.0)
+        small_bunch["x"] = 0.5
+        small_bunch["y"] = 1.0
+        aperture.track(small_bunch)
+        assert all(small_bunch.alive)
+
+    # Track a bunch where some particles are outside the elliptical aperture
+    @pytest.mark.parametrize("x, y", [(0.5, 1.75),
+                                      (0.5, -1.75),
+                                      (1.1, 0.0),
+                                      (0.0, 2.1)])
+    def test_track_particles_outside_aperture(self, small_bunch, x, y):
+        aperture = ElipticalAperture(X_radius=1.0, Y_radius=2.0)
+        small_bunch["x"] = x
+        small_bunch["y"] = y
+        aperture.track(small_bunch)
+        assert not all(small_bunch.alive)
+        
+    # Track a bunch with no particles
+    def test_track_no_particles(self, small_bunch):
+        aperture = ElipticalAperture(X_radius=1.0, Y_radius=2.0)
+        small_bunch.alive[:] = False
+        aperture.track(small_bunch)
+        assert len(small_bunch) == 0
+        
+    # Track a bunch with particles exactly on the radius boundary
+    @pytest.mark.parametrize("x, y", [(1.0, 0.0),
+                                      (0.0, 2.0),
+                                      (-1.0, 0.0),
+                                      (0.0, -2.0)])    
+    def test_track_particles_on_boundary(self, small_bunch, x, y):
+        aperture = ElipticalAperture(X_radius=1.0, Y_radius=2.0)
+        small_bunch["x"] = x
+        small_bunch["y"] = y
+        aperture.track(small_bunch)
+        assert all(small_bunch.alive)
+
+class TestRectangularAperture:
+
+    # Track method correctly identifies particles within the rectangular aperture
+    def test_track_particles_within_aperture(self, small_bunch):
+        aperture = RectangularAperture(X_right=1.0, Y_top=1.0)
+        small_bunch["x"] = 0.5
+        small_bunch["y"] = 0.5
+        aperture.track(small_bunch)
+        assert all(small_bunch.alive)
+
+    # Particles outside the defined apertures are marked as 'lost'
+    @pytest.mark.parametrize("x, y", [(1.1, 1.1),
+                                      (-1.1, -1.1),
+                                      (1.1, 0.0),
+                                      (0.0, 1.1)])
+    def test_particles_outside_aperture_lost(self, small_bunch, x, y):
+        aperture = RectangularAperture(X_right=1.0, Y_top=1.0)
+        small_bunch["x"] = x
+        small_bunch["y"] = y
+        aperture.track(small_bunch)
+        assert not any(small_bunch.alive)
+
+    # Track a bunch with no particles
+    def test_track_no_particles(self, small_bunch):
+        aperture = RectangularAperture(X_right=1.0, Y_top=1.0)
+        small_bunch.alive[:] = False
+        aperture.track(small_bunch)
+        assert len(small_bunch) == 0
+        
+    # Manages particles exactly on the aperture boundaries
+    @pytest.mark.parametrize("x, y", [(1.0, 0.0),
+                                      (0.0, 1.0),
+                                      (-1.0, 0.0),
+                                      (0.0, -1.0),
+                                      (1.0, 1.0)])
+    def test_particles_on_boundary(self, small_bunch, x, y):
+        aperture = RectangularAperture(X_right=1.0, Y_top=1.0)
+        small_bunch.particles["x"] = x
+        small_bunch.particles["y"] = y
+        aperture.track(small_bunch)
+        assert all(small_bunch.alive)
+        
+    @pytest.mark.parametrize("x, y", [(1.1, 1.1),
+                                      (-1.1, -1.1),
+                                      (0.0, 0.0),
+                                      (0.5, -0.6)])
+    def test_non_default_rect_loss(self, small_bunch, x, y):
+        aperture = RectangularAperture(X_right=1.0, Y_top=1.0, X_left=0.1, Y_bottom=-0.5)
+        small_bunch["x"] = x
+        small_bunch["y"] = y
+        aperture.track(small_bunch)
+        assert not any(small_bunch.alive)
+
+    @pytest.mark.parametrize("x, y", [(0.5, 0.0),
+                                      (0.2, -0.3)])
+    def test_non_default_rect(self, small_bunch, x, y):
+        aperture = RectangularAperture(X_right=1.0, Y_top=1.0, X_left=0.1, Y_bottom=-0.5)
+        small_bunch["x"] = x
+        small_bunch["y"] = y
+        aperture.track(small_bunch)
+        assert all(small_bunch.alive)
+        
+class TestLongitudinalAperture:
+
+    # Track a Bunch object with particles within the longitudinal bounds
+    def test_track_bunch_within_bounds(self, small_bunch):
+        aperture = LongitudinalAperture(tau_up=1.0)
+        small_bunch["tau"] = -0.5
+        aperture.track(small_bunch)
+        assert all(small_bunch.alive)
+
+    # Track a Bunch object with particles exactly on the boundary values
+    @pytest.mark.parametrize("tau",[(-1.0),(1.0)])
+    def test_track_bunch_on_boundary(self, small_bunch, tau):
+        aperture = LongitudinalAperture(tau_up=1.0)
+        small_bunch["tau"] = tau
+        aperture.track(small_bunch)
+        assert all(small_bunch.alive)
+
+    # Track a bunch with no particles
+    def test_track_no_particles(self, small_bunch):
+        aperture = LongitudinalAperture(tau_up=1.0)
+        small_bunch.alive[:] = False
+        aperture.track(small_bunch)
+        assert len(small_bunch) == 0
+
+    # Track a Bunch object with all particles outside the longitudinal bounds
+    @pytest.mark.parametrize("tau", [(1.1),(-0.6)])
+    def test_track_bunch_outside_bounds(self, small_bunch, tau):
+        aperture = LongitudinalAperture(tau_up=1.0, tau_low=-0.5)
+        small_bunch["tau"] = tau
+        aperture.track(small_bunch)
+        assert not any(small_bunch.alive)
\ No newline at end of file
diff --git a/tests/unit/tracking/test_beam_ion_effects.py b/tests/unit/tracking/test_beam_ion_effects.py
new file mode 100644
index 0000000..3a23e04
--- /dev/null
+++ b/tests/unit/tracking/test_beam_ion_effects.py
@@ -0,0 +1,333 @@
+from mbtrack2 import IonMonitor, IonParticles, IonAperture, BeamIonElement
+from utility_test_functions import assert_attr_changed
+import os
+import h5py as hp
+import numpy as np
+import pytest
+
+@pytest.fixture
+def generate_ion_particles(demo_ring):
+    def generate(mp_number=100,
+                 ion_element_length=1.0,
+                 ring=demo_ring,
+                 track_alive=False,
+                 alive=True):
+        ions = IonParticles(mp_number=mp_number,
+                            ion_element_length=ion_element_length,
+                            ring=ring,
+                            track_alive=track_alive,
+                            alive=alive)
+        return ions
+    return generate
+
+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)
+        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)
+
+    # 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)
+    
+        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)
+        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)
+
+    # 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)
+        ions2 = generate_ion_particles(mp_number=50)
+    
+        combined = ions1 + ions2
+        assert combined.mp_number == 150
+        assert all(combined[coord].shape == (150,) for coord in ["x","xp","y","yp","tau","delta"])
+        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"])
+
+    # Generate distributions with electron bunch containing no particles
+    def test_generate_from_empty_bunch(self, generate_ion_particles, small_bunch):
+        small_bunch.alive[:] = False
+        ions = generate_ion_particles()
+    
+        with pytest.raises(ValueError):
+            ions.generate_as_a_distribution(small_bunch)
+        with pytest.raises(ValueError):
+            ions.generate_from_random_samples(small_bunch)
+            
+@pytest.fixture
+def generate_ion_monitor(tmp_path):
+    def generate(save_every=1, 
+                 buffer_size=10, 
+                 total_size=10, 
+                 file_name=tmp_path / "test_monitor.hdf5"):
+        monitor = IonMonitor(save_every=save_every,
+                            buffer_size=buffer_size,
+                            total_size=total_size,
+                            file_name=file_name)
+        return monitor
+    return generate
+
+class TestIonMonitor:
+
+    # Monitor initialization with valid parameters creates HDF5 file and sets up data structures
+    def test_monitor_init_creates_valid_structures(self, generate_ion_monitor, tmp_path):
+        monitor = generate_ion_monitor()
+        assert monitor.file is not None
+        assert monitor.buffer_size == 10
+        assert monitor.total_size == 10
+        assert monitor.save_every == 1
+        assert monitor.buffer_count == 0
+        assert monitor.write_count == 0
+        assert monitor.track_count == 0
+
+    # Buffer writes to file when full and resets counter
+    def test_buffer_writes_when_full(self, generate_ion_monitor, generate_ion_particles):
+        monitor = generate_ion_monitor(buffer_size=2, total_size=4)
+        ions = generate_ion_particles()
+    
+        for _ in range(2):
+            monitor.track(ions)
+    
+        assert monitor.buffer_count == 0
+        assert monitor.write_count == 1
+
+    # 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.time.dtype == int
+
+    # Initialize monitor with total_size not divisible by buffer_size
+    def test_invalid_total_size_raises_error(self, generate_ion_monitor):
+        with pytest.raises(ValueError, match="total_size must be divisible by buffer_size"):
+            generate_ion_monitor(buffer_size=3, total_size=10)
+
+    # Initialize with invalid/missing file_name
+    def test_invalid_filename_handling(self, generate_ion_monitor):
+        with pytest.raises(OSError):
+            generate_ion_monitor(file_name="/invalid/path/file.hdf5")
+            
+class TestElipticalAperture:
+
+    # Track a bunch where all particles are within the elliptical aperture
+    def test_track_particles_within_aperture(self, generate_ion_particles):
+        ions = generate_ion_particles()
+        mp = len(ions)
+        aperture = IonAperture(X_radius=1.0, Y_radius=2.0)
+        ions["x"] = np.ones_like(ions["x"])*0.5
+        ions["y"] = np.ones_like(ions["y"])*1.0
+        aperture.track(ions)
+        assert all(ions.alive)
+        assert mp == len(ions)
+
+    # Track a bunch where some particles are outside the elliptical aperture
+    @pytest.mark.parametrize("x, y", [(0.5, 1.75),
+                                      (0.5, -1.75),
+                                      (1.1, 0.0),
+                                      (0.0, 2.1)])
+    def test_track_particles_outside_aperture(self, generate_ion_particles, x, y):
+        ions = generate_ion_particles()
+        mp = len(ions)
+        aperture = IonAperture(X_radius=1.0, Y_radius=2.0)
+        ions["x"] = np.ones_like(ions["x"])*x
+        ions["y"] = np.ones_like(ions["y"])*y
+        aperture.track(ions)
+        assert mp != len(ions)
+        
+    # Track a bunch with no particles
+    def test_track_no_particles(self, generate_ion_particles):
+        ions = generate_ion_particles()
+        ions["x"] = np.ones_like(ions["x"])*100
+        ions["y"] = np.ones_like(ions["y"])*100
+        aperture = IonAperture(X_radius=1.0, Y_radius=2.0)
+        aperture.track(ions)
+        assert len(ions) == 0
+        
+        aperture.track(ions)
+        assert True
+        
+    # Track a bunch with particles exactly on the radius boundary
+    @pytest.mark.parametrize("x, y", [(1.0, 0.0),
+                                      (0.0, 2.0),
+                                      (-1.0, 0.0),
+                                      (0.0, -2.0)])    
+    def test_track_particles_on_boundary(self, generate_ion_particles, x, y):
+        ions = generate_ion_particles()
+        mp = len(ions)
+        aperture = IonAperture(X_radius=1.0, Y_radius=2.0)
+        ions["x"] = np.ones_like(ions["x"])*x
+        ions["y"] = np.ones_like(ions["y"])*y
+        aperture.track(ions)
+        assert all(ions.alive)
+        assert mp == len(ions)
+            
+@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'):
+        
+        beam_ion = BeamIonElement(
+            ion_mass=ion_mass,
+            ion_charge=ion_charge, 
+            ionization_cross_section=ionization_cross_section,
+            residual_gas_density=residual_gas_density,
+            ring=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
+    return generate
+
+class TestBeamIonElement:
+
+    @pytest.mark.parametrize('ion_field_model, electron_field_model',
+                             [('weak','weak'), ('weak','strong'), 
+                              ('strong','weak'), ('strong', 'strong')])
+    def test_track_bunch(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"])
+        
+    @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):
+        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"])
+
+    @pytest.mark.parametrize('ion_field_model, electron_field_model',
+                             [('weak','weak'), ('weak','strong'), 
+                              ('strong','weak'), ('strong', 'strong')])        
+    def test_track_beam(self, generate_beam_ion, beam_uniform, 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, beam_uniform, attrs_changed=["xp","yp"])
+
+    @pytest.mark.parametrize('ion_field_model, electron_field_model',
+                              [('weak','weak'), ('weak','strong'), 
+                              ('strong','weak'), ('strong', 'strong')])        
+    def test_track_beam_non_uniform(self, generate_beam_ion, beam_non_uniform, 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, beam_non_uniform, attrs_changed=["xp","yp"])
+
+    # Ion generation creates expected number of macroparticles with proper distribution
+    @pytest.mark.parametrize('generate_method', [('samples'),('distribution')])
+    def test_ion_generation(self, generate_beam_ion, large_bunch, generate_method):
+        n_ions = 1e5
+        large_bunch["x"] += 1
+        large_bunch["y"] += 1
+        beam_ion = generate_beam_ion(n_ion_macroparticles_per_bunch=n_ions,
+                                     generate_method=generate_method)
+        beam_ion.generate_new_ions(large_bunch)
+    
+        assert len(beam_ion.ion_beam["x"]) == n_ions + 1
+        assert np.isclose(beam_ion.ion_beam["x"].mean(), large_bunch["x"].mean(), rtol=0.1)
+        assert np.isclose(beam_ion.ion_beam["y"].mean(), large_bunch["y"].mean(), rtol=0.1)
+
+    # Ion drift tracking properly updates ion positions based on momentum
+    def test_ion_drift_tracking(self, generate_beam_ion, small_bunch):
+        beam_ion = generate_beam_ion()
+        beam_ion.generate_new_ions(small_bunch)
+        beam_ion.ion_beam["xp"] = np.ones_like(beam_ion.ion_beam["x"])
+        beam_ion.ion_beam["yp"] = np.ones_like(beam_ion.ion_beam["y"])
+    
+        drift_length = 2.0
+        initial_x = beam_ion.ion_beam["x"].copy()
+        initial_y = beam_ion.ion_beam["y"].copy()
+    
+        beam_ion.track_ions_in_a_drift(drift_length)
+    
+        assert np.allclose(beam_ion.ion_beam["x"], initial_x + drift_length)
+        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)
+    
+        beam_ion.track(small_bunch)
+    
+        assert os.path.exists(monitor_file)
+        with hp.File(monitor_file, 'r') as f:
+            cond = False
+            for key in f.keys():
+                if key.startswith('IonData'):
+                    cond = True
+        assert cond
+
+    # Empty electron bunch handling during ion generation
+    def test_empty_bunch_handling(self, generate_beam_ion, generate_bunch):
+        beam_ion = generate_beam_ion()
+        empty_bunch = generate_bunch(mp_number=0, init_gaussian=False)
+    
+        with pytest.raises(ValueError):
+            beam_ion.generate_new_ions(empty_bunch)
+
+    # 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)
+    
+        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)
+    
+        assert len(beam_ion.ion_beam["x"]) == 0
+
+    # Ion clearing removes all particles as expected
+    def test_ion_clearing(self, generate_beam_ion, small_bunch):
+        beam_ion = generate_beam_ion()
+        beam_ion.generate_new_ions(small_bunch)
+        assert len(beam_ion.ion_beam["x"]) > 0
+    
+        beam_ion.clear_ions()
+        assert len(beam_ion.ion_beam["x"]) == 1
+        assert beam_ion.ion_beam["x"][0] == 0
\ No newline at end of file
diff --git a/tests/unit/tracking/test_element.py b/tests/unit/tracking/test_element.py
new file mode 100644
index 0000000..209c5d2
--- /dev/null
+++ b/tests/unit/tracking/test_element.py
@@ -0,0 +1,360 @@
+import numpy as np
+import pytest
+from scipy.special import factorial
+from utility_test_functions import assert_attr_changed
+from mbtrack2 import (Element, 
+                      LongitudinalMap, 
+                      SynchrotronRadiation, 
+                      SkewQuadrupole, 
+                      TransverseMapSector, 
+                      TransverseMap,
+                      transverse_map_sector_generator)
+
+class TestElement:
+       
+    def test_parallel_decorator_with_mpi_beam(self, beam_1bunch_mpi):
+        class SubElement(Element):
+            @Element.parallel
+            def track(self, bunch):
+                bunch.charge = 1
+        element = SubElement()
+        element.track(beam_1bunch_mpi)
+        assert beam_1bunch_mpi[beam_1bunch_mpi.mpi.bunch_num].charge == pytest.approx(1)
+
+    def test_parallel_decorator_with_beam(self, beam_non_uniform):
+        class SubElement(Element):
+            @Element.parallel
+            def track(self, bunch):
+                bunch.charge = 1
+        element = SubElement()
+        element.track(beam_non_uniform)
+        for i, bunch in enumerate(beam_non_uniform):
+            if beam_non_uniform.filling_pattern[i] == True:
+                assert bunch.charge == pytest.approx(1)
+            else:
+                assert bunch.charge == pytest.approx(0)
+                
+    def test_parallel_decorator_with_bunch(self, small_bunch):
+        class SubElement(Element):
+            @Element.parallel
+            def track(self, bunch):
+                bunch.charge = 1
+        element = SubElement()
+        element.track(small_bunch)
+        assert small_bunch.charge == pytest.approx(1)
+        
+    # Decorator correctly skips track method if bunch is empty
+    def test_skip_track_if_bunch_empty(self, mocker, generate_bunch):
+        mock_track = mocker.Mock()
+        decorated_track = Element.track_bunch_if_non_empty(mock_track)
+        empty_bunch = generate_bunch(mp_number=0)
+        decorated_track(None, empty_bunch)
+        mock_track.assert_not_called()
+        
+    # Decorator calls track method if bunch is not empty
+    def test_call_track_if_bunch_not_empty(self, mocker, small_bunch):
+        mock_track = mocker.Mock()
+        decorated_track = Element.track_bunch_if_non_empty(mock_track)
+        decorated_track(None, small_bunch)
+        mock_track.assert_called_once()
+        
+    # Decorator respects track_alive flag and calls track method
+    def test_respect_track_alive_flag(self, mocker, generate_bunch):
+        mock_track = mocker.Mock()
+        decorated_track = Element.track_bunch_if_non_empty(mock_track)
+        bunch = generate_bunch(track_alive=False)
+        decorated_track(None, bunch)
+        mock_track.assert_called_once()
+
+    # Track method executes when bunch is not empty and track_alive is True
+    def test_executes_with_nonempty_bunch(self, small_bunch):
+        called = []
+        @Element.track_bunch_if_non_empty
+        def track_method(self, bunch):
+            called.append(True)
+    
+        small_bunch.track_alive = True
+        track_method(self, small_bunch)
+        assert called == [True]
+
+    # Track method executes when track_alive is False regardless of bunch size
+    def test_executes_when_track_alive_false(self, small_bunch):
+        called = []
+        @Element.track_bunch_if_non_empty 
+        def track_method(self, bunch):
+            called.append(True)
+    
+        small_bunch.track_alive = False
+        track_method(self, small_bunch)
+        assert called == [True]
+
+    # Empty bunch with track_alive=True skips track method execution
+    def test_skips_empty_bunch(self, generate_bunch):
+        called = []
+        @Element.track_bunch_if_non_empty
+        def track_method(self, bunch):
+            called.append(True)
+    
+        empty_bunch = generate_bunch(alive=False)
+        empty_bunch.track_alive = True
+        track_method(self, empty_bunch)
+        assert not called
+        
+class TestLongitudinalMap:
+
+    # Track a Bunch object using the track method
+    def test_track_bunch(self, small_bunch, demo_ring):
+        long_map = LongitudinalMap(demo_ring)
+        assert_attr_changed(long_map, small_bunch, attrs_changed=["tau","delta"])
+
+class TestSynchrotronRadiation:
+
+    # SynchrotronRadiation initializes correctly with default switch values
+    def test_initialization_with_default_switch(self, demo_ring):
+        sr = SynchrotronRadiation(demo_ring)
+        assert np.array_equal(sr.switch, np.ones((3,), dtype=bool))
+
+    # SynchrotronRadiation modifies 'delta', 'xp', and 'yp' attributes of Bunch
+    def test_modifies_bunch_attributes(self, small_bunch, demo_ring):
+        sr = SynchrotronRadiation(demo_ring)
+        assert_attr_changed(sr, small_bunch)
+
+    # switch array has all False values, ensuring no changes to Bunch
+    def test_no_changes_with_all_false_switch(self, small_bunch, demo_ring):
+        sr = SynchrotronRadiation(demo_ring, switch=np.zeros((3,), dtype=bool))
+        assert_attr_changed(sr, small_bunch, change=False)
+            
+class TestSkewQuadrupole:
+
+    # Initialize SkewQuadrupole with a positive strength and track a Bunch object
+    def test_modifies_bunch_attributes(self, small_bunch):
+        skew_quad = SkewQuadrupole(strength=0.1)
+        assert_attr_changed(skew_quad, small_bunch, attrs_changed=["xp","yp"])
+        
+class TestTransverseMapSector:
+    
+    @pytest.fixture
+    def generate_trans_map_sector(demo_ring):
+        def generate(phase_diff = np.array([np.pi, np.pi]),
+                     chro_diff = np.array([0.01, 0.01]),
+                     adts=None):
+            alpha0 = np.array([1.0, 1.0])
+            beta0 = np.array([1.0, 1.0])
+            dispersion0 = np.array([0.0, 0.0, 0.0, 0.0])
+            alpha1 = np.array([2.0, 2.0])
+            beta1 = np.array([2.0, 2.0])
+            dispersion1 = np.array([0.1, 0.1, 0.1, 0.1])
+            sector = TransverseMapSector(demo_ring, 
+                                         alpha0, 
+                                         beta0, 
+                                         dispersion0, 
+                                         alpha1, 
+                                         beta1, 
+                                         dispersion1, 
+                                         phase_diff, 
+                                         chro_diff,
+                                         adts=adts)
+            return sector
+        return generate
+
+    # Track a Bunch object through TransverseMapSector and ensure coordinates are updated
+    def test_track_bunch_coordinates_update(self, generate_trans_map_sector, small_bunch):
+        sector = generate_trans_map_sector()
+        assert_attr_changed(sector, small_bunch, attrs_changed=["x", "xp", "y","yp"])
+
+    # Compute chromatic tune advances for a Bunch with non-zero chromaticity differences
+    @pytest.mark.parametrize("chro_diff", [(np.array([0.02, 0.03])),
+                                           (np.array([0.02, 0.03, 0.05, 0.06])),
+                                           (np.array([0.02, 0.03, 0.05, 0.06, 0.02, 0.03,])),
+                                           (np.array([0.02, 0.03, 0.05, 0.06, 0.02, 0.03, 0.05, 0.06])),
+                                           (np.array([0.02, 0.03, 0.05, 0.06, 0.02, 0.03, 0.05, 0.06, 0.05, 0.06])),])
+    def test_chromatic_tune_advances(self, generate_trans_map_sector, small_bunch, chro_diff):
+        # chro_diff = np.array([0.02, 0.03])
+        sector = generate_trans_map_sector(chro_diff=chro_diff)
+        tune_advance_x_chro, tune_advance_y_chro = sector._compute_chromatic_tune_advances(small_bunch)
+        
+        order = len(chro_diff) // 2
+        coefs = np.array([1 / factorial(i) for i in range(order + 1)])
+        coefs[0] = 0
+        chro_diff = np.concatenate(([0, 0], chro_diff))
+        tune_advance_x = np.polynomial.polynomial.Polynomial(chro_diff[::2] * coefs)(small_bunch['delta'])
+        tune_advance_y = np.polynomial.polynomial.Polynomial(chro_diff[1::2] * coefs)(small_bunch['delta'])
+        
+        assert np.allclose(tune_advance_x, tune_advance_x_chro)
+        assert np.allclose(tune_advance_y, tune_advance_y_chro)
+
+
+    # Check that adts are taken into account in calculation
+    def test_amplitude_dependent_tune_shifts(self, generate_trans_map_sector, small_bunch):
+        
+        sector_no_adts = generate_trans_map_sector()
+        adts=[np.array([1e10, 1e10, 1e10]),
+              np.array([1e10, 1e10, 1e10]),
+              np.array([1e10, 1e10, 1e10]),
+              np.array([1e10, 1e10, 1e10])]
+        sector_adts = generate_trans_map_sector(adts=adts)
+        
+        attrs = ["x", "xp", "y","yp"]
+        initial_values = {attr: small_bunch[attr].copy() for attr in attrs}
+
+        sector_no_adts.track(small_bunch)
+        no_adts = {attr: small_bunch[attr].copy() for attr in attrs}
+        
+        for attr in attrs:
+            small_bunch[attr] = initial_values[attr]
+        
+        sector_adts.track(small_bunch)
+        adts = {attr: small_bunch[attr].copy() for attr in attrs}
+
+        for attr in attrs:
+            assert not np.array_equal(initial_values[attr], no_adts[attr])
+            assert not np.array_equal(initial_values[attr], adts[attr])
+            assert not np.array_equal(adts[attr], no_adts[attr])
+            
+class TestTransverseMap:
+    
+    class Old_TransverseMap(Element):
+        """
+        Transverse map from mbtrack2 0.7.0.
+    
+        Parameters
+        ----------
+        ring : Synchrotron object
+        """
+    
+        def __init__(self, ring):
+            self.ring = ring
+            self.alpha = self.ring.optics.local_alpha
+            self.beta = self.ring.optics.local_beta
+            self.gamma = self.ring.optics.local_gamma
+            self.dispersion = self.ring.optics.local_dispersion
+            if self.ring.adts is not None:
+                self.adts_poly = [
+                    np.poly1d(self.ring.adts[0]),
+                    np.poly1d(self.ring.adts[1]),
+                    np.poly1d(self.ring.adts[2]),
+                    np.poly1d(self.ring.adts[3]),
+                ]
+    
+        @Element.parallel
+        def track(self, bunch):
+            """
+            Tracking method for the element.
+            No bunch to bunch interaction, so written for Bunch objects and
+            @Element.parallel is used to handle Beam objects.
+    
+            Parameters
+            ----------
+            bunch : Bunch or Beam object
+            """
+    
+            # Compute phase advance which depends on energy via chromaticity and ADTS
+            if self.ring.adts is None:
+                phase_advance_x = (
+                    2 * np.pi *
+                    (self.ring.tune[0] + self.ring.chro[0] * bunch["delta"]))
+                phase_advance_y = (
+                    2 * np.pi *
+                    (self.ring.tune[1] + self.ring.chro[1] * bunch["delta"]))
+            else:
+                Jx = ((self.ring.optics.local_gamma[0] * bunch["x"]**2) +
+                      (2 * self.ring.optics.local_alpha[0] * bunch["x"] *
+                       bunch["xp"]) +
+                      (self.ring.optics.local_beta[0] * bunch["xp"]**2))
+                Jy = ((self.ring.optics.local_gamma[1] * bunch["y"]**2) +
+                      (2 * self.ring.optics.local_alpha[1] * bunch["y"] *
+                       bunch["yp"]) +
+                      (self.ring.optics.local_beta[1] * bunch["yp"]**2))
+                phase_advance_x = (
+                    2 * np.pi *
+                    (self.ring.tune[0] + self.ring.chro[0] * bunch["delta"] +
+                     self.adts_poly[0](Jx) + self.adts_poly[2](Jy)))
+                phase_advance_y = (
+                    2 * np.pi *
+                    (self.ring.tune[1] + self.ring.chro[1] * bunch["delta"] +
+                     self.adts_poly[1](Jx) + self.adts_poly[3](Jy)))
+    
+            # 6x6 matrix corresponding to (x, xp, delta, y, yp, delta)
+            matrix = np.zeros((6, 6, len(bunch)), dtype=np.float64)
+    
+            # Horizontal
+            c_x = np.cos(phase_advance_x)
+            s_x = np.sin(phase_advance_x)
+    
+            matrix[0, 0, :] = c_x + self.alpha[0] * s_x
+            matrix[0, 1, :] = self.beta[0] * s_x
+            matrix[0, 2, :] = self.dispersion[0]
+            matrix[1, 0, :] = -1 * self.gamma[0] * s_x
+            matrix[1, 1, :] = c_x - self.alpha[0] * s_x
+            matrix[1, 2, :] = self.dispersion[1]
+            matrix[2, 2, :] = 1
+    
+            # Vertical
+            c_y = np.cos(phase_advance_y)
+            s_y = np.sin(phase_advance_y)
+    
+            matrix[3, 3, :] = c_y + self.alpha[1] * s_y
+            matrix[3, 4, :] = self.beta[1] * s_y
+            matrix[3, 5, :] = self.dispersion[2]
+            matrix[4, 3, :] = -1 * self.gamma[1] * s_y
+            matrix[4, 4, :] = c_y - self.alpha[1] * s_y
+            matrix[4, 5, :] = self.dispersion[3]
+            matrix[5, 5, :] = 1
+    
+            x = (matrix[0, 0] * bunch["x"] + matrix[0, 1] * bunch["xp"] +
+                 matrix[0, 2] * bunch["delta"])
+            xp = (matrix[1, 0] * bunch["x"] + matrix[1, 1] * bunch["xp"] +
+                  matrix[1, 2] * bunch["delta"])
+            y = (matrix[3, 3] * bunch["y"] + matrix[3, 4] * bunch["yp"] +
+                 matrix[3, 5] * bunch["delta"])
+            yp = (matrix[4, 3] * bunch["y"] + matrix[4, 4] * bunch["yp"] +
+                  matrix[4, 5] * bunch["delta"])
+    
+            bunch["x"] = x
+            bunch["xp"] = xp
+            bunch["y"] = y
+            bunch["yp"] = yp
+
+    def test_trans_map_base(self, demo_ring, small_bunch):
+        old_map = self.Old_TransverseMap(demo_ring)
+        current_map = TransverseMap(demo_ring)
+        
+        attrs = ["x", "xp", "y","yp"]
+        initial_values = {attr: small_bunch[attr].copy() for attr in attrs}
+
+        old_map.track(small_bunch)
+        old = {attr: small_bunch[attr].copy() for attr in attrs}
+        
+        for attr in attrs:
+            small_bunch[attr] = initial_values[attr]
+        
+        current_map.track(small_bunch)
+        current = {attr: small_bunch[attr].copy() for attr in attrs}
+
+        for attr in attrs:
+            assert not np.array_equal(initial_values[attr], current[attr])
+            assert not np.array_equal(initial_values[attr], old[attr])
+            assert np.allclose(current[attr], old[attr])
+            
+    def test_trans_map_adts(self, ring_with_at_lattice, small_bunch):
+        ring_with_at_lattice.get_adts()
+        old_map = self.Old_TransverseMap(ring_with_at_lattice)
+        current_map = TransverseMap(ring_with_at_lattice)
+        
+        attrs = ["x", "xp", "y","yp"]
+        initial_values = {attr: small_bunch[attr].copy() for attr in attrs}
+
+        old_map.track(small_bunch)
+        old = {attr: small_bunch[attr].copy() for attr in attrs}
+        
+        for attr in attrs:
+            small_bunch[attr] = initial_values[attr]
+        
+        current_map.track(small_bunch)
+        current = {attr: small_bunch[attr].copy() for attr in attrs}
+
+        for attr in attrs:
+            assert not np.array_equal(initial_values[attr], current[attr])
+            assert not np.array_equal(initial_values[attr], old[attr])
+            assert np.allclose(current[attr], old[attr])
+
diff --git a/tests/unit/tracking/test_emfields.py b/tests/unit/tracking/test_emfields.py
new file mode 100644
index 0000000..e7eea3b
--- /dev/null
+++ b/tests/unit/tracking/test_emfields.py
@@ -0,0 +1,102 @@
+import numpy as np
+
+from mbtrack2.tracking.emfields import (
+    _wofz,
+    _sqrt_sig,
+    _efieldn_mit,
+    _efieldn_linearized,
+    efieldn_gauss_round,
+    add_sigma_check,
+    get_displaced_efield,
+)
+
+import pytest
+
+class Test_particles_electromagnetic_fields:
+
+    def test_wofz_return_float(self):
+        real, imag = _wofz(1.0, 1.0)
+        assert isinstance(real, float)
+        assert isinstance(imag, float)
+
+    def test_sqrt_sig_return_positive(self):
+        val = _sqrt_sig(1.0, 1.0)
+        assert isinstance(val, float)
+        assert val >= 0
+        
+    @pytest.mark.parametrize("func",[(_efieldn_mit),
+                                      (efieldn_gauss_round),
+                                      (_efieldn_linearized)])
+    def test_efieldn_return_float(self, func):
+        ex, ey = func(1.0, 1.0, 1.0, 1.0)
+        assert isinstance(ex, float)
+        assert isinstance(ey, float)
+
+    # Maintains original function behavior when sig_x is greater than sig_y
+    def test_add_sigma_check_maintain_original_behavior_when_sig_x_greater_than_sig_y(self):
+        def mock_efieldn(x, y, sig_x, sig_y):
+            return x + sig_x, y + sig_y
+
+        wrapped_function = add_sigma_check(mock_efieldn)
+        result = wrapped_function(np.array([1.0]), np.array([1.0]), 2.0, 1.0)
+        assert result == (3.0, 2.0)
+
+    # Exchanges x and y when sig_x is less than sig_y
+    def test_add_sigma_check_exchange_x_y_when_sig_x_less_than_sig_y(self):
+        def mock_efieldn(x, y, sig_x, sig_y):
+            return x + sig_x, y + sig_y
+
+        wrapped_function = add_sigma_check(mock_efieldn)
+        result = wrapped_function(np.array([1.0]), np.array([1.0]), 1.0, 2.0)
+        assert result == (2.0, 3.0)
+
+    # Applies round beam field formula when sig_x is close to sig_y
+    def test_add_sigma_check_apply_round_beam_formula_when_sigmas_close(self):
+        wrapped_function = add_sigma_check(efieldn_gauss_round)
+        result = wrapped_function(np.array([1.0]), np.array([1.0]), 1.0, 1.00001)
+        assert np.allclose(result, efieldn_gauss_round(np.array([1.0]), np.array([1.0]), 1.0, 1.00001))
+
+    # Returns zero fields when sig_x and sig_y are both close to zero
+    def test_add_sigma_check_zero_fields_when_sigmas_close_to_zero(self):
+        wrapped_function = add_sigma_check(efieldn_gauss_round)
+        result = wrapped_function(np.array([1.0]), np.array([1.0]), 1e-11, 1e-11)
+        assert np.allclose(result, (np.zeros(1), np.zeros(1)))
+        
+    def test_add_sigma_check_empty_arrays(self):
+        # Define a mock efieldn function
+        def mock_efieldn(x, y, sig_x, sig_y):
+            return np.zeros_like(x), np.zeros_like(y)
+    
+        # Wrap the mock function with add_sigma_check
+        wrapped_function = add_sigma_check(mock_efieldn)
+    
+        # Create empty arrays for x and y
+        x = np.array([])
+        y = np.array([])
+        sig_x = 1.0
+        sig_y = 1.0
+    
+        # Call the wrapped function
+        en_x, en_y = wrapped_function(x, y, sig_x, sig_y)
+    
+        # Assert that the output is also empty arrays
+        assert en_x.size == 0
+        assert en_y.size == 0
+        
+    # Computes electric field for typical Gaussian charge distribution
+    def test_get_displaced_efield_return_shape(self):
+        def mock_efieldn(x, y, sig_x, sig_y):
+            return np.ones_like(x), np.ones_like(y)
+    
+        xr = np.array([1.0, 2.0, 3.0])
+        yr = np.array([1.0, 2.0, 3.0])
+        sig_x = 2.0
+        sig_y = 1.0
+        mean_x = 0.0
+        mean_y = 0.0
+    
+        en_x, en_y = get_displaced_efield(mock_efieldn, xr, yr, sig_x, sig_y, mean_x, mean_y)
+    
+        assert np.allclose(en_x, [1.0, 1.0, 1.0])
+        assert np.allclose(en_y, [1.0, 1.0, 1.0])
+
diff --git a/tests/unit/tracking/test_excite.py b/tests/unit/tracking/test_excite.py
new file mode 100644
index 0000000..d4a8042
--- /dev/null
+++ b/tests/unit/tracking/test_excite.py
@@ -0,0 +1,72 @@
+import numpy as np
+import pytest
+from mbtrack2 import Sweep
+from utility_test_functions import assert_attr_changed
+
+@pytest.fixture
+def generate_sweep(demo_ring):
+    def generate(ring=demo_ring,
+                 f0=1e3,
+                 f1=2e3,
+                 t1=1e-3,
+                 level=1e3,
+                 plane="tau",
+                 bunch_to_sweep=None):
+        sweep = Sweep(ring=ring,
+                      f0=f0,
+                      f1=f1,
+                      t1=t1,
+                      level=level,
+                      plane=plane,
+                      bunch_to_sweep=bunch_to_sweep)
+        return sweep
+    return generate
+
+class TestSweep:
+
+    # Sweep applies frequency chirp correctly to a single bunch
+    @pytest.mark.parametrize("plane, attr", [("x","xp"), ("y","yp"),("tau","delta")])
+    def test_single_bunch_chirp(self, generate_sweep, small_bunch, plane, attr):
+        sweep = generate_sweep(plane=plane)
+        assert_attr_changed(sweep, small_bunch, attrs_changed=[attr])
+
+    # Sweep applies frequency chirp correctly to all bunches in a beam
+    @pytest.mark.parametrize("plane, attr", [("x","xp"), ("y","yp"),("tau","delta")])
+    def test_all_bunches_chirp(self, generate_sweep, beam_uniform, plane, attr):
+        sweep = generate_sweep(plane=plane)
+        assert_attr_changed(sweep, beam_uniform, attrs_changed=[attr])
+
+    # Sweep applies frequency chirp correctly to all bunches in a beam
+    @pytest.mark.parametrize("plane, attr", [("x","xp"), ("y","yp"),("tau","delta")])
+    def test_beam_chirp_single_bunch(self, generate_sweep, beam_uniform, plane, attr):
+        sweep = generate_sweep(plane=plane, bunch_to_sweep=5)
+        initial_attr = [bunch[attr].copy() for bunch in beam_uniform]
+        sweep.track(beam_uniform)
+        for i, bunch in enumerate(beam_uniform):
+            if i == 5:
+                assert not np.allclose(initial_attr[i], bunch[attr]), f"Chirp not applied correctly to bunch {i}"
+            else:
+                assert np.allclose(initial_attr[i], bunch[attr]), f"Chirp applied to bunch {i}"
+
+
+    # Plot method generates a correct sweep voltage plot
+    def test_plot_sweep_voltage(self, generate_sweep):
+        sweep = generate_sweep()
+        fig = sweep.plot()
+        assert fig is not None, "Plot method did not return a figure"
+
+    # Sweep tracks correctly when MPI is enabled
+    @pytest.mark.parametrize("plane, attr", [("x","xp"), ("y","yp"),("tau","delta")])
+    def test_mpi_tracking(self, generate_sweep, beam_1bunch_mpi, plane, attr):
+        sweep = generate_sweep(plane=plane)
+        bunch = beam_1bunch_mpi[beam_1bunch_mpi.mpi.bunch_num]
+        initial_attr = bunch[attr].copy()
+        sweep.track(beam_1bunch_mpi)
+        assert not np.allclose(initial_attr, bunch[attr]), "Chirp not applied correctly with MPI"
+
+    # Sweep correctly resets count after completing a full sweep
+    def test_count_reset_after_full_sweep(self, generate_sweep, small_bunch):
+        sweep = generate_sweep()
+        for _ in range(sweep.N):  # Complete one full sweep cycle
+            sweep.track(small_bunch)
+        assert sweep.count == 0, "Count did not reset after completing a full sweep"
\ No newline at end of file
diff --git a/tests/unit/tracking/test_ibs.py b/tests/unit/tracking/test_ibs.py
new file mode 100644
index 0000000..974f1e8
--- /dev/null
+++ b/tests/unit/tracking/test_ibs.py
@@ -0,0 +1,73 @@
+from mbtrack2 import IntrabeamScattering
+import numpy as np
+import pytest
+from utility_test_functions import assert_attr_changed
+
+@pytest.fixture
+def generate_ibs(ring_with_at_lattice):
+    def generate(ring=ring_with_at_lattice,
+                 model="CIMP",
+                 n_points=100,
+                 n_bin=100):
+        ibs = IntrabeamScattering(ring=ring,
+                                  model=model,
+                                  n_points=n_points,
+                                  n_bin=n_bin)
+        return ibs
+    return generate
+
+class TestIntrabeamScattering:
+            
+    # Track a bunch and validate the momentum changes
+    @pytest.mark.parametrize("model",[("Bane"),("CIMP"), ("PM"), ("PS")])
+    def test_track_model_change_attr(self, small_bunch, generate_ibs, model):
+        ibs = generate_ibs(model=model)
+        if model == "Bane":
+            assert_attr_changed(ibs, small_bunch, attrs_changed=["xp", "delta"])
+        else:
+            assert_attr_changed(ibs, small_bunch)
+
+    # Initialize with an invalid model name and expect a ValueError
+    def test_initialize_invalid_model(self, demo_ring):
+        with pytest.warns(UserWarning):
+            with pytest.raises(ValueError):
+                IntrabeamScattering(ring=demo_ring, model="InvalidModel")
+
+    # Track a bunch with n_bin set to 1 and verify uniform distribution handling
+    def test_track_with_n_bin_one(self, small_bunch, generate_ibs):
+        ibs = generate_ibs(n_bin=1)
+        assert_attr_changed(ibs, small_bunch)
+
+    # Handle a case where the bunch is empty and ensure no operations are performed
+    def test_empty_bunch_handling(self, small_bunch, generate_ibs):
+        small_bunch.alive[:] = False
+        ibs = generate_ibs()
+        assert_attr_changed(ibs, small_bunch, change=False)
+        
+    # Handle a case where the bunch has lost some macro-particles
+    def test_partial_empty_bunch_handling(self, small_bunch, generate_ibs):
+        small_bunch.alive[0:5] = False
+        ibs = generate_ibs()
+        assert_attr_changed(ibs, small_bunch)
+        
+    # Test the warning when lattice file is not loaded and optics are approximated
+    def test_warning_for_approximated_optics(self, demo_ring):
+        demo_ring.optics.use_local_values = True
+        with pytest.warns(UserWarning):
+            IntrabeamScattering(ring=demo_ring, model="CIMP")
+
+    # Validate the computation of growth rates for each model
+    def test_growth_rate_computation(self, demo_ring, small_bunch, generate_ibs):
+        for model in ["PS", "PM", "Bane", "CIMP"]:
+            ibs = generate_ibs(model=model)
+            ibs.initialize(small_bunch)
+            if model in ["PS", "PM"]:
+                vabq, v1aq, v1bq = ibs.scatter()
+                T_x, T_y, T_p = ibs.get_scatter_T(vabq=vabq, v1aq=v1aq, v1bq=v1bq)
+            elif model == "Bane":
+                gval = ibs.scatter()
+                T_x, T_y, T_p = ibs.get_scatter_T(gval=gval)
+            elif model == "CIMP":
+                g_ab, g_ba = ibs.scatter()
+                T_x, T_y, T_p = ibs.get_scatter_T(g_ab=g_ab, g_ba=g_ba)
+            assert T_x >= 0 and T_y >= 0 and T_p >= 0
\ No newline at end of file
diff --git a/tests/unit/tracking/test_parallel.py b/tests/unit/tracking/test_parallel.py
new file mode 100644
index 0000000..3404b86
--- /dev/null
+++ b/tests/unit/tracking/test_parallel.py
@@ -0,0 +1,98 @@
+import pytest
+import numpy as np
+from mbtrack2.tracking.parallel import Mpi
+from mpi4py import MPI
+
+@pytest.fixture
+def mpi_size1():
+    filling_pattern = np.array([True])
+    return Mpi(filling_pattern)
+
+@pytest.fixture
+def generate_mpi_mock(mocker):
+    def generate(filling_pattern,
+                 rank=0):
+        
+        size = filling_pattern.sum()
+        mocker.patch.object(MPI, 'COMM_WORLD')
+        MPI.COMM_WORLD.Get_size.return_value = size
+        MPI.COMM_WORLD.Get_rank.return_value = rank
+
+        return Mpi(filling_pattern)
+    return generate
+
+@pytest.fixture
+def mpi_mock_size4_fill6(generate_mpi_mock):
+    filling_pattern = np.ones((6,), dtype=bool)
+    filling_pattern[1] = False
+    filling_pattern[4] = False
+    return generate_mpi_mock(filling_pattern)
+
+class TestMpi:
+
+    # Verify table creation
+    def test_initialize_mpi_size1(self, mpi_size1):
+        assert np.array_equal(mpi_size1.table, np.array([[0, 0]]))
+        assert mpi_size1.bunch_num == 0
+        assert mpi_size1.next_bunch == 0
+        assert mpi_size1.previous_bunch == 0
+
+    # Verify correct rank-to-bunch and bunch-to-rank mappings
+    def test_rank_to_bunch_and_bunch_to_rank_mappings(self, mpi_mock_size4_fill6):
+        assert mpi_mock_size4_fill6.rank_to_bunch(0) == 0
+        assert mpi_mock_size4_fill6.bunch_to_rank(0) == 0
+        assert mpi_mock_size4_fill6.rank_to_bunch(1) == 2
+        assert mpi_mock_size4_fill6.bunch_to_rank(1) == None
+        assert mpi_mock_size4_fill6.rank_to_bunch(2) == 3
+        assert mpi_mock_size4_fill6.bunch_to_rank(2) == 1
+        assert mpi_mock_size4_fill6.rank_to_bunch(3) == 5
+        assert mpi_mock_size4_fill6.bunch_to_rank(3) == 2
+        assert mpi_mock_size4_fill6.bunch_to_rank(4) == None
+        assert mpi_mock_size4_fill6.bunch_to_rank(5) == 3
+
+    def test_next_previous_bunch(self, mpi_mock_size4_fill6):
+        assert mpi_mock_size4_fill6.rank == 0
+        assert mpi_mock_size4_fill6.next_bunch == 1
+        assert mpi_mock_size4_fill6.previous_bunch == 3
+        
+    @pytest.mark.parametrize("dim", [("tau"), ("delta"), ("x"), ("xp"), ("y"), ("yp")])
+    def test_share_distributions(self, beam_1bunch_mpi, dim):
+        n_bin = 75
+        beam_1bunch_mpi.mpi.share_distributions(beam_1bunch_mpi, dimensions=dim, n_bin=n_bin)
+        assert beam_1bunch_mpi.mpi.__getattribute__(dim + "_center").shape == (beam_1bunch_mpi.mpi.size, n_bin)
+        assert beam_1bunch_mpi.mpi.__getattribute__(dim + "_profile").shape == (beam_1bunch_mpi.mpi.size, n_bin)
+        assert beam_1bunch_mpi.mpi.__getattribute__(dim + "_sorted_index").shape == (len(beam_1bunch_mpi[beam_1bunch_mpi.mpi.bunch_num]),)
+        assert beam_1bunch_mpi.mpi.charge_per_mp_all is not None
+
+    def test_share_distributions_multidim(self, beam_1bunch_mpi):
+        n_bin = [75,60,22]
+        dims = ["x","yp","tau"]
+        beam_1bunch_mpi.mpi.share_distributions(beam_1bunch_mpi, dimensions=dims, n_bin=n_bin)
+        assert beam_1bunch_mpi.mpi.charge_per_mp_all is not None
+        for i, dim in enumerate(dims):
+            assert beam_1bunch_mpi.mpi.__getattribute__(dim + "_center").shape == (beam_1bunch_mpi.mpi.size, n_bin[i])
+            assert beam_1bunch_mpi.mpi.__getattribute__(dim + "_profile").shape == (beam_1bunch_mpi.mpi.size, n_bin[i])
+            assert beam_1bunch_mpi.mpi.__getattribute__(dim + "_sorted_index").shape == (len(beam_1bunch_mpi[beam_1bunch_mpi.mpi.bunch_num]),)
+
+    def test_share_distributions_nobunch(self, beam_1bunch_mpi):
+        n_bin = 75
+        dim = "x"
+        beam_1bunch_mpi[beam_1bunch_mpi.mpi.bunch_num].alive[:] = False
+        beam_1bunch_mpi.mpi.share_distributions(beam_1bunch_mpi, dimensions=dim, n_bin=n_bin)
+        assert beam_1bunch_mpi.mpi.charge_per_mp_all is not None
+        assert np.allclose(beam_1bunch_mpi.mpi.__getattribute__(dim + "_center"), np.zeros((n_bin, ), dtype=np.int64))
+        assert np.allclose(beam_1bunch_mpi.mpi.__getattribute__(dim + "_profile"), np.zeros((n_bin, ), dtype=np.float64))
+        assert beam_1bunch_mpi.mpi.__getattribute__(dim + "_sorted_index") is None
+
+
+    def test_share_means(self, beam_1bunch_mpi):
+        beam_1bunch_mpi.mpi.share_means(beam_1bunch_mpi)
+        assert beam_1bunch_mpi.mpi.charge_all is not None
+        assert beam_1bunch_mpi.mpi.mean_all.shape == (beam_1bunch_mpi.mpi.size, 6)
+        assert beam_1bunch_mpi.mpi.mean_all[0,:] == pytest.approx(beam_1bunch_mpi[beam_1bunch_mpi.mpi.bunch_num].mean)
+
+    def test_share_stds(self, beam_1bunch_mpi):
+        beam_1bunch_mpi.mpi.share_stds(beam_1bunch_mpi)
+        assert beam_1bunch_mpi.mpi.charge_all is not None
+        assert beam_1bunch_mpi.mpi.std_all.shape == (beam_1bunch_mpi.mpi.size, 6)
+        assert beam_1bunch_mpi.mpi.std_all[0,:] == pytest.approx(beam_1bunch_mpi[beam_1bunch_mpi.mpi.bunch_num].std)
\ No newline at end of file
diff --git a/tests/unit/tracking/test_particle.py b/tests/unit/tracking/test_particle.py
new file mode 100644
index 0000000..7a1a51f
--- /dev/null
+++ b/tests/unit/tracking/test_particle.py
@@ -0,0 +1,306 @@
+import numpy as np
+import matplotlib.pyplot as plt
+import pytest
+from scipy.constants import e, m_p, c
+from mbtrack2 import Particle, Bunch, Beam
+
+class TestParticle:
+
+    # Accessing the E_rest property to calculate rest energy
+    def test_access_E_rest_property(self):
+        particle = Particle(mass=m_p, charge=e)
+        expected_E_rest = m_p * c**2 / e
+        assert particle.E_rest == expected_E_rest
+        
+@pytest.fixture
+def generate_bunch(demo_ring):
+    def generate(ring=demo_ring,
+                 mp_number=1e3,
+                 current=1e-3,
+                 track_alive=True,
+                 alive=True,
+                 load_from_file=None,
+                 load_suffix=None,
+                 init_gaussian=True,
+                 ):
+        bunch = Bunch(ring=ring,
+                    mp_number=mp_number,
+                    current=current,
+                    track_alive=track_alive,
+                    alive=alive,
+                    load_from_file=load_from_file,
+                    load_suffix=load_suffix)
+        if init_gaussian:
+            bunch.init_gaussian()
+        return bunch
+    return generate
+
+@pytest.fixture
+def small_bunch(generate_bunch):
+    return generate_bunch(mp_number=10)
+
+@pytest.fixture
+def large_bunch(generate_bunch):
+    return generate_bunch(mp_number=1e5)
+
+class TestBunch:
+
+    # Calculate and verify the mean, std, skew, and kurtosis of particle positions
+    def test_statistics_single_mp(self, generate_bunch):
+        bunch = generate_bunch(mp_number=1)
+        assert len(bunch.mean) == 6
+        assert len(bunch.std) == 6
+        assert len(bunch.skew) == 6
+        assert len(bunch.kurtosis) == 6
+        assert len(bunch.emit) == 3
+        assert len(bunch.cs_invariant) == 3
+        assert np.any(np.isnan(bunch.mean)) == False
+        assert np.any(np.isnan(bunch.std)) == False
+        assert np.any(np.isnan(bunch.skew)) == False
+        assert np.any(np.isnan(bunch.kurtosis)) == False
+        assert np.any(np.isnan(bunch.emit)) == False
+        assert np.any(np.isnan(bunch.cs_invariant)) == False
+
+    # Initialize a Bunch with zero macro-particles and verify behavior
+    def test_initialize_zero_macro_particles(self, generate_bunch):
+        bunch = generate_bunch(mp_number=0)
+        assert bunch.mp_number == 0
+        assert bunch.is_empty == True
+        
+    def test_drop_zero_macro_particles(self, generate_bunch):
+        bunch = generate_bunch(mp_number=1)
+        bunch.alive[0] = False
+        assert len(bunch) == 0
+        assert bunch.is_empty == True
+        assert pytest.approx(bunch.charge) == 0
+
+    # Verify the behavior of init_gaussian with custom covariance and mean
+    def test_init_gaussian_custom_cov_mean(self, large_bunch):
+    
+        custom_cov = np.eye(6) * 0.5
+        custom_mean = np.ones(6) * 2.0
+    
+        large_bunch.init_gaussian(cov=custom_cov, mean=custom_mean)
+    
+        assert np.allclose(large_bunch.mean, custom_mean, atol=1e-1)
+        assert np.allclose(np.cov(large_bunch.particles['x'], large_bunch.particles['xp']), custom_cov[:2, :2], atol=1e-1)
+
+    def test_bunch_values(self, small_bunch, demo_ring):
+        mp_number = small_bunch.mp_number
+        current = small_bunch.current
+
+        assert len(small_bunch) == mp_number
+        np.testing.assert_allclose(small_bunch.alive, np.ones((mp_number,), dtype=bool))
+        assert pytest.approx(small_bunch.charge) == current * demo_ring.T0
+        assert pytest.approx(small_bunch.charge_per_mp) == current * demo_ring.T0 / mp_number
+        assert pytest.approx(small_bunch.particle_number) == current * demo_ring.T0 / e
+        assert small_bunch.is_empty == False
+
+    def test_bunch_magic(self, generate_bunch):
+        mybunch = generate_bunch(init_gaussian=False)
+        for label in mybunch:
+            np.testing.assert_allclose(mybunch[label], np.zeros(len(mybunch)))
+            mybunch[label] = np.ones(len(mybunch))
+            np.testing.assert_allclose(mybunch[label], np.ones(len(mybunch)))
+
+    def test_bunch_losses(self, small_bunch):
+        charge_init = small_bunch.charge
+        small_bunch.alive[0] = False
+        assert len(small_bunch) == small_bunch.mp_number - 1
+        assert pytest.approx(small_bunch.charge) == charge_init * len(small_bunch) / small_bunch.mp_number
+
+    def test_bunch_init_gauss(self, large_bunch):
+        large_bunch.init_gaussian(mean=np.ones((6,)))
+        np.testing.assert_allclose(large_bunch.mean, np.ones((6,)), rtol=1e-2)
+
+    def test_bunch_save_load(self, small_bunch, generate_bunch, tmp_path):
+        small_bunch["x"] += 1
+        small_bunch.save(str(tmp_path / "test"))
+    
+        mybunch2 = generate_bunch(mp_number=1, current=1e-5)
+        mybunch2.load(str(tmp_path / "test.hdf5"))
+    
+        assert small_bunch.mp_number == mybunch2.mp_number
+        assert pytest.approx(small_bunch.charge) == mybunch2.charge
+        for label in small_bunch:
+            np.testing.assert_allclose(small_bunch[label], mybunch2[label])
+
+    def test_bunch_stats(self, demo_ring, large_bunch):
+        np.testing.assert_array_almost_equal(large_bunch.mean, np.zeros((6,)), decimal=5)
+        sig = np.concatenate((demo_ring.sigma(), [demo_ring.sigma_0, demo_ring.sigma_delta]))
+        np.testing.assert_allclose(large_bunch.std, sig, rtol=1e-2)
+        np.testing.assert_allclose(large_bunch.emit[:2], demo_ring.emit, rtol=1e-2)
+        np.testing.assert_allclose(large_bunch.cs_invariant[:2], demo_ring.emit*2, rtol=1e-2)
+
+    @pytest.mark.parametrize('n_bin',
+                             [(75),
+                              (1),
+                              (2)]
+        )
+    def test_bunch_binning(self, small_bunch, n_bin):
+        (bins, sorted_index, profile, center) = small_bunch.binning(n_bin=n_bin)
+        profile0 = np.zeros((len(bins)-1,))
+        for i, val in enumerate(sorted_index):
+            assert bins[val] <= small_bunch["tau"][i] <= bins[val+1]
+            profile0[val] += 1
+        np.testing.assert_allclose(profile0, profile)
+
+    def test_bunch_plots(self, small_bunch):
+        small_bunch.plot_phasespace()
+        small_bunch.plot_profile()
+        assert True
+
+    def test_bunch_emittance(self, generate_bunch, demo_ring):
+        mp_number = 1_000_000
+        mybunch = generate_bunch(mp_number=mp_number, track_alive=False)
+        
+        np.testing.assert_allclose(mybunch.emit[0], demo_ring.emit[0], rtol=1e-2, atol=0,
+         err_msg=f'Emittances do not match. {demo_ring.emit[0]} initialised, {mybunch.emit[0]:} calculated')
+        np.testing.assert_allclose(mybunch.emit[1], demo_ring.emit[1], rtol=1e-2, atol=0,
+         err_msg=f'Emittances do not match. {demo_ring.emit[1]} initialised, {mybunch.emit[1]:} calculated')
+    
+        np.testing.assert_allclose(mybunch.emit[0], mybunch.cs_invariant[0]/2, rtol=1e-2, atol=0,
+         err_msg=f'Emittances do not match. {mybunch.cs_invariant[0]/2} calculated with optics functions, {mybunch.emit[0]:} calculated with coordinates only')
+        np.testing.assert_allclose(mybunch.emit[1], mybunch.cs_invariant[1]/2, rtol=1e-2, atol=0,
+         err_msg=f'Emittances do not match. {mybunch.cs_invariant[1]/2} calculated with optics functions, {mybunch.emit[1]:} calculated with coordinates only')
+    
+    def test_bunch_emittance_with_dispersion(self, generate_bunch, demo_ring):
+        mp_number = 1_000_000
+        mybunch = generate_bunch(mp_number=mp_number, track_alive=False)
+
+        np.testing.assert_allclose(mybunch.emit[0], demo_ring.emit[0], rtol=1e-2, atol=0,
+         err_msg=f'Emittances do not match. {demo_ring.emit[0]} initialised, {mybunch.emit[0]:} calculated')
+        np.testing.assert_allclose(mybunch.emit[1], demo_ring.emit[1], rtol=1e-2, atol=0,
+         err_msg=f'Emittances do not match. {demo_ring.emit[1]} initialised, {mybunch.emit[1]:} calculated')
+    
+        np.testing.assert_allclose(mybunch.emit[0], mybunch.cs_invariant[0]/2, rtol=1e-2, atol=0,
+         err_msg=f'Emittances do not match. {mybunch.cs_invariant[0]/2} calculated with optics functions, {mybunch.emit[0]:} calculated with coordinates only')
+        np.testing.assert_allclose(mybunch.emit[1], mybunch.cs_invariant[1]/2, rtol=1e-2, atol=0,
+         err_msg=f'Emittances do not match. {mybunch.cs_invariant[1]/2} calculated with optics functions, {mybunch.emit[1]:} calculated with coordinates only')
+
+
+@pytest.fixture
+def generate_beam(demo_ring, generate_bunch):
+    def generate(ring=demo_ring,
+                 filling_pattern=None, 
+                 current_per_bunch=1e-3, 
+                 mp_per_bunch=10,
+                 track_alive=True,
+                 mpi=False):
+
+        beam = Beam(ring=ring)
+        if filling_pattern is None:
+            filling_pattern = np.ones((ring.h,), dtype=bool)
+        beam.init_beam(filling_pattern=filling_pattern, 
+                       current_per_bunch=current_per_bunch, 
+                       mp_per_bunch=mp_per_bunch,
+                       track_alive=track_alive,
+                       mpi=mpi)
+
+        return beam
+    return generate
+
+@pytest.fixture
+def beam_uniform(generate_beam):
+    return generate_beam()
+
+@pytest.fixture
+def beam_non_uniform(generate_beam, demo_ring):
+    filling_pattern = np.ones((demo_ring.h,), dtype=bool)
+    filling_pattern[4] = False
+    filling_pattern[-3:] = False
+    return generate_beam(filling_pattern=filling_pattern)
+
+@pytest.fixture
+def beam_1bunch_mpi(generate_beam, demo_ring):
+    filling_pattern = np.zeros((demo_ring.h,), dtype=bool)
+    filling_pattern[0] = True
+    return generate_beam(filling_pattern=filling_pattern, mpi=True)
+
+class TestBeam:
+
+    @pytest.mark.parametrize("n_bunch", [(1),(5),(10),(20)])
+    def test_initialize_beam(self, generate_beam, demo_ring, n_bunch):
+        filling_pattern = np.zeros((demo_ring.h,), dtype=bool)
+        filling_pattern[0:n_bunch] = True
+        beam = generate_beam(filling_pattern=filling_pattern)
+        assert len(beam) == n_bunch
+
+    @pytest.mark.parametrize("n_bunch", [(1),(5),(10),(20)])
+    def test_calculate_total_beam_properties(self, generate_beam, demo_ring, n_bunch):
+        filling_pattern = np.zeros((demo_ring.h,), dtype=bool)
+        filling_pattern[0:n_bunch] = True
+        beam = generate_beam(filling_pattern=filling_pattern, current_per_bunch=0.002)
+        assert beam.current == pytest.approx(0.002 * n_bunch)
+        assert beam.charge == pytest.approx(np.sum([bunch.charge for bunch in beam]))
+        assert beam.particle_number == pytest.approx(np.sum([bunch.particle_number for bunch in beam]))
+
+    def test_save_and_load_beam_data(self, tmp_path, beam_uniform, demo_ring):
+        file_path = tmp_path / "beam_data"
+        beam_uniform.save(str(file_path))
+        loaded_beam = Beam(demo_ring)
+        loaded_beam.load(str(file_path) + ".hdf5", mpi=False)
+        assert np.array_equal(beam_uniform.bunch_current, loaded_beam.bunch_current)
+        assert np.array_equal(beam_uniform.bunch_mean, loaded_beam.bunch_mean)
+        assert np.array_equal(beam_uniform.bunch_std, loaded_beam.bunch_std)
+
+    @pytest.mark.parametrize("var,option",
+                             [("bunch_current", None),
+                              ("bunch_charge", None),
+                              ("bunch_particle", None),
+                              ("bunch_mean","x"),
+                              ("bunch_std","x"),
+                              ("bunch_emit","x")])
+    def test_plot_variables_with_respect_to_bunch_number(self, beam_uniform, var, option):
+        fig = beam_uniform.plot(var, option)
+        assert fig is not None
+        plt.close("all")
+
+    def test_initialize_beam_mismatched_bunch_list_length(self, demo_ring, generate_bunch):
+        mismatched_bunch_list = [generate_bunch() for _ in range(demo_ring.h - 1)]
+        with pytest.raises(ValueError):
+            Beam(demo_ring, mismatched_bunch_list)
+
+    def test_filling_pattern_and_distance_between_bunches(self, generate_beam, demo_ring):
+        filling_pattern = np.ones((demo_ring.h,), dtype=bool)
+        filling_pattern[5] = False
+        filling_pattern[8:10] = False
+        beam = generate_beam(filling_pattern=filling_pattern)
+        np.testing.assert_array_equal(beam.filling_pattern, filling_pattern)
+        expected_distances = np.ones((demo_ring.h,))
+        expected_distances[4] = 2
+        expected_distances[5] = 0
+        expected_distances[7] = 3
+        expected_distances[8:10] = 0
+        np.testing.assert_array_equal(beam.distance_between_bunches, expected_distances)
+
+    def test_update_filling_pattern_and_distance_between_bunches(self, beam_uniform, demo_ring):
+        for i in [5,8,9]:
+            beam_uniform[i].charge = 0
+        beam_uniform[5].charge = 0
+        beam_uniform.update_filling_pattern()
+        beam_uniform.update_distance_between_bunches()
+
+        expected_filling_pattern = np.ones((demo_ring.h,), dtype=bool)
+        expected_filling_pattern[5] = False
+        expected_filling_pattern[8:10] = False
+        np.testing.assert_array_equal(beam_uniform.filling_pattern, expected_filling_pattern)
+
+        expected_distances = np.ones((demo_ring.h,))
+        expected_distances[4] = 2
+        expected_distances[5] = 0
+        expected_distances[7] = 3
+        expected_distances[8:10] = 0
+        np.testing.assert_array_equal(beam_uniform.distance_between_bunches, expected_distances)
+
+    def test_mpi_gather_and_close_consistency(self, mocker, demo_ring, generate_bunch):
+        mock_mpi = mocker.patch('mbtrack2.tracking.parallel.Mpi')
+        mock_mpi_instance = mock_mpi.return_value
+        mock_mpi_instance.comm.allgather.return_value = [generate_bunch() for _ in range(demo_ring.h)]
+        beam = Beam(ring=demo_ring)
+        beam.mpi_init()
+        beam.mpi_gather()
+        assert mock_mpi_instance.comm.allgather.called
+        beam.mpi_close()
+        assert not beam.mpi_switch
+        assert beam.mpi is None
\ No newline at end of file
diff --git a/tests/unit/tracking/test_rf.py b/tests/unit/tracking/test_rf.py
new file mode 100644
index 0000000..d41e51e
--- /dev/null
+++ b/tests/unit/tracking/test_rf.py
@@ -0,0 +1,420 @@
+import pytest
+import numpy as np
+from utility_test_functions import assert_attr_changed
+from mbtrack2 import (RFCavity, 
+                      CavityResonator, 
+                      ProportionalLoop, 
+                      ProportionalIntegralLoop,
+                      TunerLoop,
+                      DirectFeedback)
+
+class TestRFCavity:
+
+    # Initialize RFCavity with valid parameters and verify attributes are changed when tracked
+    def test_rf_cavity(self, demo_ring, small_bunch):
+        cavity = RFCavity(ring=demo_ring, m=1, Vc=1e6, theta=np.pi/4)
+        assert_attr_changed(cavity, small_bunch, attrs_changed=["delta"])
+
+@pytest.fixture
+def cav_res(demo_ring):
+    cav_res = CavityResonator(demo_ring, m=1, Rs=1e6, Q=1e4, QL=1e3, detune=-20e3, Ncav=4)
+    return cav_res
+
+@pytest.fixture
+def cav_res_tracking(demo_ring, cav_res):
+    cav_res.Vc = 1e6
+    cav_res.theta = np.arccos(demo_ring.U0/cav_res.Vc)
+    cav_res.set_generator(0.5)   
+    return cav_res
+
+class TestCavityResonator:
+
+    @pytest.mark.parametrize("n_bin", [(75), (1)])
+    def test_track_mpi(self, cav_res_tracking, beam_1bunch_mpi, n_bin):
+        cav_res_tracking.n_bin = n_bin
+        initial_phasor = cav_res_tracking.beam_phasor
+        initial_beam_phasor_record = cav_res_tracking.beam_phasor_record.copy()
+
+        beam_1bunch_mpi.mpi.share_distributions(beam_1bunch_mpi, n_bin=n_bin)
+        assert_attr_changed(cav_res_tracking, beam_1bunch_mpi,attrs_changed=["delta"])
+        
+        assert not np.array_equal(cav_res_tracking.beam_phasor, initial_phasor)
+        assert not np.array_equal(cav_res_tracking.beam_phasor_record, initial_beam_phasor_record)
+
+    @pytest.mark.parametrize("n_bin", [(75), (1)])
+    def test_track_no_mpi(self, cav_res_tracking, beam_uniform, n_bin):
+        cav_res_tracking.n_bin = n_bin
+        initial_phasor = cav_res_tracking.beam_phasor
+        initial_beam_phasor_record = cav_res_tracking.beam_phasor_record.copy()
+        
+        assert_attr_changed(cav_res_tracking, beam_uniform, attrs_changed=["delta"])
+
+        assert not np.array_equal(cav_res_tracking.beam_phasor, initial_phasor)
+        assert not np.array_equal(cav_res_tracking.beam_phasor_record, initial_beam_phasor_record)
+        
+    @pytest.mark.parametrize("n_bin", [(75), (1)])
+    def test_track_no_mpi_non_uniform(self, cav_res_tracking, beam_non_uniform, n_bin):
+        cav_res_tracking.n_bin = n_bin
+        initial_phasor = cav_res_tracking.beam_phasor
+        initial_beam_phasor_record = cav_res_tracking.beam_phasor_record.copy()
+        
+        assert_attr_changed(cav_res_tracking, beam_non_uniform, attrs_changed=["delta"])
+
+        assert not np.array_equal(cav_res_tracking.beam_phasor, initial_phasor)
+        assert not np.array_equal(cav_res_tracking.beam_phasor_record, initial_beam_phasor_record)
+        
+    # Track a beam with zero charge per macro-particle
+    def test_track_zero_charge_per_mp(self, cav_res_tracking, generate_beam):
+        beam = generate_beam(current_per_bunch=0)
+        assert_attr_changed(cav_res_tracking, beam, change=False)
+        
+    # Apply RF feedback loops correctly during tracking
+    def test_apply_rf_feedback(self, cav_res_tracking, beam_uniform, mocker):
+        cav_res_tracking.feedback.append(mocker.Mock())
+        cav_res_tracking.track(beam_uniform)
+        cav_res_tracking.feedback[0].track.assert_called_once(), "RF feedback should be applied"
+    
+    @pytest.mark.parametrize("ref_frame", [("beam"), ("rf")])
+    def test_phasor_decay(self, cav_res, ref_frame):
+        init_phasor = 1 + 1j
+        cav_res.beam_phasor = init_phasor
+        cav_res.phasor_decay(10e-6, ref_frame)
+        assert cav_res.beam_phasor != init_phasor
+        
+    @pytest.mark.parametrize("ref_frame", [("beam"), ("rf")])
+    def test_phasor_evol(self, cav_res, ref_frame):
+        init_phasor = 1 + 1j
+        cav_res.beam_phasor = init_phasor
+        profile = np.random.randint(1000, size=(10,))
+        bin_length = 1e-12
+        charge_per_mp = 1e-15
+        cav_res.phasor_evol(profile, bin_length, charge_per_mp, ref_frame)
+        assert cav_res.beam_phasor != init_phasor
+        
+    def test_phasor_init(self, cav_res, beam_non_uniform):
+        init_phasor = cav_res.beam_phasor
+        cav_res.init_phasor_track(beam_non_uniform)
+        phasor_init_phasor_track = cav_res.beam_phasor
+        
+        cav_res.beam_phasor = init_phasor
+        cav_res.init_phasor(beam_non_uniform)
+        phasor_init_phasor = cav_res.beam_phasor
+        
+        assert phasor_init_phasor != init_phasor
+        assert phasor_init_phasor_track != init_phasor
+        assert np.allclose(phasor_init_phasor, phasor_init_phasor_track, rtol=1e-2)
+        
+    # Setting detune updates _detune, _fr, _wr, and _psi correctly
+    def test_detune(self, cav_res):
+        detune_value = 1000
+        cav_res.detune = detune_value
+        assert cav_res._detune == detune_value
+        assert cav_res._fr == detune_value + cav_res.m * cav_res.ring.f1
+        assert cav_res._wr == cav_res._fr * 2 * np.pi
+        assert cav_res._psi == np.arctan(cav_res.QL * (cav_res._fr / (cav_res.m * cav_res.ring.f1) - (cav_res.m * cav_res.ring.f1) / cav_res._fr))
+
+    # update_feedback is called after setting detune
+    def test_update_feedback_called(self, mocker, cav_res):
+        mocker.spy(cav_res, 'update_feedback')
+        cav_res.detune = 1000
+        cav_res.update_feedback.assert_called_once()        
+
+    def test_plot_phasor_diagram(self, cav_res_tracking):
+        fig = cav_res_tracking.plot_phasor(0.5)
+        assert fig is not None
+        
+    # Calculating generator phasor using Vg and theta_g
+    def test_generator_phasor_calculation(self, cav_res):
+        cav_res.Vg = 1000
+        cav_res.theta_g = np.pi / 4
+        expected_phasor = 1000 * np.exp(1j * np.pi / 4)
+        assert cav_res.generator_phasor == expected_phasor
+
+    # Computing cavity phasor by summing generator and beam phasors
+    def test_cavity_phasor_computation(self, cav_res):
+        cav_res.Vg = 1000
+        cav_res.theta_g = np.pi / 4
+        cav_res.beam_phasor = 500 * np.exp(1j * np.pi / 6)
+        expected_phasor = cav_res.generator_phasor + cav_res.beam_phasor
+        assert cav_res.cavity_phasor == expected_phasor
+
+    # Retrieving cavity phasor record for each bunch
+    def test_cavity_phasor_record_retrieval(self, cav_res):
+        cav_res.generator_phasor_record = np.array([1+1j, 2+2j])
+        cav_res.beam_phasor_record = np.array([0.5+0.5j, 1+1j])
+        expected_record = np.array([1.5+1.5j, 3+3j])
+        assert np.allclose(cav_res.cavity_phasor_record, expected_record)
+
+    # Accessing ig phasor record when feedback is present
+    def test_ig_phasor_record_with_feedback(self, cav_res):
+        pi_loop = ProportionalIntegralLoop(cav_res.ring, cav_res, [1.0, 1.0], 10, 10, 10)
+        pi_loop.ig_phasor_record = np.array([1+1j, 2+2j])
+        cav_res.feedback.append(pi_loop)
+        assert np.allclose(cav_res.ig_phasor_record, pi_loop.ig_phasor_record)
+
+    # Determining cavity voltage and phase from cavity phasor
+    def test_cavity_voltage_and_phase_determination(self, cav_res):
+        cav_res.Vg = 1000
+        cav_res.theta_g = np.pi / 4
+        cav_res.beam_phasor = 500 * np.exp(1j * np.pi / 6)
+        expected_voltage = np.abs(cav_res.cavity_phasor)
+        expected_phase = np.angle(cav_res.cavity_phasor)
+        assert cav_res.cavity_voltage == expected_voltage
+        assert cav_res.cavity_phase == expected_phase
+
+    # Calculating beam voltage and phase from beam phasor
+    def test_beam_voltage_and_phase_calculation(self, cav_res):
+        cav_res.beam_phasor = 500 * np.exp(1j * np.pi / 6)
+        expected_voltage = np.abs(cav_res.beam_phasor)
+        expected_phase = np.angle(cav_res.beam_phasor)
+        assert cav_res.beam_voltage == expected_voltage
+        assert cav_res.beam_phase == expected_phase
+
+    # Handling feedback list with no ProportionalIntegralLoop or DirectFeedback
+    def test_ig_phasor_record_no_feedback(self, cav_res):
+        cav_res.feedback = []
+        expected_record = np.zeros(cav_res.ring.h)
+        assert np.allclose(cav_res.ig_phasor_record, expected_record)
+
+    # Calculating loaded shunt impedance RL
+    def test_loaded_shunt_impedance_rl(self, cav_res):
+        # Set up the initial conditions
+        cav_res.Rs = 1e6  # Shunt impedance
+        cav_res.QL = 1e3  # Loaded quality factor
+        cav_res.Q = 1e4   # Quality factor
+
+        # Calculate expected RL
+        expected_RL = cav_res.Rs / (1 + (cav_res.Q / cav_res.QL - 1))
+
+        # Assert that the calculated RL matches the expected value
+        assert cav_res.RL == expected_RL
+
+    # Set optimal coupling and detuning and verify changes in attributes
+    def test_set_optimal_coupling_and_detuning(self, demo_ring, cav_res):
+        initial_beta = cav_res.beta
+        initial_psi = cav_res.psi
+        cav_res.set_optimal_coupling(0.5)
+        cav_res.set_optimal_detune(0.5)
+        assert cav_res.beta != initial_beta
+        assert cav_res.psi != initial_psi
+    
+    # Verify that is_CBI_stable correctly calculates growth rate and mode number
+    def test_calculate_growth_rate_and_mode(self, cav_res_tracking):
+        growth_rate, mu = cav_res_tracking.is_CBI_stable(I0=0.5)
+        assert isinstance(growth_rate, float)
+        assert np.issubdtype(type(mu), np.integer)
+        
+    # Ensure is_CBI_stable returns growth rates for specified modes when modes is provided
+    def test_return_growth_rates_for_specified_modes(self, cav_res_tracking):
+        modes = [0, 1, 2]
+        growth_rates = cav_res_tracking.is_CBI_stable(I0=0.5, modes=modes)
+        assert len(growth_rates) == len(modes)
+        
+    def test_is_DC_Robinson_stable(self, cav_res_tracking):
+        assert isinstance(cav_res_tracking.is_DC_Robinson_stable(0.5), (bool,np.bool_))
+        
+    def test_plot_DC_Robinson_stability(self, cav_res_tracking):
+        fig = cav_res_tracking.plot_DC_Robinson_stability()
+        assert fig is not None
+        
+    @pytest.mark.parametrize("z,expected_type",
+                             [(1e-3, float),
+                              (np.linspace(-1e-3, 1e-3, 100), np.ndarray)])
+    def test_VRF(self, cav_res_tracking, z, expected_type):
+        assert isinstance(cav_res_tracking.VRF(z, 0.5), expected_type)
+        assert isinstance(cav_res_tracking.dVRF(z, 0.5), expected_type)
+        assert isinstance(cav_res_tracking.ddVRF(z, 0.5), expected_type)
+        assert isinstance(cav_res_tracking.deltaVRF(z, 0.5), expected_type)
+        
+    def test_sample_voltage_default_parameters(self, cav_res_tracking, beam_uniform):
+        cav_res_tracking.init_tracking(beam_uniform)
+        n_points=1e4
+        beam_phasor_init = 1 + 1j
+        cav_res_tracking.beam_phasor = beam_phasor_init
+        pos, voltage_rec = cav_res_tracking.sample_voltage(n_points=n_points)
+        assert len(pos) == n_points
+        assert len(voltage_rec) == n_points
+        assert np.allclose(cav_res_tracking.beam_phasor, beam_phasor_init)
+        
+    def test_set_generator(self, cav_res, demo_ring):
+        cav_res.Vc = 1e6
+        cav_res.theta = np.arccos(demo_ring.U0/cav_res.Vc)
+        cav_res.set_generator(0.5)
+        assert cav_res.Pg is not None
+        assert cav_res.Vgr is not None
+        assert cav_res.theta_gr is not None
+        assert cav_res.Vg is not None
+        assert cav_res.theta_g is not None
+        assert cav_res.generator_phasor_record is not None
+        
+    def test_functions(self, cav_res_tracking):
+        assert isinstance(cav_res_tracking.Pb(0.5), float)
+        assert isinstance(cav_res_tracking.Pr(0.5), float)
+        assert isinstance(cav_res_tracking.Vbr(0.5), float)
+        assert isinstance(cav_res_tracking.Vb(0.5), float)
+        assert isinstance(cav_res_tracking.Z(1e9), complex)
+        
+    def test_properties(self, cav_res_tracking):
+        assert cav_res_tracking.Pc > 0
+        assert cav_res_tracking.filling_time > 0
+        assert cav_res_tracking.loss_factor > 0
+        
+class TestProportionalLoop:
+    
+    @pytest.fixture
+    def prop_loop(self, demo_ring, cav_res_tracking):
+        return ProportionalLoop(demo_ring, cav_res_tracking, gain_A=0.5, gain_P=0.5, delay=2)
+
+    # track method updates cav_res.Vg and cav_res.theta_g correctly
+    def test_track_delay(self, prop_loop):
+        initial_Vg = prop_loop.cav_res.Vg
+        initial_theta_g = prop_loop.cav_res.theta_g
+        prop_loop.track()
+        assert prop_loop.cav_res.Vg == initial_Vg
+        assert prop_loop.cav_res.theta_g == initial_theta_g
+        
+    # track method updates cav_res.Vg and cav_res.theta_g correctly
+    def test_track_update(self, prop_loop):
+        initial_Vg = prop_loop.cav_res.Vg
+        initial_theta_g = prop_loop.cav_res.theta_g
+        prop_loop.track()
+        prop_loop.track() # delay=2 is 3 turns to take action
+        prop_loop.track()
+        assert prop_loop.cav_res.Vg != initial_Vg
+        assert prop_loop.cav_res.theta_g != initial_theta_g
+
+    # Initialization with delay less than 1 raises ValueError
+    def test_initialization_delay_less_than_one(self, demo_ring, cav_res):
+        with pytest.raises(ValueError):
+            ProportionalLoop(demo_ring, cav_res, gain_A=0.5, gain_P=0.5, delay=0)
+
+    # Initialization with non-integer delay converts it to integer
+    def test_initialization_non_integer_delay(self, demo_ring, cav_res):
+        loop = ProportionalLoop(demo_ring, cav_res, gain_A=0.5, gain_P=0.5, delay=2.7)
+        assert loop.delay == 2
+        
+class TestTunerLoop:
+    
+    @pytest.fixture
+    def tuner_loop(self, demo_ring, cav_res_tracking):
+        return TunerLoop(demo_ring, cav_res_tracking, gain=1, avering_period=2, offset=0)
+    
+    def test_track_once(self, tuner_loop):
+        initial_psi = tuner_loop.cav_res.psi
+        tuner_loop.track()
+        assert tuner_loop.diff != 0
+        assert tuner_loop.count == 1
+        assert tuner_loop.cav_res.psi == initial_psi
+
+    # Track method correctly adjusts cav_res.psi when count equals avering_period
+    def test_track_adjusts_psi(self, tuner_loop):
+        initial_psi = tuner_loop.cav_res.psi
+        for _ in range(tuner_loop.avering_period+1):
+            tuner_loop.track()
+        assert tuner_loop.diff == 0
+        assert tuner_loop.cav_res.psi != initial_psi
+
+    # Initialize with avering_period set to None and verify default calculation
+    def test_default_averaging_period_calculation(self, demo_ring, cav_res_tracking):
+        tuner_loop = TunerLoop(demo_ring, cav_res_tracking, avering_period=None)
+        fs = demo_ring.synchrotron_tune(cav_res_tracking.Vc) * demo_ring.f0
+        expected_period = int(2 / fs / demo_ring.T0)
+        assert tuner_loop.avering_period == expected_period
+
+    # Test track method with zero gain to ensure no changes to cav_res.psi
+    def test_track_with_zero_gain(self, tuner_loop):
+        tuner_loop.Pgain = 0
+        initial_psi = tuner_loop.cav_res.psi
+        for _ in range(tuner_loop.avering_period):
+            tuner_loop.track()
+        assert tuner_loop.cav_res.psi == initial_psi
+
+class TestProportionalIntegralLoop:
+    
+    @pytest.fixture
+    def pi_loop(self, demo_ring, cav_res_tracking):
+        loop = ProportionalIntegralLoop(demo_ring, cav_res_tracking, [0.5, 1e4], 8, 7, 5)
+        return loop
+
+    def test_track(self, pi_loop):
+        initial_ig_phasor = pi_loop.ig_phasor.copy()
+        generator_phasor_record_init = pi_loop.cav_res.generator_phasor_record.copy()
+        pi_loop.track()
+        assert not np.array_equal(initial_ig_phasor, pi_loop.ig_phasor)
+        assert not np.array_equal(generator_phasor_record_init, pi_loop.cav_res.generator_phasor_record)
+        
+    # Ig2Vg method correctly updates generator phasor record and cavity parameters
+    def test_Ig2Vg_updates_generator_phasor(self, pi_loop):
+        generator_phasor_record_init = pi_loop.cav_res.generator_phasor_record.copy()
+        Vg_init = pi_loop.cav_res.Vg
+        theta_g_init = pi_loop.cav_res.theta_g
+        pi_loop.Ig2Vg()
+        assert not np.allclose(generator_phasor_record_init, pi_loop.cav_res.generator_phasor_record)
+        assert pi_loop.cav_res.Vg != Vg_init
+        assert pi_loop.cav_res.theta_g != theta_g_init
+
+    # IIR filter processes input correctly and returns expected output
+    def test_IIR_filter_output(self, pi_loop):
+        input_signal = np.array([1.0 + 1.0j])
+        output = pi_loop.IIR(input_signal)
+        assert output is not None
+
+    # Init_FFconst initializes feedforward constant based on FF flag
+    def test_init_FFconst_with_FF_true(self, pi_loop):
+        pi_loop.init_FFconst()
+        assert pi_loop.FFconst != 0
+        
+    def test_parameters(self, pi_loop):
+        assert isinstance(pi_loop.Vg2Ig(1e6), complex)
+        pi_loop.IIR_init(1e9)
+        assert isinstance(pi_loop.IIRcutoff, float)
+        assert isinstance(pi_loop.IIR(1.0 + 1.0j), complex)
+
+class TestDirectFeedback:
+    
+    @pytest.fixture
+    def drf_fb(self, demo_ring, cav_res_tracking):
+        drf_fb = DirectFeedback(DFB_gain=1.0, 
+                              DFB_phase_shift=0.1, 
+                              ring=demo_ring, 
+                              cav_res=cav_res_tracking, 
+                              gain=[0.5, 1e4], 
+                              sample_num=8, 
+                              every=7, 
+                              delay=5)
+        return drf_fb
+    
+    def test_properties(self, drf_fb):
+        assert isinstance(drf_fb.DFB_phase_shift, float)
+        assert isinstance(drf_fb.phase_shift, float)
+        assert isinstance(drf_fb.DFB_psi, float)
+        assert isinstance(drf_fb.DFB_alpha, float)
+        assert isinstance(drf_fb.DFB_gamma, float)
+        assert isinstance(drf_fb.DFB_Rs, float)
+        
+    def test_methods(self, drf_fb):
+        vg_main, vg_drf = drf_fb.DFB_Vg()
+        assert isinstance(vg_main, complex)
+        assert isinstance(vg_drf, complex)
+        assert isinstance(drf_fb.DFB_fs(), float)
+
+    def test_track(self, drf_fb):
+        initial_ig_phasor_record = drf_fb.ig_phasor_record.copy()
+        initial_dfb_ig_phasor = drf_fb.DFB_ig_phasor.copy()
+        generator_phasor_record_init = drf_fb.cav_res.generator_phasor_record.copy()
+        drf_fb.track()
+        assert not np.array_equal(drf_fb.ig_phasor_record, initial_ig_phasor_record)
+        assert not np.array_equal(drf_fb.DFB_ig_phasor, initial_dfb_ig_phasor)
+        assert not np.array_equal(generator_phasor_record_init, drf_fb.cav_res.generator_phasor_record)
+
+    # Set DFB parameters using DFB_parameter_set and verify changes in DFB_ig_phasor
+    def test_set_dfb_parameters(self, drf_fb):
+        initial_DFB_ig_phasor = drf_fb.DFB_ig_phasor.copy()
+        initial_ig_phasor = drf_fb.ig_phasor.copy()
+        
+        drf_fb.DFB_parameter_set(DFB_gain=2.0, DFB_phase_shift=0.2)
+        
+        assert not np.array_equal(drf_fb.DFB_ig_phasor, initial_DFB_ig_phasor)
+        assert not np.array_equal(drf_fb.ig_phasor, initial_ig_phasor)
+        assert drf_fb.DFB_gain == 2.0
+        assert drf_fb.DFB_phase_shift == 0.2
\ No newline at end of file
diff --git a/tests/unit/tracking/test_spacecharge.py b/tests/unit/tracking/test_spacecharge.py
new file mode 100644
index 0000000..b60a215
--- /dev/null
+++ b/tests/unit/tracking/test_spacecharge.py
@@ -0,0 +1,37 @@
+import pytest
+from mbtrack2 import TransverseSpaceCharge
+from utility_test_functions import assert_attr_changed
+import numpy as np
+
+class TestTransverseSpaceCharge:
+
+    # Track a bunch with valid x, y coordinates through the space charge element
+    def test_track_with_track_alive(self, demo_ring, large_bunch):
+        tsc = TransverseSpaceCharge(demo_ring, 1000.0)
+        large_bunch.alive[5:700] = False
+        assert_attr_changed(tsc, large_bunch, attrs_changed=["xp", "yp"])
+        
+    # Track a bunch with valid x, y coordinates through the space charge element
+    def test_track_without_track_alive(self, demo_ring, large_bunch):
+        tsc = TransverseSpaceCharge(demo_ring, 1000.0)
+        large_bunch.track_alive = False
+        assert_attr_changed(tsc, large_bunch, attrs_changed=["xp", "yp"])
+
+    # Initialize with zero interaction length and verify no kicks are applied
+    def test_zero_interaction_length_no_kicks(self, demo_ring, small_bunch):
+        tsc = TransverseSpaceCharge(demo_ring, 0.0)
+        assert_attr_changed(tsc, small_bunch, attrs_changed=["xp", "yp"], change=False)
+
+    # Handle a bunch with bins having zero particles without errors
+    def test_handle_bins_with_zero_particles(self, demo_ring, small_bunch):
+        small_bunch.alive[:] = False
+        tsc = TransverseSpaceCharge(demo_ring, 1.0)
+        tsc.track(small_bunch)
+        assert True
+
+    # Test with different n_bins
+    @pytest.mark.parametrize("n_bins",[(1),(2),(100)])
+    def test_n_bins_zero_behavior(self, demo_ring, small_bunch, n_bins):
+        tsc = TransverseSpaceCharge(demo_ring, 1.0, n_bins=n_bins)
+        tsc.track(small_bunch)
+        assert True
\ No newline at end of file
diff --git a/tests/unit/tracking/test_synchrotron.py b/tests/unit/tracking/test_synchrotron.py
new file mode 100644
index 0000000..dc59e4a
--- /dev/null
+++ b/tests/unit/tracking/test_synchrotron.py
@@ -0,0 +1,120 @@
+import numpy as np
+import pytest
+import at
+from scipy.constants import c, e
+from mbtrack2 import Electron, Synchrotron
+
+@pytest.fixture
+def demo_ring(local_optics):
+    h = 20
+    L = 100
+    E0 = 1e9
+    particle = Electron()
+    ac = 1e-3
+    U0 = 250e3
+    tau = np.array([10e-3, 10e-3, 5e-3])
+    tune = np.array([18.2, 10.3])
+    emit = np.array([50e-9, 50e-9*0.01])
+    sigma_0 = 30e-12
+    sigma_delta = 1e-3
+    chro = [1.0,1.0]
+   
+    ring = Synchrotron(h, local_optics, particle, L=L, E0=E0, ac=ac, U0=U0, tau=tau,
+                       emit=emit, tune=tune, sigma_delta=sigma_delta, 
+                       sigma_0=sigma_0, chro=chro)
+    
+    return ring
+
+@pytest.fixture
+def demo_ring_h1(demo_ring):
+    demo_ring.h = 1
+    return demo_ring
+
+@pytest.fixture
+def ring_with_at_lattice(at_optics):
+    h = 416
+    tau = np.array([6.56e-3, 6.56e-3, 3.27e-3])
+    emit = np.array([3.9e-9, 3.9e-9*0.01])
+    sigma_0 = 15e-12
+    sigma_delta = 1.025e-3
+    particle = Electron()
+    ring = Synchrotron(h, at_optics, particle, tau=tau, emit=emit, 
+                       sigma_0=sigma_0, sigma_delta=sigma_delta)
+    return ring
+
+class TestSynchrotron:
+
+    def test_synchrotron_values(self, demo_ring):
+        h = 20
+        L = 100
+        E0 = 1e9
+        particle = Electron()
+        ac = 1e-3
+        U0 = 250e3
+        tau = np.array([10e-3, 10e-3, 5e-3])
+        tune = np.array([18.2, 10.3])
+        emit = np.array([50e-9, 50e-9*0.01])
+        sigma_0 = 30e-12
+        sigma_delta = 1e-3
+        chro = [1.0,1.0]
+    
+        assert pytest.approx(demo_ring.h) == h
+        assert pytest.approx(demo_ring.L) == L
+        assert pytest.approx(demo_ring.E0) == E0
+        assert pytest.approx(demo_ring.U0) == U0
+        assert pytest.approx(demo_ring.ac) == ac
+        np.testing.assert_allclose(demo_ring.tau, tau)
+        np.testing.assert_allclose(demo_ring.tune, tune)
+        np.testing.assert_allclose(demo_ring.emit, emit)
+        assert pytest.approx(demo_ring.sigma_0) == sigma_0
+        assert pytest.approx(demo_ring.sigma_delta) == sigma_delta
+        np.testing.assert_allclose(demo_ring.chro, chro)
+        assert pytest.approx(demo_ring.T0) == L/c
+        assert pytest.approx(demo_ring.T1) == L/c/h
+        assert pytest.approx(demo_ring.f0) == c/L
+        assert pytest.approx(demo_ring.f1) == 1/(L/c/h)
+        assert pytest.approx(demo_ring.omega0) == 2 * np.pi * c/L
+        assert pytest.approx(demo_ring.omega1) == 2 * np.pi * 1/(L/c/h)
+        assert pytest.approx(demo_ring.k1) == 2 * np.pi * 1/(L/c/h) / c
+        assert pytest.approx(demo_ring.gamma) == E0 / (particle.mass * c**2 / e)
+        assert pytest.approx(demo_ring.beta) == np.sqrt(1 - (E0 / (particle.mass * c**2 / e))**-2)
+    
+    def test_synchrotron_mcf(self, demo_ring):
+        demo_ring.mcf_order = [5e-4, 1e-4, 1e-3]
+        assert pytest.approx(demo_ring.mcf(0.5)) == 5e-4*(0.5**2) + 1e-4*0.5 + 1e-3
+        assert pytest.approx(demo_ring.eta(0.5)) == demo_ring.mcf(0.5) - 1 / (demo_ring.gamma**2)
+    
+    def test_synchrotron_tune(self, demo_ring):
+        tuneS = demo_ring.synchrotron_tune(1e6)
+        assert pytest.approx(tuneS, rel=1e-4) == 0.0017553
+        
+    def test_synchrotron_sigma(self, demo_ring):
+        np.testing.assert_allclose(demo_ring.sigma(), np.array([2.23606798e-04, 2.23606798e-04, 2.23606798e-05, 2.23606798e-05]))
+
+    def test_synchrotron_sigma_position(self, ring_with_at_lattice):
+        pos = np.linspace(0, ring_with_at_lattice.L, 100)
+        sig = ring_with_at_lattice.sigma(pos)
+        assert sig.shape == (4, 100)
+        
+    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_chroma(self, ring_with_at_lattice):
+        ring_with_at_lattice.get_chroma()
+        assert len(ring_with_at_lattice.chro) == 8
+        
+    def test_get_mcf_order(self, ring_with_at_lattice):
+        ring_with_at_lattice.get_mcf_order()
+        assert len(ring_with_at_lattice.mcf_order) == 3
+    
+    def test_synchrotron_long_twiss(self, demo_ring):
+        tuneS, long_alpha, long_beta, long_gamma = demo_ring.get_longitudinal_twiss(1e6, add=False)
+        assert pytest.approx(tuneS, rel=1e-4) == demo_ring.synchrotron_tune(1e6)
+        assert pytest.approx(long_alpha, rel=1e-4) == -0.0055146
+        assert pytest.approx(long_beta, rel=1e-4) == 3.0236e-08
+        assert pytest.approx(long_gamma, rel=1e-4) == 3.30736e7
+    
+    def test_to_pyat(self, demo_ring):
+        pyat_simple_ring = demo_ring.to_pyat(1e6)
+        assert isinstance(pyat_simple_ring, at.lattice.lattice_object.Lattice)
\ No newline at end of file
diff --git a/tests/unit/tracking/test_wakepotential.py b/tests/unit/tracking/test_wakepotential.py
new file mode 100644
index 0000000..8581839
--- /dev/null
+++ b/tests/unit/tracking/test_wakepotential.py
@@ -0,0 +1,316 @@
+import pytest
+import numpy as np
+import pandas as pd
+from mbtrack2 import WakePotential, Resonator, LongRangeResistiveWall
+from utility_test_functions import assert_attr_changed
+
+@pytest.fixture
+def generate_resonator():
+    def generate(time=np.linspace(-1000e-12, 1000e-12, int(1e5)), 
+                 frequency=np.linspace(0.1e6, 100e9, int(1e5)), 
+                 Rs=100, 
+                 fr=1e9, 
+                 Q=20, 
+                 plane="long"):
+        res = Resonator(time, frequency, Rs, fr, Q, plane)
+        return res
+    return generate
+
+@pytest.fixture
+def generate_wakepotential(demo_ring, generate_resonator):
+    def generate(ring=demo_ring, 
+                 wakefield=None, 
+                 n_bin=80, 
+                 interp_on_postion=True, 
+                 **kwargs):
+        if wakefield is None:
+            wakefield = generate_resonator(**kwargs)
+        wp = WakePotential(ring, wakefield, n_bin, interp_on_postion)
+        return wp
+    return generate
+
+class TestWakePotential:
+
+    # Compute charge density profile for a given Bunch object
+    def test_charge_density_profile(self, generate_wakepotential, small_bunch):
+        wp = generate_wakepotential()
+        wp.charge_density(small_bunch)
+        assert len(wp.rho) == len(wp.tau)
+        assert wp.dtau > 0
+
+    # Calculate dipole moment for a Bunch object on specified plane
+    @pytest.mark.parametrize("plane", [("x"), ("y")])
+    def test_dipole_moment_calculation(self, generate_wakepotential, small_bunch, plane):
+        wp = generate_wakepotential()
+        wp.charge_density(small_bunch)
+        dipole = wp.dipole_moment(small_bunch, plane, wp.tau)
+        assert len(dipole) == len(wp.tau)
+
+    # Prepare wake function for a specified wake type
+    @pytest.mark.parametrize("plane", [("x"), ("y"),("long")])
+    def test_prepare_wakefunction_smaller_window(self, generate_wakepotential, plane):
+        wp = generate_wakepotential(plane=plane)
+        tau = np.linspace(-100e-12, 100e-12, int(1e5))
+        if plane == "x" or plane == "y":
+            comp = f"W{plane}dip"
+        else:
+            comp = "Wlong"
+        tau0, dtau0, W0 = wp.prepare_wakefunction(comp, tau)
+        assert len(tau0) > 0
+        assert len(W0) > 0
+        
+    # Prepare wake function for a specified wake type
+    @pytest.mark.parametrize("plane", [("x"), ("y"),("long")])
+    def test_prepare_wakefunction_larger_window(self, generate_wakepotential, plane):
+        wp = generate_wakepotential(plane=plane)
+        tau = np.linspace(-2000e-12, 2000e-12, int(1e5))
+        if plane == "x" or plane == "y":
+            comp = f"W{plane}dip"
+        else:
+            comp = "Wlong"
+        tau0, dtau0, W0 = wp.prepare_wakefunction(comp, tau)
+        assert len(tau0) > 0
+        assert len(W0) > 0
+
+    # Compute wake potential for a Bunch object and wake type
+    @pytest.mark.parametrize("plane", [("x"), ("y"),("long")])
+    def test_compute_wakepotential(self, generate_wakepotential, small_bunch, plane):
+        wp = generate_wakepotential(plane=plane)
+        wp.charge_density(small_bunch)
+        if plane == "x" or plane == "y":
+            comp = f"W{plane}dip"
+        else:
+            comp = "Wlong"
+        tau0, Wp = wp.get_wakepotential(small_bunch, comp)
+        assert len(Wp) == len(tau0)
+
+    # Track a Bunch object through the WakePotential element
+    @pytest.mark.parametrize("plane, attr", [("x","xp"), ("y","yp"),("long","delta")])
+    def test_track_bunch(self, generate_wakepotential, small_bunch, plane, attr):
+        wp = generate_wakepotential(plane=plane)
+        assert_attr_changed(wp, small_bunch, attrs_changed=[attr])
+        
+    # Track a Bunch object through the WakePotential element
+    @pytest.mark.parametrize("plane, attr", [("x","xp"), ("y","yp"),("long","delta")])
+    def test_track_bunch_no_exact_interp(self, generate_wakepotential, small_bunch, plane, attr):
+        wp = generate_wakepotential(plane=plane, interp_on_postion=False)
+        assert_attr_changed(wp, small_bunch, attrs_changed=[attr])
+
+    # Handle empty Bunch object in track method
+    @pytest.mark.parametrize("plane, attr", [("x","xp"), ("y","yp"),("long","delta")])
+    def test_track_empty_bunch(self, generate_wakepotential, small_bunch, plane, attr):
+        wp = generate_wakepotential(plane=plane)
+        small_bunch.alive[:] = False
+        assert_attr_changed(wp, small_bunch, attrs_changed=[attr], change=False)
+
+    # Manage non-uniformly sampled wake functions
+    def test_non_uniform_sampling_error(self, generate_wakepotential):
+        with pytest.raises(ValueError):
+            wp = generate_wakepotential(time=np.logspace(1, 5, 50))
+            wp.check_sampling()
+
+    # Calculate reference loss factor and compare to Gaussian bunch
+    @pytest.mark.parametrize("plane", [("x"), ("y"),("long")])
+    def test_reference_loss_factor_comparison(self, generate_wakepotential, large_bunch, plane):
+        # Generate a WakePotential instance
+        wp = generate_wakepotential(plane=plane)
+    
+        # Calculate the reference loss factor using the small bunch
+        large_bunch["x"] += 1e-3
+        large_bunch["y"] += 1e-3
+        wp.track(large_bunch)
+        loss_data = wp.reference_loss(large_bunch)
+    
+        # Check if the loss data is a DataFrame and contains expected columns
+        assert isinstance(loss_data, pd.DataFrame), "Loss data should be a DataFrame"
+        assert 'TD factor' in loss_data.columns, "DataFrame should contain 'TD factor' column"
+        assert 'FD factor' in loss_data.columns, "DataFrame should contain 'FD factor' column"
+        assert 'Relative error [%]' in loss_data.columns, "DataFrame should contain 'Relative error [%]' column"
+    
+        # Verify that the calculated loss factors are within a reasonable range
+        for index, row in loss_data.iterrows():
+            assert abs(row['Relative error [%]']) < 1, f"Relative error for {index} is too high"
+
+    # Reduce wake function sampling by a specified factor
+    def test_reduce_sampling(self, generate_wakepotential):
+        # Create a WakePotential instance with a mock wakefield
+        wp = generate_wakepotential()
+    
+        # Original length of the wake function data
+        original_length = len(wp.wakefield.Wlong.data.index)
+    
+        # Define a reduction factor
+        reduction_factor = 2
+    
+        # Reduce the sampling of the wake function
+        wp.reduce_sampling(reduction_factor)
+    
+        # New length of the wake function data after reduction
+        new_length = len(wp.wakefield.Wlong.data.index)
+    
+        # Assert that the new length is approximately half of the original length
+        assert new_length == original_length // reduction_factor
+
+    # Test plotting functions with different plot options
+    def test_plot_last_wake_with_various_options(self, generate_wakepotential, small_bunch):
+        wp = generate_wakepotential(plane="x")
+        wp.track(small_bunch)
+    
+        # Test with default options
+        fig1 = wp.plot_last_wake('Wxdip')
+        assert fig1 is not None
+    
+        # Test with plot_rho=False
+        fig2 = wp.plot_last_wake('Wxdip', plot_rho=False)
+        assert fig2 is not None
+    
+        # Test with plot_dipole=True
+        fig3 = wp.plot_last_wake('Wxdip', plot_dipole=True)
+        assert fig3 is not None
+    
+        # Test with plot_wake_function=False
+        fig4 = wp.plot_last_wake('Wxdip', plot_wake_function=False)
+        assert fig4 is not None
+        
+    @pytest.mark.parametrize("plane", [("x"), ("y"),("long")])
+    def test_get_gaussian_wakepotential(self, generate_wakepotential, plane):
+        wp = generate_wakepotential(plane=plane)
+        if plane == "x" or plane == "y":
+            comp = f"W{plane}dip"
+        else:
+            comp = "Wlong"
+        
+        tau0, W0, Wp, profile0, dipole0 = wp.get_gaussian_wakepotential(1e-12, wake_type=comp)
+        
+        assert tau0 is not None
+        assert W0 is not None
+        assert Wp is not None
+        assert profile0 is not None
+        assert dipole0 is not None
+        
+    @pytest.mark.parametrize("plane", [("x"), ("y"),("long")])
+    def test_plot_gaussian_wake(self, generate_wakepotential, plane):
+        wp = generate_wakepotential(plane=plane)
+        if plane == "x" or plane == "y":
+            comp = f"W{plane}dip"
+        else:
+            comp = "Wlong"
+        fig = wp.plot_gaussian_wake(sigma=10e-12, wake_type=comp)
+        assert fig is not None
+    
+@pytest.fixture
+def generate_lrrw(demo_ring, beam_uniform):
+    def generate(ring=demo_ring,
+                 beam=beam_uniform,
+                 length=demo_ring.L,
+                 rho=1e-6,
+                 radius=6e-12,
+                 types=["Wlong", "Wxdip", "Wydip"],
+                 nt=50,
+                 x3=None,
+                 y3=None):
+        lrrw = LongRangeResistiveWall(ring=ring, 
+                                      beam=beam, 
+                                      length=length, 
+                                      rho=rho,
+                                      radius=radius,
+                                      types=types,
+                                      nt=nt, 
+                                      x3=x3,
+                                      y3=y3)
+        return lrrw
+    return generate
+
+class TestLongRangeResistiveWall:
+    
+    # Test tracking w/ uniform beam
+    @pytest.mark.parametrize("types, attr", [("Wxdip","xp"), ("Wydip","yp"), ("Wlong","delta")])
+    def test_track(self, generate_lrrw, beam_uniform, types, attr):
+        lrrw = generate_lrrw(types=types)
+        for i, bunch in enumerate(beam_uniform.not_empty):
+            setattr(self, "initial_attr_{i}", bunch[attr].copy())
+        with np.errstate(over='ignore'):
+            lrrw.track(beam_uniform)
+        for i, bunch in enumerate(beam_uniform.not_empty):
+            assert not np.allclose(getattr(self, "initial_attr_{i}"), bunch[attr])
+            
+    # Test tracking w/ non-uniform beam
+    @pytest.mark.parametrize("types, attr", [("Wxdip","xp"), ("Wydip","yp"), ("Wlong","delta")])
+    def test_track_non_uniform(self, generate_lrrw, beam_non_uniform, types, attr):
+        lrrw = generate_lrrw(types=types, beam=beam_non_uniform)
+        for i, bunch in enumerate(beam_non_uniform.not_empty):
+            setattr(self, "initial_attr_{i}", bunch[attr].copy())
+        with np.errstate(over='ignore'):
+            lrrw.track(beam_non_uniform)
+        for i, bunch in enumerate(beam_non_uniform.not_empty):
+            assert not np.allclose(getattr(self, "initial_attr_{i}"), bunch[attr])
+            
+    # Test tracking w/ mpi beam
+    @pytest.mark.parametrize("types, attr", [("Wxdip","xp"), ("Wydip","yp"), ("Wlong","delta")])
+    def test_track_mpi(self, generate_lrrw, beam_1bunch_mpi, types, attr):
+        lrrw = generate_lrrw(types=types, beam=beam_1bunch_mpi)
+        bunch = beam_1bunch_mpi[beam_1bunch_mpi.mpi.bunch_num]
+        initial_attr = bunch[attr].copy()
+        with np.errstate(over='ignore'):
+            lrrw.track(beam_1bunch_mpi)
+            lrrw.track(beam_1bunch_mpi) # two turns are needed to see the effect of the bunch on itself
+        assert not np.allclose(initial_attr, bunch[attr])
+        
+    # test that two turns are needed to see the effect of the bunch on itself
+    def test_kick_signle_bunch(self, generate_lrrw, beam_1bunch_mpi):
+        lrrw = generate_lrrw(beam=beam_1bunch_mpi)
+        with np.errstate(over='ignore'):
+            lrrw.track(beam_1bunch_mpi)
+            kick = lrrw.get_kick(0, "Wlong")
+        assert kick == 0
+
+    @pytest.mark.parametrize("plane", [("long"), ("x"), ("y")])
+    def test_wake(self, generate_lrrw, plane):
+        lrrw = generate_lrrw()
+        t = 1e-9
+        if plane == "long":
+            wake = lrrw.Wlong(t)
+        else:
+            wake = lrrw.Wdip(t, plane)
+        assert isinstance(wake, float)
+
+    # Update tables with a Beam object and verify the correct table updates
+    @pytest.mark.parametrize("beam", [("beam_uniform"), ("beam_1bunch_mpi")])
+    def test_update_tables_with_beam(self, generate_lrrw, request, beam):
+        beam = request.getfixturevalue(beam)
+        lrrw = generate_lrrw()
+        initial_tau = np.copy(lrrw.tau)
+        initial_x = np.copy(lrrw.x)
+        initial_y = np.copy(lrrw.y)
+        initial_charge = np.copy(lrrw.charge)
+        lrrw.update_tables(beam)
+        assert not np.array_equal(lrrw.tau, initial_tau)
+        assert not np.array_equal(lrrw.x, initial_x)
+        assert not np.array_equal(lrrw.y, initial_y)
+        assert not np.array_equal(lrrw.charge, initial_charge)
+
+    # Verify the behavior when the approximated wake functions are not valid
+    def test_invalid_approximated_wake_functions(self, demo_ring, beam_uniform):
+        demo_ring.T1 = 1e-18  # Set T1 to a very small value to trigger the error
+        with pytest.raises(ValueError):
+            LongRangeResistiveWall(demo_ring, beam_uniform, 100, 1e-6, 6e-12)
+
+    # Test track_bunch method with different wake types and verify correct kick application
+    def test_track_bunch_kick_application(self, generate_lrrw, beam_uniform):
+        # Arrange
+        lrrw = generate_lrrw()
+        bunch = beam_uniform[0]
+        initial_delta = bunch["delta"].copy()
+        initial_xp = bunch["xp"].copy()
+        initial_yp = bunch["yp"].copy()
+    
+        # Act
+        lrrw.track_bunch(bunch, rank=0)
+    
+        # Assert
+        if "Wlong" in lrrw.types:
+            assert not np.array_equal(bunch["delta"], initial_delta), "Longitudinal kick not applied correctly"
+        if "Wxdip" in lrrw.types:
+            assert not np.array_equal(bunch["xp"], initial_xp), "Horizontal dipole kick not applied correctly"
+        if "Wydip" in lrrw.types:
+            assert not np.array_equal(bunch["yp"], initial_yp), "Vertical dipole kick not applied correctly"
\ No newline at end of file
diff --git a/tests/unit/utilities/test_optics.py b/tests/unit/utilities/test_optics.py
new file mode 100644
index 0000000..c3d7fe0
--- /dev/null
+++ b/tests/unit/utilities/test_optics.py
@@ -0,0 +1,295 @@
+import numpy as np
+import pytest
+import at
+import matplotlib.pyplot as plt
+from pathlib import Path
+from mbtrack2 import Optics, PhysicalModel
+
+@pytest.fixture
+def local_optics():
+    beta = np.array([1, 1])
+    alpha = np.array([0, 0])
+    dispersion = np.array([0, 0, 0, 0])
+    local_optics = Optics(local_beta=beta, local_alpha=alpha, 
+                      local_dispersion=dispersion)
+    return local_optics
+
+@pytest.fixture
+def at_optics():
+    path_to_file = Path(__file__).parent
+    lattice_file = path_to_file / ".." / ".." / ".." / "examples" / "SOLEIL_OLD.mat"
+    at_optics = Optics(lattice_file=lattice_file)
+    return at_optics
+
+class TestOptics:
+
+    # Initialize Optics with a valid lattice file and verify attributes are set correctly
+    def test_initialize_with_at_lattice(self, at_optics):
+        assert not at_optics.use_local_values
+        assert at_optics.lattice is not None
+
+    # Initialize Optics with local parameters and verify attributes are set correctly
+    def test_initialize_with_local_parameters(self, local_optics):
+        gamma = (1 + local_optics.local_alpha**2) / local_optics.local_beta
+        assert local_optics.use_local_values
+        np.testing.assert_allclose(gamma, local_optics.local_gamma)
+
+    # Load a lattice using load_from_AT and verify optic functions are interpolated
+    def test_interpolation(self, at_optics):
+        lattice = at_optics.lattice
+        
+        refpts = np.arange(0, len(lattice))
+        twiss0, tune, chrom, twiss = at.linopt(lattice,
+                                               refpts=refpts,
+                                               get_chrom=True)
+        randnum = np.random.randint(0, len(lattice))
+        pos = twiss.s_pos[randnum]
+        
+        np.testing.assert_allclose(at_optics.beta(pos), twiss.beta[randnum, :], rtol=1e-3, atol=1e-4)
+        np.testing.assert_allclose(at_optics.alpha(pos), twiss.alpha[randnum, :], rtol=1e-3, atol=1e-4)
+        np.testing.assert_allclose(at_optics.mu(pos), twiss.mu[randnum, :], rtol=1e-3, atol=1e-4)
+        np.testing.assert_allclose(at_optics.dispersion(pos), twiss.dispersion[randnum, :], rtol=1e-3, atol=1e-4)
+
+    # Retrieve beta, alpha, gamma, dispersion, and mu functions at specified positions
+    def test_local_optic_functions(self, local_optics):
+        position = np.array([0.5])
+        beta = np.squeeze(local_optics.beta(position))
+        alpha = np.squeeze(local_optics.alpha(position))
+        gamma = np.squeeze(local_optics.gamma(position))
+        dispersion = np.squeeze(local_optics.dispersion(position))
+        mu = np.squeeze(local_optics.mu(position))
+        
+        np.testing.assert_allclose(beta, local_optics.local_beta)
+        np.testing.assert_allclose(alpha, local_optics.local_alpha)
+        np.testing.assert_allclose(gamma, local_optics.local_gamma)
+        np.testing.assert_allclose(dispersion, local_optics.local_dispersion)
+        np.testing.assert_allclose(mu, np.array([0, 0]))
+
+    # Plot optical variables and verify the plot is generated correctly
+    def test_optics_plot(self, local_optics):
+        fig = local_optics.plot('beta', 'x')
+        assert fig is not None
+
+class TestPhysicalModel:
+    
+    @pytest.fixture
+    def generate_pm(self, ring_with_at_lattice):
+        def generate(ring=ring_with_at_lattice,
+                     x_right=0.01,
+                     y_top=0.02,
+                     shape="rect",
+                     rho=1e-7,
+                     x_left=None,
+                     y_bottom=None,
+                     n_points=1e4
+                     ):
+            pm = PhysicalModel(ring, 
+                                x_right=x_right, 
+                                y_top=y_top, 
+                                shape=shape,
+                                rho=rho,
+                                x_left=x_left,
+                                y_bottom=y_bottom,
+                                n_points=n_points)
+            return pm
+        return generate
+
+    # Initialize PhysicalModel with default symmetric aperture values and verify array shapes
+    def test_init_default_symmetric_aperture(self, generate_pm):
+        n_points=1e3
+        pm = generate_pm(n_points=n_points)
+        assert pm.x_right.shape == (int(n_points),)
+        assert pm.x_left.shape == (int(n_points),)
+        assert np.allclose(pm.x_left, -pm.x_right)
+        assert np.allclose(pm.y_bottom, -pm.y_top)
+
+    # Change aperture values in a specific section using change_values() with symmetric settings
+    def test_change_values_symmetric(self, generate_pm):
+        x_right_init = 0.01
+        y_top_init = 0.02
+        shape_init = 'rect'
+        rho_init = 1e-7
+        model = generate_pm(x_right=x_right_init, 
+                            y_top=y_top_init, 
+                            shape=shape_init,
+                            rho=rho_init)
+        x_right_modif = 0.015
+        y_top_modif = 0.025
+        shape_modif = 'elli'
+        rho_modif = 1e-6
+        start_position = 10
+        end_position = 20
+        model.change_values(start_position, 
+                            end_position, 
+                            x_right=x_right_modif, 
+                            y_top=y_top_modif, 
+                            shape=shape_modif,
+                            rho=rho_modif)
+        idx = (model.position > start_position) & (model.position < end_position)
+        idx2 = ((model.position[:-1] > start_position) &
+                (model.position[1:] < end_position))
+        
+        # Assert modif
+        assert np.allclose(model.x_right[idx], x_right_modif)
+        assert np.allclose(model.x_left[idx], -x_right_modif)
+        assert np.allclose(model.y_top[idx], y_top_modif)
+        assert np.allclose(model.y_bottom[idx], -y_top_modif)
+        assert np.all(model.shape[idx2] == shape_modif)
+        assert np.allclose(model.rho[idx2], rho_modif)
+        # Assert init values
+        assert np.allclose(model.x_right[~idx], x_right_init)
+        assert np.allclose(model.x_left[~idx], -x_right_init)
+        assert np.allclose(model.y_top[~idx], y_top_init)
+        assert np.allclose(model.y_bottom[~idx], -y_top_init)
+        assert np.all(model.shape[~idx2] == shape_init)
+        assert np.allclose(model.rho[~idx2], rho_init)
+        
+    # Change values with sym=False and verify asymmetric updates
+    def test_change_values_asymmetric(self, generate_pm):
+        x_right_init = 0.01
+        y_top_init = 0.02
+        model = generate_pm(x_right=x_right_init, y_top=y_top_init)
+        model.change_values(10, 20, x_right=0.02, x_left=-0.015, sym=False)
+        idx = (model.position > 10) & (model.position < 20)
+        assert np.allclose(model.x_right[idx], 0.02)
+        assert np.allclose(model.x_left[idx], -0.015)
+
+    # Create tapered transition between two positions with different aperture values
+    def test_taper_transition(self, generate_pm):
+        x_right_init = 0.01
+        y_top_init = 0.02
+        shape_init = 'rect'
+        rho_init = 1e-7
+        model = generate_pm(x_right=x_right_init, 
+                            y_top=y_top_init,
+                            shape=shape_init,
+                            rho=rho_init)
+        x_right_start = 0.01
+        x_right_end = 0.025
+        shape_modif = 'elli'
+        rho_modif = 1e-6
+        start_position = 10
+        end_position = 20
+        model.taper(start_position, 
+                    end_position, 
+                    x_right_start=x_right_start, 
+                    x_right_end=x_right_end,
+                    shape=shape_modif,
+                    rho=rho_modif)
+        idx = (model.position > start_position) & (model.position < end_position)
+        idx2 = ((model.position[:-1] > start_position) &
+                (model.position[1:] < end_position))
+        
+        # Assert edge points
+        assert model.x_right[idx][0] == pytest.approx(x_right_start)
+        assert model.x_right[idx][-1] == pytest.approx(x_right_end)
+        assert model.x_left[idx][0] == pytest.approx(-x_right_start)
+        assert model.x_left[idx][-1] == pytest.approx(-x_right_end)
+        # Assert modif
+        assert np.all(model.x_right[idx][1:] > x_right_start)
+        assert np.all(model.x_left[idx][1:] < -x_right_start)
+        assert np.all(model.x_right[idx][:-1] < x_right_end)
+        assert np.all(model.x_left[idx][:-1] > -x_right_end)
+        assert np.all(model.shape[idx2] == shape_modif)
+        assert np.allclose(model.rho[idx2], rho_modif)
+        # Assert init values
+        assert np.allclose(model.x_right[~idx], x_right_init)
+        assert np.allclose(model.x_left[~idx], -x_right_init)
+        assert np.allclose(model.y_top, y_top_init)
+        assert np.allclose(model.y_bottom, -y_top_init)
+        assert np.all(model.shape[~idx2] == shape_init)
+        assert np.allclose(model.rho[~idx2], rho_init)
+        
+    # Create tapered transition between two positions with different aperture values
+    def test_taper_nonsym_transition(self, generate_pm):
+        x_right_init = 0.01
+        y_top_init = 0.02
+        shape_init = 'rect'
+        rho_init = 1e-7
+        model = generate_pm(x_right=x_right_init, 
+                            y_top=y_top_init,
+                            shape=shape_init,
+                            rho=rho_init)
+        x_right_start = 0.01
+        x_right_end = 0.025
+        shape_modif = 'elli'
+        rho_modif = 1e-6
+        start_position = 10
+        end_position = 20
+        model.taper(start_position, 
+                    end_position, 
+                    x_right_start=x_right_start, 
+                    x_right_end=x_right_end,
+                    shape=shape_modif,
+                    rho=rho_modif,
+                    sym=False)
+        idx = (model.position > start_position) & (model.position < end_position)
+        idx2 = ((model.position[:-1] > start_position) &
+                (model.position[1:] < end_position))
+        
+        # Assert edge points
+        assert model.x_right[idx][0] == pytest.approx(x_right_start)
+        assert model.x_right[idx][-1] == pytest.approx(x_right_end)
+        # Assert modif
+        assert np.all(model.x_right[idx][1:] > x_right_start)
+        assert np.all(model.x_right[idx][:-1] < x_right_end)
+        assert np.all(model.shape[idx2] == shape_modif)
+        assert np.allclose(model.rho[idx2], rho_modif)
+        # Assert init values
+        assert np.allclose(model.x_right[~idx], x_right_init)
+        assert np.allclose(model.x_left, -x_right_init)
+        assert np.allclose(model.y_top, y_top_init)
+        assert np.allclose(model.y_bottom, -y_top_init)
+        assert np.all(model.shape[~idx2] == shape_init)
+        assert np.allclose(model.rho[~idx2], rho_init)
+
+    # Calculate effective radius for resistive wall with default right/top settings
+    def test_resistive_wall_effective_radius_default(self, generate_pm, ring_with_at_lattice):
+        model = generate_pm()
+        out = model.resistive_wall_effective_radius(ring_with_at_lattice.optics)
+        for val in out:
+            assert val > 0
+
+    # Plot aperture values and verify figure/axes objects are returned
+    def test_plot_aperture_returns(self, generate_pm):
+        model = generate_pm()
+        fig, axs = model.plot_aperture()
+        assert fig is not None
+        assert axs is not None
+        plt.close(fig)
+
+    # Interpolate aperture values at arbitrary position using get_aperture()
+    def test_get_aperture_interpolation(self, generate_pm):
+        model = generate_pm()
+        aperture = model.get_aperture(15.5)
+        assert len(aperture) == 4
+        assert all(isinstance(x, float) for x in aperture)
+
+    # Handle zero resistivity sections in resistive_wall_effective_radius calculations
+    def test_zero_resistivity_handling(self, generate_pm, ring_with_at_lattice):
+        model = generate_pm()
+        model.change_values(10, 20, rho=0)
+        out = model.resistive_wall_effective_radius(ring_with_at_lattice.optics)
+        out[0] < ring_with_at_lattice.L
+        for val in out:
+            assert val > 0
+
+    # Handle start_position > end_position in change_values and taper methods
+    def test_invalid_position_order(self, generate_pm):
+        model = generate_pm(x_right=0.01)
+        with pytest.raises(ValueError):
+            model.change_values(20, 10, x_right=0.02)
+        with pytest.raises(ValueError):
+            model.taper(20, 10, x_right_start=0.01, x_right_end=0.03)
+
+    # Handle positions outside ring length range
+    def test_positions_outside_range(self, generate_pm):
+        model = generate_pm(x_right=0.01)
+        with pytest.raises(ValueError):
+            model.change_values(-10, 10, x_right=0.02)
+        with pytest.raises(ValueError):
+            model.change_values(0, model.position[-1] + 10, x_right=0.02)
+        with pytest.raises(ValueError):
+            model.taper(-10, 10, x_right_start=0.01, x_right_end=0.03)
+        with pytest.raises(ValueError):
+            model.taper(0, model.position[-1] + 10, x_right_start=0.01, x_right_end=0.03)
\ No newline at end of file
diff --git a/tests/utility_test_functions.py b/tests/utility_test_functions.py
new file mode 100644
index 0000000..1be6045
--- /dev/null
+++ b/tests/utility_test_functions.py
@@ -0,0 +1,63 @@
+import numpy as np
+from mbtrack2 import Bunch, Beam
+
+def assert_attr_changed(element, 
+                        bunch, 
+                        attrs_changed=["xp", "yp", "delta"],
+                        change=True):
+    
+    if isinstance(bunch, Bunch):
+        assert_attr_changed_bunch(element, bunch, attrs_changed, change=change)
+    elif isinstance(bunch, Beam):
+        assert_attr_changed_beam(element, bunch, attrs_changed, change=change)
+    else:
+        raise TypeError
+
+def assert_attr_changed_bunch(element, 
+                              bunch, 
+                              attrs_changed=["xp", "yp", "delta"],
+                              change=True):
+    
+    attrs = ["x","xp","y","yp","tau","delta"]
+    attrs_unchanged = [attr for attr in attrs if attr not in attrs_changed]
+    
+    initial_values_changed = {attr: bunch[attr].copy() for attr in attrs_changed}
+    
+    initial_values_unchanged = {attr: bunch[attr].copy() for attr in attrs_unchanged}
+    
+    element.track(bunch)
+
+    for attr in attrs_changed:
+        if change:
+            assert not np.array_equal(initial_values_changed[attr], bunch[attr]), f"{attr}"
+        else:
+            assert np.array_equal(initial_values_changed[attr], bunch[attr]), f"{attr}"
+        
+    for attr in attrs_unchanged:
+        assert np.array_equal(initial_values_unchanged[attr], bunch[attr]), f"{attr}"
+        
+def assert_attr_changed_beam(element, 
+                             beam,
+                             attrs_changed=["xp", "yp", "delta"],
+                             change=True):
+    
+    attrs = ["x","xp","y","yp","tau","delta"]
+    attrs_unchanged = [attr for attr in attrs if attr not in attrs_changed]
+    
+    initial_values_changed_b = [{attr: bunch[attr].copy() for attr in attrs_changed} for bunch in beam]
+    
+    initial_values_unchanged_b = [{attr: bunch[attr].copy() for attr in attrs_unchanged} for bunch in beam]
+    
+    element.track(beam)
+    
+    for i, bunch in enumerate(beam):
+        initial_values_changed = initial_values_changed_b[i]
+        initial_values_unchanged = initial_values_unchanged_b[i]
+        for attr in attrs_changed:
+            if change and (bunch.charge != 0):
+                assert not np.array_equal(initial_values_changed[attr], bunch[attr]), f"{attr}"
+            else:
+                assert np.array_equal(initial_values_changed[attr], bunch[attr]), f"{attr}"
+            
+        for attr in attrs_unchanged:
+            assert np.array_equal(initial_values_unchanged[attr], bunch[attr]), f"{attr}"
\ No newline at end of file
-- 
GitLab