Repository: Cosmoglobe/zodipy (language: python)

Issue:
Replace SciPy's `interp1d` with new intended interpolation API
[`scipy.interpolate.interp1d`](https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html#scipy.interpolate.interp1d) is now considered legacy code and should be switched out with the interpolation API.
[`CubicSpline`](https://docs.scipy.org/doc/scipy/tutorial/interpolate/1D.html#tutorial-interpolate-1dsection) is probabily the way to go.
Use `synphot` package to perform bandpass integration
from https://github.com/pyOpenSci/software-submission/issues/161:

Instead of manually performing bandpass integrations with the custom `Bandpass` type, we should use the Astropy affiliated [`synphot`](https://synphot.readthedocs.io/en/latest/) package. This would also provide better integration with Astropy.

Move bandpass evaluation and construction
This will does not need to be recomputed every time the model is evaluated. This would make it necessary initialize several models if one wishes to evaluate over several bandpasses. Perhaps it could be cached instead?
Add support for specifying an `obstime` and `obspos` per coordinate.
This pull request adds support for coordinate specific `obstime` and `obspos` values. For time-ordered zodiacal light simulations, ZodiPy will now draw correct line-of-sights for each observations, and the calls to the `evaluate` method will no longer have to be chunked into fine < ~1h chunks.
Fix bug with model registry types
The model registry claims to return a ZodiacalLightModel, but actually returns a Zodipy model.


Discussion / hints:

 I noticed that `synphot` is only available for Python >= 3.10, so I will put this off until ZodiPy drop 3.9 unless there is a work around @pllim? Is it correct that according to https://numpy.org/neps/nep-0029-deprecation_policy.html astropy-affiliated packages should drop python 3.9 already on Apr 05 this year?
Hello and I apologize for any inconvenience. Yes, synphot dropped support for Python 3.9 but only recently via https://github.com/spacetelescope/synphot_refactor/pull/387 . You should still be able to get `synphot < 1.4` in Python 3.9 .
Would be happy to discuss more if you are interested. Integration is nothing too fancy. `synphot` uses trapezoid integration under the curve provided by scipy (used to be `np.trapz` in older versions).  See https://synphot.readthedocs.io/en/latest/api/synphot.spectrum.BaseSpectrum.html#synphot.spectrum.BaseSpectrum.integrate (analytical was added much later and defers to `astropy.modeling` but not widely used).
Thanks it should work with `synphot < 1.4`.

Yeah that would be nice! I want to use `synphot` to compute a table of bandpass integrated blackbody emission values where the blackbodies have varying temperature. It should work for either a center frequency or an empirical bandpass provided by the user as two 1D arrays corresponding to weights and frequencies.  See `get_bandpass_interpolation_table` in https://github.com/Cosmoglobe/zodipy/blob/main/zodipy/_bandpass.py. 
Not quite sure how I would do this using these models but `synphot.models.Empirical1d` or `synphot.spectrum.BaseSpectrum` seem relevant? I also see that there is a `synphot.models.Blackbody1D` which means that I probably don't need to use any custom `b_nu` function in ZodiPy either?
Generally speaking, this would give you a blackbody spectrum (before passing through the bandpass):

```python
from astropy import units as u
from synphot import SourceSpectrum
from synphot.models import BlackBodyNorm1D

bb_temp = 5777 * u.K
bb = SourceSpectrum(BlackBodyNorm1D, temperature=bb_temp)

# Sampling frequencies. This samples most of the flux but feel free to extend as needed.
freq = bb.waveset.to(u.Hz, u.spectral())

# Sampled flux
flux_unit = u.MJy
flux = bb(freq, flux_unit=flux_unit)
```

But to put it through bandpass first before sampling, you have to construct an `Observation`, as documented in https://synphot.readthedocs.io/en/latest/synphot/observation.html .

Often times, you might also want to renormalize the source spectrum first to some standard you want *before* passing it through the bandpass using https://synphot.readthedocs.io/en/latest/api/synphot.spectrum.BaseSourceSpectrum.html#synphot.spectrum.BaseSourceSpectrum.normalize .

So a more complete example would be more like this:

```python
from astropy import units as u
from synphot import SourceSpectrum, SpectralElement, Observation, units as syn_units
from synphot.models import BlackBodyNorm1D, Box1D

bb_temp = 5777 * u.K
bb = SourceSpectrum(BlackBodyNorm1D, temperature=bb_temp)

# Say, you know it is 17 VEGAMAG in Johnson V filter
vega = SourceSpectrum.from_vega()  # For unit conversion  
johnson_v = SpectralElement.from_filter('johnson_v')  
bb_norm = bb.normalize(17 * syn_units.VEGAMAG, johnson_v, vegaspec=vega)

# Now pass the normalized blackbody into your bandpass. Using box here but there are many available.
bandpass = SpectralElement(Box1D, amplitude=1, x_0=5000 * u.AA, width=10 * u.AA)
obs = Observation(bb_norm, bandpass)

# https://synphot.readthedocs.io/en/latest/synphot/formulae.html#synphot-formula-effstim
effstim = obs.effstim(flux_unit=u.MJy)
```

When it comes to an Observation, usually people want the effective stimulus instead of integration under the curve.

Hope this helps!




Fix patch:
diff --git a/.github/workflows/draft-pdf.yml b/.github/workflows/draft-pdf.yml
index e4c5c62..d3f5980 100644
--- a/.github/workflows/draft-pdf.yml
+++ b/.github/workflows/draft-pdf.yml
@@ -1,4 +1,9 @@
-on: [push, workflow_dispatch]
+on:
+  push:
+    paths: ["paper/**"]
+  pull_request:
+    paths: ["paper/**"]
+  workflow_dispatch:
 
 jobs:
   paper:
@@ -20,4 +25,4 @@ jobs:
           # This is the output path where Pandoc will write the compiled
           # PDF. Note, this should be the same directory as the input
           # paper.md
-          path: paper/paper.pdf
\ No newline at end of file
+          path: paper/paper.pdf
diff --git a/.gitignore b/.gitignore
index 9eb1fee..b41ee6f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -173,4 +173,7 @@ settings.json
 test.py
 context.py
 
-*.pdf
\ No newline at end of file
+*.pdf
+
+work_in_progress/
+old_tests/
\ No newline at end of file
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 14e02c9..f7c68dc 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -2,17 +2,44 @@ repos:
   - repo: https://github.com/pre-commit/pre-commit-hooks
     rev: v4.3.0
     hooks:
-    - id: check-added-large-files
-    - id: check-yaml
-      langauge_version: python3
+      - id: check-added-large-files
+      - id: check-yaml
+        langauge_version: python3
 
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    # Ruff version.
     rev: v0.3.7
     hooks:
       - id: ruff
       - id: ruff-format
 
-hooks:
-  - id: poetry-export
-    args: ["--dev", "-f", "requirements.txt", "-o", "requirements-dev.txt"]
\ No newline at end of file
+  - repo: https://github.com/pre-commit/mirrors-mypy
+    rev: v1.10.0
+    hooks:
+      - id: mypy
+        exclude: "(tests|docs)/.*"
+        additional_dependencies: [numpy>=1.21.0]
+
+  - repo: local
+    hooks:
+      - id: poetry-check
+        name: poetry-check
+        description: run poetry check to validate config
+        entry: poetry check
+        language: python
+        pass_filenames: false
+        files: ^(.*/)?pyproject\.toml$
+      - id: poetry-lock
+        name: poetry-lock
+        description: run poetry lock to update lock file
+        entry: poetry lock
+        language: python
+        pass_filenames: false
+        files: ^(.*/)?(poetry\.lock|pyproject\.toml)$
+      - id: poetry-export
+        name: poetry-export
+        description: run poetry export to sync lock file with requirements-dev.txt
+        entry: poetry export
+        language: python
+        pass_filenames: false
+        files: ^(.*/)?poetry\.lock$
+        args: ["-f", "requirements.txt", "-o", "requirements.txt"]
\ No newline at end of file
diff --git a/README.md b/README.md
index 02f262a..61495d0 100644
--- a/README.md
+++ b/README.md
@@ -16,33 +16,38 @@
 ---
 
 
+
 ZodiPy is an [Astropy affiliated](https://www.astropy.org/affiliated/#affiliated-package-list) package for simulating zodiacal light in intensity for arbitrary Solar system observers.
 
 ![plot](docs/img/zodipy_map.png)
 
+
 ## Documentation
-See the [documentation](https://cosmoglobe.github.io/zodipy/) for more information and examples on how to use ZodiPy for different applications.
+See the [documentation](https://cosmoglobe.github.io/zodipy/) for a list of supported zodiacal light models and examples of how to use ZodiPy.
 
 ## A simple example
 ```python
 import astropy.units as u
+from astropy.coordinates import SkyCoord
 from astropy.time import Time
 
-from zodipy import Zodipy
+import zodipy
 
+# Initialize a zodiacal light model at a wavelength/frequency or over a bandpass
+model = zodipy.Model(25*u.micron)
 
-model = Zodipy(model="dirbe")
+# Use Astropy's `SkyCoord` to specify coordinate
+lon = [10, 10.1, 10.2] * u.deg
+lat = [90, 89, 88] * u.deg
+obstimes = Time(["2022-01-01 12:00:00", "2022-01-01 12:01:00", "2022-01-01 12:02:00"])
 
-emission = model.get_emission_ang(
-    25 * u.micron,
-    theta=[10, 10.1, 10.2] * u.deg,
-    phi=[90, 89, 88] * u.deg,
-    obs_time=Time("2022-01-01 12:00:00"),
-    obs="earth",
-)
+skycoord = SkyCoord(lon, lat, obstime=obstimes, frame="galactic")
+
+# Compute the zodiacal light as seen from Earth
+emission = model.evaluate(skycoord, obspos="earth")
 
 print(emission)
-#> [15.35392831 15.35495051 15.35616009] MJy / sr
+#> [27.52410841 27.66572294 27.81251906] MJy / sr
 ```
 
 ## Related scientific papers
@@ -52,63 +57,55 @@ See [CITATION](https://github.com/Cosmoglobe/zodipy/blob/main/CITATION.bib)
 
 
 ## Install
-ZodiPy is installed using `pip install zodipy`.
+ZodiPy is installed with pip
+```bash
+pip install zodipy
+```
 
 ## Dependencies
 ZodiPy supports all Python versions >= 3.9, and has the following dependencies:
 - [Astropy](https://www.astropy.org/) (>=5.0.1)
 - [NumPy](https://numpy.org/)
-- [healpy](https://healpy.readthedocs.io/en/latest/)
 - [jplephem](https://pypi.org/project/jplephem/)
 - [SciPy](https://scipy.org/)
 
 ## For developers
-Contributing developers will need to download the following additional dependencies to test, lint, format and build documentation locally:
-- pytest
-- pytest-cov
-- hypothesis
-- coverage
-- ruff
-- mypy
-- pre-commit
-- mkdocs
-- pymdown-extensions
-- markdown-include
-- mkdocs-material
-- mkdocstrings
-- mkdocstrings-python
-- markdown (<3.4.0)
-
-which are required to test and build ZodiPy.
-
 ### Poetry
-Developers can install ZodiPy through [Poetry](https://python-poetry.org/) (Poetry >= 1.8.0) by first cloning or forking the repository, and then running 
-```
-poetry install
-```
-in a virtual environment from the repository root. This will read the `pyproject.toml` file in the repository and install all dependencies. 
+ZodiPy uses [Poetry](https://python-poetry.org/) for development. To build and commit to the repository with the existing pre-commit setup, developers need to have Poetry (>= 1.8.0) installed. See the Poetry [documentation](https://python-poetry.org/docs/) for installation guide. 
 
-### pip
-Developers not using Poetry can install ZodiPy in a virtual environment with all dependencies by first cloning or forking the repository and then running 
+After poetry has been installed, developers should create a new virtual environment and run the following in the root of the ZodiPy repositry
 ```
-pip install -r requirements-dev.txt
+poetry install
 ```
-from the repositry root. This will read and download all the dependencies from the `requirements-dev.txt` file in the repository. 
+This will download all dependencies (including dev)from `pyproject.toml`, and `poetry.lock`.
 
-Note that developers using Python 3.12 will need to upgrade their pip versions with `python3 -m pip install --upgrade pip` before being able to install ZodiPy. This is due to known incompatibilities between older pip versions and Python 3.12
-
-### Tests, linting and formatting
+### Tests, linting and formatting, and building documentation
 The following tools should be run from the root of the repository with no errors. (These are ran automatically as part of the CI workflows on GitHub, but should be tested locally first)
 
-- [pytest](https://docs.pytest.org/en/8.0.x/): Tests are run with pytest by simply running `pytest` in the command line in the root of the repository. 
-- [ruff](https://github.com/astral-sh/ruff): Formating and linting is done with `ruff` by simply running `ruff check` and `ruff format` in the command line in the root of the repository. 
-- [mypy](https://mypy-lang.org/): Type checking is done with `mypy` by simply running `mypy zodipy/` in the root of the repository.
-
+#### pytest
+Testing is done with [pytest](https://docs.pytest.org/en/8.0.x/). To run the tests, run the following command from the repository root
+```bash
+pytest
+``` 
+#### ruff
+Formating and linting is done with [ruff](https://github.com/astral-sh/ruff). To format and lint, run the following command from the repository root
+```bash
+ruff check
+ruff format
+``` 
+#### mypy
+ZodiPy is fully typed. We use [mypy](https://mypy-lang.org/) as a static type checker. To type check, run the following command from the repositry root
+
+```bash
+mypy zodipy/
+```
 Remeber to add tests when implementing new features to maintain a high code coverage.
 
-### Documentation
-We use [MkDocs](https://www.mkdocs.org/) to create our documentation. The documentation is built locally with `mkdocs build` from the repository root, and served with `mkdocs serve`.
-
+#### MkDocs
+We use [MkDocs](https://www.mkdocs.org/) to create our documentation. To serve the docs locally on you machine, run the following from the repositry root
+```bash
+mkdocs serve
+```
 
 ## Funding
 This work has received funding from the European Union's Horizon 2020 research and innovation programme under grant agreements No 776282 (COMPET-4; BeyondPlanck), 772253 (ERC; bits2cosmology) and 819478 (ERC; Cosmoglobe).
diff --git a/docs/examples/astropy_healpix_map.py b/docs/examples/astropy_healpix_map.py
new file mode 100644
index 0000000..5c282c3
--- /dev/null
+++ b/docs/examples/astropy_healpix_map.py
@@ -0,0 +1,33 @@
+import multiprocessing
+
+import astropy.units as u
+import astropy_healpix as ahp
+import healpy as hp
+import matplotlib.pyplot as plt
+import numpy as np
+from astropy.time import Time
+
+import zodipy
+
+model = zodipy.Model(30 * u.micron)
+
+healpix = ahp.HEALPix(nside=256, frame="galactic")
+pixels = np.arange(healpix.npix)
+skycoord = healpix.healpix_to_skycoord(pixels)
+
+# Note that we manually set the obstime attribute
+skycoord.obstime = Time("2022-01-14")
+
+emission = model.evaluate(skycoord, nprocesses=multiprocessing.cpu_count())
+
+# Plot with healpy
+hp.mollview(
+    emission,
+    unit="MJy/sr",
+    cmap="afmhot",
+    min=0,
+    max=80,
+    title="Zodiacal light at 30 µm (2022-01-14)",
+)
+# plt.savefig("../img/healpix_map.png", dpi=300, bbox_inches="tight")
+plt.show()
diff --git a/docs/examples/component_maps.py b/docs/examples/component_maps.py
new file mode 100644
index 0000000..07e81d2
--- /dev/null
+++ b/docs/examples/component_maps.py
@@ -0,0 +1,49 @@
+import multiprocessing
+
+import astropy.units as u
+import healpy as hp
+import matplotlib.pyplot as plt
+import numpy as np
+from astropy.coordinates import SkyCoord
+from astropy.time import Time
+
+import zodipy
+
+COMP_NAMES = [
+    "Smooth cloud",
+    "Dust band 1",
+    "Dust band 2",
+    "Dust band 3",
+    "Circum-solar Ring",
+    "Earth-trailing Feature",
+]
+
+model = zodipy.Model(24 * u.micron)
+
+nside = 128
+pixels = np.arange(hp.nside2npix(nside))
+lon, lat = hp.pix2ang(nside, pixels, lonlat=True)
+
+skycoord = SkyCoord(
+    lon,
+    lat,
+    unit=u.deg,
+    frame="barycentricmeanecliptic",
+    obstime=Time("2022-01-14"),
+)
+
+emission = model.evaluate(skycoord, return_comps=True, nprocesses=multiprocessing.cpu_count())
+
+fig = plt.figure(figsize=(8, 7))
+for idx, comp_emission in enumerate(emission):
+    hp.mollview(
+        comp_emission,
+        title=COMP_NAMES[idx],
+        norm="log" if idx == 0 else None,
+        cmap="afmhot",
+        cbar=False,
+        sub=(3, 2, idx + 1),
+        fig=fig,
+    )
+plt.savefig("../img/component_maps.png", dpi=250, bbox_inches="tight")
+plt.show()
diff --git a/docs/examples/ecliptic_scan.py b/docs/examples/ecliptic_scan.py
new file mode 100644
index 0000000..6b29eb4
--- /dev/null
+++ b/docs/examples/ecliptic_scan.py
@@ -0,0 +1,28 @@
+import astropy.units as u
+import matplotlib.pyplot as plt
+import numpy as np
+from astropy.coordinates import BarycentricMeanEcliptic, SkyCoord
+from astropy.time import Time
+
+from zodipy import Model
+
+model = Model(30 * u.micron)
+
+# Longitude and Latitude values corresponding to a scan through the eclitpic plane
+lats = np.linspace(-90, 90, 100) * u.deg
+lons = np.zeros_like(lats)
+
+coords = SkyCoord(
+    lons,
+    lats,
+    frame=BarycentricMeanEcliptic,
+    obstime=Time("2022-06-14"),
+)
+
+emission = model.evaluate(coords)
+
+plt.plot(lats, emission)
+plt.xlabel("Latitude [deg]")
+plt.ylabel("Emission [MJy/sr]")
+plt.savefig("../img/ecliptic_scan.png", dpi=300, bbox_inches="tight")
+plt.show()
diff --git a/docs/examples/get_bandpass_integrated_emission.py b/docs/examples/get_bandpass_integrated_emission.py
deleted file mode 100644
index 132c3c6..0000000
--- a/docs/examples/get_bandpass_integrated_emission.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import astropy.units as u
-import healpy as hp
-import matplotlib.pyplot as plt
-import numpy as np
-from astropy.time import Time
-
-from zodipy import Zodipy
-
-nside = 128
-
-center_freq = 800 * u.GHz
-freqs = np.linspace(750, 850, 11) * u.GHz
-weights = np.array([2, 3, 5, 9, 11, 11.5, 11, 9, 5, 3, 2])
-
-plt.plot(freqs, weights)
-plt.xlabel("Frequency [GHz]")
-plt.ylabel("Weights")
-plt.savefig("../img/bandpass.png", dpi=300)
-
-model = Zodipy(model="planck18")
-
-emission_central_freq = model.get_binned_emission_pix(
-    freq=center_freq,
-    pixels=np.arange(hp.nside2npix(nside)),
-    nside=nside,
-    obs_time=Time("2022-03-10"),
-    obs="SEMB-L2",
-)
-
-emission_bandpass_integrated = model.get_binned_emission_pix(
-    freq=freqs,
-    weights=weights,
-    pixels=np.arange(hp.nside2npix(nside)),
-    nside=nside,
-    obs_time=Time("2022-03-10"),
-    obs="SEMB-L2",
-)
-
-hp.mollview(
-    emission_central_freq,
-    title="Center frequency",
-    unit="MJy/sr",
-    cmap="afmhot",
-    norm="log",
-)
-plt.savefig("../img/center_freq.png", dpi=300)
-
-hp.mollview(
-    emission_bandpass_integrated,
-    title="Bandpass integrated",
-    unit="MJy/sr",
-    cmap="afmhot",
-    norm="log",
-)
-plt.savefig("../img/bandpass_integrated.png", dpi=300)
-plt.show()
diff --git a/docs/examples/get_binned_emission.py b/docs/examples/get_binned_emission.py
deleted file mode 100644
index ace5eea..0000000
--- a/docs/examples/get_binned_emission.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import astropy.units as u
-import healpy as hp
-import matplotlib.pyplot as plt
-import numpy as np
-from astropy.time import Time
-
-from zodipy import Zodipy
-
-model = Zodipy("planck18")
-nside = 256
-
-binned_emission = model.get_binned_emission_pix(
-    857 * u.GHz,
-    pixels=np.arange(hp.nside2npix(nside)),
-    nside=nside,
-    obs_time=Time("2022-06-14"),
-    obs="earth",
-)
-
-hp.mollview(
-    binned_emission,
-    title="Binned zodiacal emission at 857 GHz",
-    unit="MJy/sr",
-    min=0,
-    max=1,
-    cmap="afmhot",
-)
-plt.savefig("../img/binned.png", dpi=300)
-plt.show()
diff --git a/docs/examples/get_binned_emission_solar_cutoff.py b/docs/examples/get_binned_emission_solar_cutoff.py
deleted file mode 100644
index c1fa9ca..0000000
--- a/docs/examples/get_binned_emission_solar_cutoff.py
+++ /dev/null
@@ -1,29 +0,0 @@
-import astropy.units as u
-import healpy as hp
-import matplotlib.pyplot as plt
-import numpy as np
-from astropy.time import Time
-
-from zodipy import Zodipy
-
-model = Zodipy("dirbe", solar_cut=60 * u.deg)
-nside = 256
-
-binned_emission = model.get_binned_emission_pix(
-    25 * u.micron,
-    pixels=np.arange(hp.nside2npix(nside)),
-    nside=nside,
-    obs_time=Time("2020-01-01"),
-    obs="earth",
-)
-
-hp.mollview(
-    binned_emission,
-    title="Solar cutoff at 60 degrees",
-    unit="MJy/sr",
-    max=80,
-    coord="E",
-    cmap="afmhot",
-)
-plt.savefig("../img/binned_solar_cutoff.png", dpi=300)
-plt.show()
diff --git a/docs/examples/get_binned_gal_emission.py b/docs/examples/get_binned_gal_emission.py
deleted file mode 100644
index f7f8f6b..0000000
--- a/docs/examples/get_binned_gal_emission.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import astropy.units as u
-import healpy as hp
-import matplotlib.pyplot as plt
-import numpy as np
-from astropy.time import Time
-
-from zodipy import Zodipy
-
-model = Zodipy("planck18")
-nside = 256
-
-binned_emission = model.get_binned_emission_pix(
-    857 * u.GHz,
-    pixels=np.arange(hp.nside2npix(nside)),
-    nside=nside,
-    obs_time=Time("2022-02-20"),
-    obs="earth",
-    coord_in="G",  # Coordinates of the input pointing
-)
-
-hp.mollview(
-    binned_emission,
-    title="Binned zodiacal emission at 857 GHz",
-    unit="MJy/sr",
-    cmap="afmhot",
-    min=0,
-    max=1,
-)
-plt.savefig("../img/binned_gal.png", dpi=300)
-plt.show()
diff --git a/docs/examples/get_comp_binned_emission.py b/docs/examples/get_comp_binned_emission.py
deleted file mode 100644
index 1e89be3..0000000
--- a/docs/examples/get_comp_binned_emission.py
+++ /dev/null
@@ -1,33 +0,0 @@
-import astropy.units as u
-import healpy as hp
-import matplotlib.pyplot as plt
-import numpy as np
-from astropy.time import Time
-
-from zodipy import Zodipy
-
-model = Zodipy("dirbe")
-nside = 256
-
-binned_emission = model.get_binned_emission_pix(
-    25 * u.micron,
-    pixels=np.arange(hp.nside2npix(nside)),
-    nside=nside,
-    obs_time=Time("2022-01-01"),
-    obs="earth",
-    return_comps=True,
-)
-fig = plt.figure(figsize=(8, 6.5), constrained_layout=True)
-comps = ["Cloud", "Band1", "Band2", "Band3", "Ring", "Feature"]
-for idx, binned_comp_emission in enumerate(binned_emission):
-    hp.mollview(
-        binned_comp_emission,
-        title=comps[idx],
-        norm="log" if idx == 0 else None,
-        cmap="afmhot",
-        cbar=False,
-        sub=(3, 2, idx + 1),
-        fig=fig,
-    )
-# plt.savefig("../img/binned_comp.png", dpi=300)
-plt.show()
diff --git a/docs/examples/get_density_contour.py b/docs/examples/get_density_contour.py
deleted file mode 100644
index 74fdc48..0000000
--- a/docs/examples/get_density_contour.py
+++ /dev/null
@@ -1,30 +0,0 @@
-import matplotlib.pyplot as plt
-import numpy as np
-from matplotlib.colors import LogNorm
-
-from zodipy import tabulate_density
-
-N = 200
-
-x = np.linspace(-5, 5, N)  # x-plane
-y = np.linspace(-5, 5, N)  # y-plane
-z = np.linspace(-2, 2, N)  # z-plane
-
-grid = np.asarray(np.meshgrid(x, y, z))
-density_grid = tabulate_density(grid, model="dirbe")
-density_grid = density_grid.sum(axis=0)  # Sum over all components
-
-plt.pcolormesh(
-    x,
-    y,
-    density_grid[N // 2].T,  # cross section in the yz-plane
-    cmap="afmhot",
-    norm=LogNorm(vmin=density_grid.min(), vmax=density_grid.max()),
-    shading="gouraud",
-    rasterized=True,
-)
-plt.title("Cross section of the interplanetary dust density (yz-plane)")
-plt.xlabel("x [AU]")
-plt.ylabel("z [AU]")
-# plt.savefig("../img/density_grid.png", dpi=300)
-plt.show()
diff --git a/docs/examples/get_emission_ang.py b/docs/examples/get_emission_ang.py
deleted file mode 100644
index 146271a..0000000
--- a/docs/examples/get_emission_ang.py
+++ /dev/null
@@ -1,27 +0,0 @@
-import astropy.units as u
-import matplotlib.pyplot as plt
-import numpy as np
-from astropy.time import Time
-
-from zodipy import Zodipy
-
-model = Zodipy("dirbe")
-
-latitudes = np.linspace(-90, 90, 10000) * u.deg
-longitudes = np.zeros_like(latitudes)
-
-emission = model.get_emission_ang(
-    30 * u.micron,
-    theta=longitudes,
-    phi=latitudes,
-    lonlat=True,
-    obs_time=Time("2022-06-14"),
-    obs="earth",
-)
-
-
-plt.plot(latitudes, emission)
-plt.xlabel("Latitude [deg]")
-plt.ylabel("Emission [MJy/sr]")
-plt.savefig("../img/timestream.png", dpi=300)
-plt.show()
diff --git a/docs/examples/get_parallel_emission.py b/docs/examples/get_parallel_emission.py
deleted file mode 100644
index e5be445..0000000
--- a/docs/examples/get_parallel_emission.py
+++ /dev/null
@@ -1,42 +0,0 @@
-import time
-
-import astropy.units as u
-import healpy as hp
-import numpy as np
-from astropy.time import Time
-
-from zodipy import Zodipy
-
-nside = 256
-pixels = np.arange(hp.nside2npix(nside))
-obs_time = Time("2020-01-01")
-n_proc = 8
-
-model = Zodipy()
-model_parallel = Zodipy(n_proc=n_proc)
-
-start = time.perf_counter()
-emission = model.get_binned_emission_pix(
-    40 * u.micron,
-    pixels=pixels,
-    nside=nside,
-    obs_time=obs_time,
-)
-print("Time spent on a single CPU:", round(time.perf_counter() - start, 2), "seconds")
-# > Time spent on a single CPU: 35.23 seconds
-
-start = time.perf_counter()
-emission_parallel = model_parallel.get_binned_emission_pix(
-    40 * u.micron,
-    pixels=pixels,
-    nside=nside,
-    obs_time=obs_time,
-)
-print(
-    f"Time spent on {n_proc} CPUs:",
-    round(time.perf_counter() - start, 2),
-    "seconds",
-)
-# > Time spent on 8 CPUs: 12.85 seconds
-
-assert np.allclose(emission, emission_parallel)
diff --git a/docs/examples/healpy_map.py b/docs/examples/healpy_map.py
new file mode 100644
index 0000000..5227cae
--- /dev/null
+++ b/docs/examples/healpy_map.py
@@ -0,0 +1,31 @@
+import multiprocessing
+
+import astropy.units as u
+import healpy as hp
+import matplotlib.pyplot as plt
+import numpy as np
+from astropy.coordinates import SkyCoord
+from astropy.time import Time
+
+import zodipy
+
+model = zodipy.Model(30 * u.micron)
+
+nside = 256
+pixels = np.arange(hp.nside2npix(nside))
+lon, lat = hp.pix2ang(nside, pixels, lonlat=True)
+
+skycoord = SkyCoord(lon, lat, unit=u.deg, frame="galactic", obstime=Time("2022-01-14"))
+
+emission = model.evaluate(skycoord, nprocesses=multiprocessing.cpu_count())
+
+hp.mollview(
+    emission,
+    unit="MJy/sr",
+    cmap="afmhot",
+    min=0,
+    max=80,
+    title="Zodiacal light at 30 µm (2022-01-14)",
+)
+plt.savefig("../img/healpix_map.png", dpi=300, bbox_inches="tight")
+plt.show()
diff --git a/docs/examples/number_density.py b/docs/examples/number_density.py
new file mode 100644
index 0000000..c057b2e
--- /dev/null
+++ b/docs/examples/number_density.py
@@ -0,0 +1,37 @@
+import astropy.units as u
+import matplotlib.pyplot as plt
+import numpy as np
+from astropy.time import Time
+from matplotlib.colors import LogNorm
+
+from zodipy import grid_number_density
+
+N = 200
+
+x = np.linspace(-5, 5, N) * u.AU  # x-plane
+y = np.linspace(-5, 5, N) * u.AU  # y-plane
+z = np.linspace(-2, 2, N) * u.AU  # z-plane
+
+density_grid = grid_number_density(
+    x,
+    y,
+    z,
+    obstime=Time("2021-01-01T00:00:00", scale="utc"),
+    name="DIRBE",
+)
+density_grid = density_grid.sum(axis=0)  # Sum over all components
+
+plt.pcolormesh(
+    x,
+    y,
+    density_grid[N // 2].T,  # cross section in the yz-plane
+    cmap="afmhot",
+    norm=LogNorm(vmin=density_grid.min(), vmax=density_grid.max()),
+    shading="gouraud",
+    rasterized=True,
+)
+plt.title("Cross section of the number density in the DIRBE model")
+plt.xlabel("x [AU]")
+plt.ylabel("z [AU]")
+plt.savefig("../img/number_density.png", dpi=300, bbox_inches="tight")
+plt.show()
diff --git a/docs/img/bandpass.png b/docs/img/bandpass.png
deleted file mode 100644
index 865dacc..0000000
Binary files a/docs/img/bandpass.png and /dev/null differ
diff --git a/docs/img/bandpass_integrated.png b/docs/img/bandpass_integrated.png
deleted file mode 100644
index 5b89bbc..0000000
Binary files a/docs/img/bandpass_integrated.png and /dev/null differ
diff --git a/docs/img/binned.png b/docs/img/binned.png
deleted file mode 100644
index b9eca81..0000000
Binary files a/docs/img/binned.png and /dev/null differ
diff --git a/docs/img/binned_comp.png b/docs/img/binned_comp.png
deleted file mode 100644
index 94184a2..0000000
Binary files a/docs/img/binned_comp.png and /dev/null differ
diff --git a/docs/img/binned_gal.png b/docs/img/binned_gal.png
deleted file mode 100644
index 27fed9c..0000000
Binary files a/docs/img/binned_gal.png and /dev/null differ
diff --git a/docs/img/binned_solar_cutoff.png b/docs/img/binned_solar_cutoff.png
deleted file mode 100644
index 8b2cc3c..0000000
Binary files a/docs/img/binned_solar_cutoff.png and /dev/null differ
diff --git a/docs/img/center_freq.png b/docs/img/center_freq.png
deleted file mode 100644
index 6f005cc..0000000
Binary files a/docs/img/center_freq.png and /dev/null differ
diff --git a/docs/img/component_maps.png b/docs/img/component_maps.png
new file mode 100644
index 0000000..b98acf8
Binary files /dev/null and b/docs/img/component_maps.png differ
diff --git a/docs/img/compponent_maps.png b/docs/img/compponent_maps.png
new file mode 100644
index 0000000..75b4a9e
Binary files /dev/null and b/docs/img/compponent_maps.png differ
diff --git a/docs/img/density_grid.png b/docs/img/density_grid.png
deleted file mode 100644
index f9fbad0..0000000
Binary files a/docs/img/density_grid.png and /dev/null differ
diff --git a/docs/img/ecliptic_scan.png b/docs/img/ecliptic_scan.png
new file mode 100644
index 0000000..1ed4112
Binary files /dev/null and b/docs/img/ecliptic_scan.png differ
diff --git a/docs/img/healpix_map.png b/docs/img/healpix_map.png
new file mode 100644
index 0000000..a36a86e
Binary files /dev/null and b/docs/img/healpix_map.png differ
diff --git a/docs/img/number_density.png b/docs/img/number_density.png
new file mode 100644
index 0000000..82aeaba
Binary files /dev/null and b/docs/img/number_density.png differ
diff --git a/docs/img/timestream.png b/docs/img/timestream.png
deleted file mode 100644
index ee81073..0000000
Binary files a/docs/img/timestream.png and /dev/null differ
diff --git a/docs/index.md b/docs/index.md
index 775bd4a..01d130b 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -14,41 +14,32 @@
 [![DOI](https://zenodo.org/badge/394929213.svg)](https://zenodo.org/doi/10.5281/zenodo.10999611)
 [![DOI](https://joss.theoj.org/papers/10.21105/joss.06648/status.svg)](https://doi.org/10.21105/joss.06648)
 
-ZodiPy is an [Astropy affiliated](https://www.astropy.org/affiliated/) package for simulating zodiacal light in intensity for arbitrary Solar system observers.
+ZodiPy is an [Astropy-affiliated](https://www.astropy.org/affiliated/) package for simulating zodiacal light in intensity for arbitrary Solar system observers.
 ![ZodiPy Logo](img/zodipy_map.png)
 
-
 ## A simple example
 ```python
 import astropy.units as u
+from astropy.coordinates import SkyCoord
 from astropy.time import Time
 
-from zodipy import Zodipy
+import zodipy
+
+# Initialize a zodiacal light model at a wavelength/frequency or over a bandpass
+model = zodipy.Model(25*u.micron)
 
+# Use Astropy's `SkyCoord` to specify coordinate
+lon = [10, 10.1, 10.2] * u.deg
+lat = [90, 89, 88] * u.deg
+obstimes = Time(["2022-01-01 12:00:00", "2022-01-01 12:01:00", "2022-01-01 12:02:00"])
 
-model = Zodipy("dirbe")
+skycoord = SkyCoord(lon, lat, obstime=obstimes, frame="galactic")
 
-emission = model.get_emission_ang(
-    25 * u.micron,
-    theta=[10, 10.1, 10.2] * u.deg,
-    phi=[90, 89, 88] * u.deg,
-    obs_time=Time("2022-01-01 12:00:00"),
-    obs="earth",
-    lonlat=True,
-)
+# Compute the zodiacal light as seen from Earth
+emission = model.evaluate(skycoord, obspos="earth")
 
 print(emission)
-#> [15.53095493 15.52883577 15.53121942] MJy / sr
+#> [27.52410841 27.66572294 27.81251906] MJy / sr
 ```
 
-What's going on here:
-
-- We start by initializing the [`Zodipy`][zodipy.zodipy.Zodipy] class, which is our interface, where we specify that we want to use the DIRBE interplanetary dust model.
-- We use the [`get_emission_ang`][zodipy.zodipy.Zodipy.get_emission_ang] method which is a method to simulate emission from angular sky coordinates (see the [reference](reference.md) for other available simulation methods).
-- The first argument to the [`get_emission_ang`][zodipy.zodipy.Zodipy.get_emission_ang] method, `25 * u.micron`, specifies the wavelength of the simulated observation. Note that we use Astropy units for many of the input arguments.
-- `theta` and `phi` represent the pointing of the observation (co-latitude and longitude, following the healpy convention). In this example we observe three sky coordinates.
-- `obs_time` represents the time of observation, which we need to compute the position of the observer and all other required solar system bodies.
-- `obs` represents the observer, and must be an solar system observer supported by the [Astropy ephemeris](https://docs.astropy.org/en/stable/coordinates/solarsystem.html) used internally. If we wish to be more specific about the observer position, we can use the `obs_pos` keyword instead of `obs`, which takes in a heliocentric cartesian position in units of AU.
-- Finally, `lonlat` is a boolean which converts the convention of `theta` and `phi` from co-latitude and longitude to longitude and latitude.
-
 For more information on using ZodiPy, see [the usage section](usage.md).
diff --git a/docs/install.md b/docs/install.md
index 179e612..7c960ec 100644
--- a/docs/install.md
+++ b/docs/install.md
@@ -1,19 +1,18 @@
 # Install
 
+ZodiPy supports python versions >= 3.9.
+
 Installing ZodiPy is as simple as:
 
 ```
 pip install zodipy
 ```
 
-!!! note
-    ZodiPy supports python versions >=3.9.
 
 ## Dependencies
-ZodiPy has the following dependencies (these are automatically downloaded alongside ZodiPy):
+ZodiPy has the following dependencies:
 
 - [Astropy](https://www.astropy.org) (>= 5.0.1)
 - [NumPy](https://numpy.org)
-- [healpy](https://healpy.readthedocs.io/en/latest/)
 - [jplehem](https://pypi.org/project/jplephem/)
 - [SciPy](https://scipy.org/)
diff --git a/docs/introduction.md b/docs/introduction.md
index b69ae36..335820d 100644
--- a/docs/introduction.md
+++ b/docs/introduction.md
@@ -1,31 +1,25 @@
 # Introduction
 
-ZodiPy is an open source Python tool for simulating the zodiacal emission that a solar system observer is predicted to see given an interplanetary dust model. We attempts to make zodiacal emission simulations more accessible by providing the community with a simple interface to existing models. For other zodiacal emission tools, see [Zodiacal Light Models on LAMBDA](https://lambda.gsfc.nasa.gov/product/foreground/fg_models.html). All contributions are most welcome.
+ZodiPy is a Python package for zodiacal light simulations. Its purpose is to provide the astrophysics and cosmology communities with an easy-to-use and accessible interface to existing zodiacal light models in Python, assisting in astrophysical data analysis and zodiacal light forecasting for future experiments.
 
+For other zodiacal light tools, see [Zodiacal Light Models on LAMBDA](https://lambda.gsfc.nasa.gov/product/foreground/fg_models.html).
 
-## Interplanetary Dust Models
-ZodiPy supports the following interplanetary dust models:
+## Supported zodiacal light models
 
-**1.25-240 $\boldsymbol{\mu}$m**
+- DIRBE [`"dirbe"`] ([Kelsall et al. 1998](https://ui.adsabs.harvard.edu/abs/1998ApJ...508...44K/abstract))
+- Rowan-Robinson and May (experimental) [`"rrm-experimental"`] ([Rowan-Robinson and May 2013](https://ui.adsabs.harvard.edu/abs/2013MNRAS.429.2894R/abstract))
+- Planck 2013 [`"planck13"`] ([Planck Collaboration et al. 2014](https://ui.adsabs.harvard.edu/abs/2014A%26A...571A..14P/abstract>))
+- Planck 2015 [`"planck15"`] ([Planck Collaboration et al. 2016](https://ui.adsabs.harvard.edu/abs/2016A&A...594A...8P))
+- Planck 2018 [`"planck18"`] ([Planck Collaboration et al. 2020](https://ui.adsabs.harvard.edu/abs/2020A&A...641A...3P))
+- Odegard [`"odegard"`] ([Odegard et al. 2019](https://ui.adsabs.harvard.edu/abs/2019ApJ...877...40O/abstract))
 
-- DIRBE ([Kelsall et al. 1998](https://ui.adsabs.harvard.edu/abs/1998ApJ...508...44K/abstract))
-- RRM (experimental) ([Rowan-Robinson and May 2013](https://ui.adsabs.harvard.edu/abs/2013MNRAS.429.2894R/abstract))
+The names in the brackets are the string representations used in the `zodipy.Model` object to select the model.
 
-**100-857 GHz**
+If you see a missing model or wish to add a new one, please open an issue on GitHub. Contributors are very welcome!
 
-- Planck 2013 ([Planck Collaboration et al. 2014](https://ui.adsabs.harvard.edu/abs/2014A%26A...571A..14P/abstract>))
-- Planck 2015 ([Planck Collaboration et al. 2016](https://ui.adsabs.harvard.edu/abs/2016A&A...594A...8P))
-- Planck 2018 ([Planck Collaboration et al. 2020](https://ui.adsabs.harvard.edu/abs/2020A&A...641A...3P))
-- Odegard ([Odegard et al. 2019](https://ui.adsabs.harvard.edu/abs/2019ApJ...877...40O/abstract))
 
-!!! info
-    The Planck and Odegard models extend the DIRBE interplanetary dust model to CMB frequencies by fitting the blackbody emissivity of the dust in the respective DIRBE interplanetary dust components to Planck HFI data.
-    The distribution of the interplanetary dust is exactly the same as in the DIRBE model.
+## Related scientific papers
+See [CITATION](https://github.com/Cosmoglobe/zodipy/blob/main/CITATION.bib)
 
-If you see a missing model, please feel free to contact us by opening an issue on GitHub. 
-
-
-## Scientific Paper
-For an overview of the modeling approach used in ZodiPy and other information regarding zodiacal emission and interplanetary dust modeling we refer to the scientific paper on ZodiPy:
-
-- [Cosmoglobe: Simulating zodiacal emission with ZodiPy](https://arxiv.org/abs/2205.12962)
\ No newline at end of file
+- [Cosmoglobe: Simulating zodiacal emission with ZodiPy (San et al. 2022)](https://arxiv.org/abs/2205.12962). 
+- [ZodiPy: A Python package for zodiacal light simulations (San 2024)](https://joss.theoj.org/papers/10.21105/joss.06648#). 
diff --git a/docs/reference.md b/docs/reference.md
index bd046c8..02643d6 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -1,3 +1,3 @@
 # Reference
 
-::: zodipy.zodipy
\ No newline at end of file
+::: zodipy.Model
\ No newline at end of file
diff --git a/docs/usage.md b/docs/usage.md
index 76ccc9e..0fea164 100644
--- a/docs/usage.md
+++ b/docs/usage.md
@@ -1,118 +1,238 @@
-## Timestreams
-Below we illustrate how ZodiPy can be used to create timestreams of the zodiacal emission. 
-Note that since ZodiPy assumes a constant observer position over the input pointing sequence, the output
-will not be *real* timestreams, but for small enough time intervals the error is negligible.
+!!! warning "Breaking API changes in `v1.0.0`"
+    From version `v1.0.0` and out ZodiPy no implements the `get_emission_ang` and `get_emission_pix` methods. See the section on [HEALPix maps](usage.md#healpix-maps) for an example of how to make simulations from HEALPix pixel indices. These are changes following the integration of ZodiPy into the Astropy ecosystem.
 
+As an Astropy-affiliated package, ZodiPy is highly integrated with the astropy ecosystem.
+To make zodiacal light simulations, the `astropy.units`, `astropy.coordinates`, and `astropy.time` modules are used to provide user input. The coordinates for which ZodiPy will simulate the zodiacal light is specified in through the `astropy.coordinates.SkyCoord` object. Using ZodiPy is very simple and the user will only interact with *one* object `zodipy.Model`, which has *one* method `evaluate`.
 
-### Emission along a meridian
-In the following example we simulate what an observer on Earth is expected to see on 14 June, 
-2022 when looking along a meridian (line of constant longitude) at 30 microns, given the 
-DIRBE interplanetary dust model.
+## Initializing Zodipy
+### Working with the `Model` class
+The interface to ZodiPy is the `zodipy.Model` class
+```py hl_lines="2 4"
+import astropy.units as u
+import zodipy
 
-```python
-{!examples/get_emission_ang.py!}
+model = zodipy.Model(25 * u.micron)
 ```
+It has *one* required positional argument `x`, which represents a center wavelength/frequency or the points of an empirical bandpass. If `x` represents the points of a bandpass, the `weights` argument must also be provided
+```py hl_lines="4 5 7"
+import astropy.units as u
+import zodipy
 
-![Zodiacal emission timestream](img/timestream.png)
+points = [3, 4, 5, 6] * u.micron
+weights = [0.2, 0.4, 0.3, 0.1]
 
-!!! note
-    ZodiPy assumes a constant observer position over an input pointing sequence. For an observer on Earth, 
-    the true zodiacal emission signal will move along the ecliptic on the sky by roughly one degree each day. 
-    To account for this effect, the full pointing sequence of an experiment should be chunked into small 
-    subsequences with timescales corresponding to at maximum a day.
+model = zodipy.Model(points, weights=weights)
+```
+
+ZodiPy supports several zodiacal light models (see the [introduction](introduction.md) page for more information regarding the supported models), which are all valid in wavelength/frequency ranges. By default, the `Model` object will initialize using the DIRBE model. To select a different model, we specify the keyword argument `name`
+```py hl_lines="4"
+import astropy.units as u
+import zodipy
 
+model = zodipy.Model(25 * u.micron, name="planck18")
+```
+## Simulating zodiacal light
+To make zodiacal light simulations ZodiPy needs to know three things: 1) Sky coordinates for which to simulate zodiacal light; 2) The position of the observer to know where the vertex of the rays is positioned; and 3) the time of observation, used to query the position of Earth. 
 
-## HEALPix maps
-Below we illustrate how ZodiPy can be used to create simulated binned HEALPix maps of the zodiacal emission.
+### The SkyCoord object
+The sky coordinates for which to simulate zodiacal light is given to ZodiPy using Astropy's `astropy.coordinates.SkyCoord` object. Users unfamiliar with the `SkyCoord` object should visit the [official Astropy documentation](https://docs.astropy.org/en/stable/coordinates/index.html) before using ZodiPy to learn the basics.
 
 
-### Instantaneous map in ecliptic coordinates
-In the following example we make an instantaneous map of of the zodiacal emission at 857 GHz
-as seen by an observer on earth on 14 June, 2022 given the Planck 2018 interplanetary dust model.
+For a single observation in galactic coordinates, the `SkyCoord` object may look something like
+```py hl_lines="2 5 6 7 8 9 10"
+import astropy.units as u
+from astropy.coordinates import SkyCoord
+from astropy.time import Time
 
-```python
-{!examples/get_binned_emission.py!}
+skycoord = SkyCoord(
+    40 * u.deg, 
+    60 * u.deg, 
+    obstime=Time("2020-01-01"), 
+    frame="galactic"
+)
+```
+The `obstime` keyword is mandatory, and is given by an `astropy.time.Time` object, which can represent time in many formats, including Julian and modified Julian dates (see the [Astropy documentation](https://docs.astropy.org/en/stable/time/)).
+Correspondingly for several observations, each with their own `obstime` input:
+```py hl_lines="6 7 8"
+import astropy.units as u
+from astropy.coordinates import SkyCoord
+from astropy.time import Time
+
+skycoord = SkyCoord(
+    [40, 41, 42] * u.deg, 
+    [60, 59, 58] * u.deg, 
+    obstime=Time(["2020-01-01", "2020-01-02", "2020-01-03"]), 
+    frame="galactic"
+)
+```
+If a single value is given for `obstime`, all coordinates are assumed to be viewed instantaneously at that time from a single position in the Solar system. Otherwise, each coordinate must have its own `obstime` value.
+
+The sky coordinates should represent observer-centric coordinates. The observer-position is therefore required to compute the line-of-sight integrals, but this is provided not in the `SkyCoord` object, but rather in the `evaluate` method which we will see soon.
+
+Common coordinate frames are the Ecliptic, Galactic, and Celestial frames ("E", "G", "C" in `healpy`), which can be specified through string representations:
+
+- `"barycentricmeanecliptic"` (Ecliptic)
+- `"galactic"` (Galactic)
+- `"icrs"` (Celestial)
+
+or using the frame objects imported from `astropy.coordinates`:
+
+- `BarycentricMeanEcliptic`
+- `Galactic`
+- `ICRS`
+
+!!! info "Notes on coordinate frames"
+    While the above listed built-in Astropy frames do *not* represent observer-centric coordinate frames,
+    we still use these to specify the frame rotation, which is required internally as the model is evaluated in
+    ecliptic coordinates.
+
+In the following, we show three sets of observations in all three coordinate frames
+```py hl_lines="2 9 15 21"
+import astropy.units as u
+from astropy.coordinates import SkyCoord, BarycentricMeanEcliptic, Galactic, ICRS
+from astropy.time import Time
+
+skycoord_ecliptic = SkyCoord(
+    40 * u.deg, 
+    60 * u.deg, 
+    obstime=Time("2020-01-01"), 
+    frame=BarycentricMeanEcliptic
+)
+skycoord_galactic = SkyCoord(
+    203 * u.deg, 
+    10 * u.deg, 
+    obstime=Time("2020-01-01"), 
+    frame=Galactic
+)
+skycoord_celestial = SkyCoord(
+    12 * u.deg, 
+    40 * u.deg, 
+    obstime=Time("2020-01-01"), 
+    frame=ICRS
+)
 ```
-![Zodiacal emission map](img/binned.png)
-*Note that the color bar is logarithmic.*
 
+### The `evaluate` method
+The zodiacal light is evaluated by providing the `SkyCoord` object to the `zodipy.Model.evaluate` method.
 
-### Bandpass integrated emission
-Instruments do not typically observe at delta frequencies. Usually, we are more interested in finding out
-what the emission looks like over some instrument bandpass. ZodiPy will accept a sequence of frequencies to the `freq`
-argument in addition to the corresponding bandpass weights to the `weights` argument and perform bandpass integration. 
-Note that the bandpass weights must be in spectral radiance units (Jy/sr), even though the weights them self are unitless. A top hat bandpass is assumed if a sequence of frequencies are used without providing weights.
-```python hl_lines="11 12 13 31 32"
-{!examples/get_bandpass_integrated_emission.py!}
-```
-![Generated Bandpass](img/bandpass.png)
-![Center frequency emission](img/center_freq.png)
-![Bandpass integrated emission](img/bandpass_integrated.png)
+```py hl_lines="15"
+import astropy.units as u
+from astropy.coordinates import SkyCoord
+from astropy.time import Time
+import zodipy
 
-### Solar cutoff angle
-Few experiments look directly in towards the Sun. We can initialize `Zodipy` with the `solar_cut` 
-argument to mask all input pointing that looks in towards the sun with an angular distance smaller 
-than the `solar_cut` value.
+model = zodipy.Model(25 * u.micron)
 
-```python hl_lines="9"
-{!examples/get_binned_emission_solar_cutoff.py!}
+skycoord = SkyCoord(
+    40 * u.deg, 
+    60 * u.deg, 
+    obstime=Time("2020-01-01"), 
+    frame="galactic"
+)
+
+emission = model.evaluate(skycoord)
+print(emission)
+# <Quantity [25.08189292] MJy / sr>
+```
+By default, the observer is assumed to be the center of the Earth. The position of the observer can be explicitly provided as the keyword argument `obspos` in the `evaluate` method
+
+```py hl_lines="15 19"
+import astropy.units as u
+from astropy.coordinates import SkyCoord
+from astropy.time import Time
+import zodipy
+
+model = zodipy.Model(25 * u.micron)
+
+skycoord = SkyCoord(
+    40 * u.deg, 
+    60 * u.deg, 
+    obstime=Time("2020-01-01"), 
+    frame="galactic"
+)
+
+emission = model.evaluate(skycoord, obspos="mars")
+print(emission)
+# <Quantity [8.36985535] MJy / sr>
+
+emission = model.evaluate(skycoord, obspos=[0.87, -0.53, 0.001] * u.AU)
+print(emission)
+# <Quantity [20.37750965] MJy / sr>
 ```
-![Zodiacal emission map](img/binned_solar_cutoff.png)
+This argument accepts both a string representing a body recognized by `astropy.coordinates.solar_system_ephemeris` (see the Astropy [documentation](https://docs.astropy.org/en/stable/api/astropy.coordinates.solar_system_ephemeris.html)), or a heliocentric ecliptic cartesian position.
 
+Similar to with the `obstime` attribute in the `SkyCoord` object, the `obspos` keyword have shape `(3,)` or `(3, ncoords)`. If a string representation of a body is used, with several values `obstime` ZodiPy will internally compute a position for each coordinate.
 
-### Non-ecliptic coordinates
-We can specify the coordinate system of the input pointing with the `coord_in` keyword
 
-```python hl_lines="18"
-{!examples/get_binned_gal_emission.py!}
-```
-![Zodiacal emission map galactic](img/binned_gal.png)
+### Multiprocessing
+ZodiPy will distribute the input coordinates to cores if the keyword argument `nprocesses` in the `evaluate` method is `nprocesses >= 1` using Python's `multiprocessing` module.
+```py hl_lines="1 17"
+import multiprocessing
 
+import astropy.units as u
+from astropy.coordinates import SkyCoord
+from astropy.time import Time
+import zodipy
 
-### Component-wise maps
-ZodiPy can also return the zodiacal emission component-wise. In the following example we use
-the DIRBE model since the later Planck models excluded the circumsolar-ring and Earth-trailing 
-feature components. For more information on the interplanetary dust models, please 
-read [Cosmoglobe: Simulating Zodiacal Emission with ZodiPy](https://arxiv.org/abs/2205.12962).
+model = zodipy.Model(25 * u.micron)
 
-```python hl_lines="18"
-{!examples/get_comp_binned_emission.py!}
+skycoord = SkyCoord(
+    40 * u.deg, 
+    60 * u.deg, 
+    obstime=Time("2020-01-01"), 
+    frame="galactic"
+)
+
+emission = model.evaluate(skycoord, nprocesses=multiprocessing.cpu_count())
 ```
-![Component-wise emission maps](img/binned_comp.png)
-*Note that the color for the Cloud component is logarithmic, while the others are linear.*
+
+!!! tip 
+    For all available optional keyword arguments in `zodipy.Model` see [the API reference](reference.md#zodipy.zodipy.Model).
 
 
-## Parallelization
-If you are not using ZodiPy in an already parallelized environment, you may specify the number of cores used by ZodiPy through the `n_proc` keyword. By default `n_proc` is set to 1. For values of `n_proc` > 1, the line-of-sight calculations are parallelized using the `multiprocessing` module.
+## Examples
 
-```python hl_lines="15 16"
-{!examples/get_parallel_emission.py!}
+### Emission along an Ecliptic scan
+In the following, we simulate a scan across the Ecliptic plane
+
+``` py title="ecliptic_scan.py"
+{!examples/ecliptic_scan.py!}
 ```
 
-!!! warning "Windows users"
-    Windows users must make sure to wrap the `get_*_emission_*` function calls in a `if __name__ == "__main__"` guard to avoid spawning infinite processes: 
-    ```python
-    ...
-    if __name__ == "__main__":
-        emission = model.get_emission_pix(
-            ...
-        )
+![Ecliptic scan profile](img/ecliptic_scan.png)
+
+
+### HEALPix maps
+We can use [healpy](https://healpy.readthedocs.io/en/latest/) or [Astropy-healpix](https://astropy-healpix.readthedocs.io/en/latest/) to create a `SkyCoord` object directly from a HEALPIx pixelization
+
+=== "healpy"
+
+    ```py
+    --8<-- "docs/examples/healpy_map.py"
     ```
 
-!!! warning "Using ZodiPy in parallelized environments"
-    If ZodiPy is used in a parallelized environment one may have to specifically set the environment variable 
-    `OMP_NUM_THREADS=1` to avoid oversubscription. This is due automatic parallelization in third party libraries such as `healpy` where for instance the `hp.Rotator` object automatically parallelizes rotation of unit vectors.
-    This means that when using ZodiPy with pointing in a coordinate system other than ecliptic, even if `Zodipy` is initialized with `n_proc`=1, `healpy` will under the hood automatically distribute the pointing to available CPU's.
+=== "astropy-healpix"
 
+    ```py
+    --8<-- "docs/examples/astropy_healpix_map.py"
+    ```
 
-## Visualizing the interplanetary dust distribution of a model
-It is possible to visualize the three-dimensional interplanetary dust distribution of the models used in
-ZodiPy by using the `tabulate_density` function which takes in a interplanetary dust model and a custom grid.
+![HEALPix map](img/healpix_map.png)
+
+### Component-wise zodiacal light
+We can return the zodiacal light for each component by using setting the keyword argument `return_comps` to `True`
+``` py title="component_maps.py"
+{!examples/component_maps.py!}
+```
+
+![Component maps](img/component_maps.png)
 
-In the following example we tabulate the density distribution of the DIRBE interplanetary dust model
-and plot the cross section of the diffuse cloud components density in the yz-plane.
 
-```python
-{!examples/get_density_contour.py!}
+### Visualizing the interplanetary dust distribution
+We can visualize the number density of a supported zodiacal light model by using the `grid_number_density` function
+``` py title="number_density.py"
+{!examples/number_density.py!}
 ```
-![Interplanetary dust distribution](img/density_grid.png)
\ No newline at end of file
+
+![DIRBE model interplanetary dust distribution](img/number_density.png)
+
diff --git a/mkdocs.yml b/mkdocs.yml
index cc35ebd..46f83f6 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -8,16 +8,29 @@ theme:
     - media: "(prefers-color-scheme: light)"
       scheme: default
       toggle:
-        icon: material/lightbulb
+        icon: material/weather-sunny
         name: Switch to dark mode
     - media: "(prefers-color-scheme: dark)"
       scheme: slate
       toggle:
-        icon:  material/lightbulb-outline
+        icon:  material/weather-night
         name: Switch to light mode
   icon:
     logo: material/book-open-page-variant
-
+  features:
+    - search.suggest
+    - search.highlight
+    - content.tabs.link
+    - navigation.indexes
+    - content.tooltips
+    - navigation.path
+    - content.code.annotate
+    - content.code.copy
+    - content.code.select
+    - navigation.tabs
+    - navigation.instant
+    - navigation.top
+    - navigation.tabs.sticky
 markdown_extensions:
   - markdown_include.include:
       base_path: docs
@@ -29,7 +42,8 @@ markdown_extensions:
   - pymdownx.snippets
   - pymdownx.arithmatex:
       generic: true
-
+  - pymdownx.tabbed:
+      alternate_style: true
 extra_javascript:
   - javascripts/mathjax.js
   - https://polyfill.io/v3/polyfill.min.js?features=es6
@@ -45,7 +59,15 @@ nav:
 plugins:
 - search
 - mkdocstrings:
-    default_handler: python
     handlers:
-      options:
-        merge_init_into_class: true
\ No newline at end of file
+      python:
+        options:
+          inherited_members: true
+          merge_init_into_class: true
+          members_order: source
+          separate_signature: true
+          unwrap_annotated: true
+          docstring_section_style: spacy
+          signature_crossrefs: true
+          show_symbol_type_heading: true
+          show_symbol_type_toc: true
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index cc32659..b059d22 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -52,15 +52,42 @@ recommended = ["matplotlib (>=3.3,!=3.4.0,!=3.5.2)", "scipy (>=1.5)"]
 test = ["pytest (>=7.0)", "pytest-astropy (>=0.10)", "pytest-astropy-header (>=0.2.1)", "pytest-doctestplus (>=0.12)", "pytest-xdist", "threadpoolctl"]
 test-all = ["astropy[test]", "coverage[toml]", "ipython (>=4.2)", "objgraph", "sgp4 (>=2.3)", "skyfield (>=1.20)"]
 
+[[package]]
+name = "astropy-healpix"
+version = "1.0.3"
+description = "BSD-licensed HEALPix for Astropy"
+optional = false
+python-versions = ">=3.9"
+files = [
+    {file = "astropy_healpix-1.0.3-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:1a523b4bda2618a94efad12b60431c28c0f5ef4b5afb890d21a858aef2ed779a"},
+    {file = "astropy_healpix-1.0.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:fa34adecf7f5168d65cda05d40db95d4a1aad3e6b66f43489ed4aac471f76496"},
+    {file = "astropy_healpix-1.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a7d2163421ba66e06ed40b7ce7ce89fea60a342d035974c98c2c06c6cf187e9"},
+    {file = "astropy_healpix-1.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5b03a5b18c28ab7ac030a22157cfcd8ff951db7fc751e29e854c17d3da52537"},
+    {file = "astropy_healpix-1.0.3-cp39-abi3-win32.whl", hash = "sha256:ec083f48e091baa5ce97c2e96e94b9fc8921a8c370768a92ac14e86fa3465248"},
+    {file = "astropy_healpix-1.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:e8c32aabfa1fa7d6fd629e3918680cd0585656cfca6ec0c5dafe9e1c7a304508"},
+    {file = "astropy_healpix-1.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:cce4687deb102d1e9dc40f381fcd33027cd9bd0ca7955aa0d70e464128c6914e"},
+    {file = "astropy_healpix-1.0.3-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4d4e3e86d8d010eb50bd31ca0ba438c1b50cdc9ed251da97665a7821880206e"},
+    {file = "astropy_healpix-1.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:59b1dd358c5bcd6196213845c007cee4920b2e1936234374afc7d8cfd6506cfc"},
+    {file = "astropy_healpix-1.0.3.tar.gz", hash = "sha256:de5d2a7ec97b167045066971f25a150d1e4061f07159be23649634db36e79746"},
+]
+
+[package.dependencies]
+astropy = ">=3"
+numpy = ">=1.19"
+
+[package.extras]
+docs = ["matplotlib", "sphinx-astropy"]
+test = ["hypothesis", "pytest-astropy"]
+
 [[package]]
 name = "astropy-iers-data"
-version = "0.2024.4.8.0.32.4"
+version = "0.2024.7.1.0.34.3"
 description = "IERS Earth Rotation and Leap Second tables for the astropy core package"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "astropy-iers-data-0.2024.4.8.0.32.4.tar.gz", hash = "sha256:420afca681a63ef2fa9fa22f78b7629008bd8638ed0a73f9c9288a08dca08bc7"},
-    {file = "astropy_iers_data-0.2024.4.8.0.32.4-py3-none-any.whl", hash = "sha256:4d461830f74b7b8bedac00d45d1a5413b6db58442e2df7c38c6fdc7cabc7a553"},
+    {file = "astropy_iers_data-0.2024.7.1.0.34.3-py3-none-any.whl", hash = "sha256:195053a2b198f964ad89440b419fa4e506d9db6a8e1cc18a387bc9c3b480a09e"},
+    {file = "astropy_iers_data-0.2024.7.1.0.34.3.tar.gz", hash = "sha256:0e3b47ae2ed44a03c3bea40a2b73bfce1858cf4059b4612c677a5e4a90f86003"},
 ]
 
 [package.extras]
@@ -86,15 +113,29 @@ tests = ["attrs[tests-no-zope]", "zope-interface"]
 tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
 tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
 
+[[package]]
+name = "babel"
+version = "2.15.0"
+description = "Internationalization utilities"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "Babel-2.15.0-py3-none-any.whl", hash = "sha256:08706bdad8d0a3413266ab61bd6c34d0c28d6e1e7badf40a2cebe67644e2e1fb"},
+    {file = "babel-2.15.0.tar.gz", hash = "sha256:8daf0e265d05768bc6c7a314cf1321e9a123afc328cc635c18622a2f30a04413"},
+]
+
+[package.extras]
+dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"]
+
 [[package]]
 name = "certifi"
-version = "2024.2.2"
+version = "2024.6.2"
 description = "Python package for providing Mozilla's CA Bundle."
 optional = false
 python-versions = ">=3.6"
 files = [
-    {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"},
-    {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"},
+    {file = "certifi-2024.6.2-py3-none-any.whl", hash = "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56"},
+    {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"},
 ]
 
 [[package]]
@@ -232,128 +273,65 @@ files = [
     {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
 ]
 
-[[package]]
-name = "contourpy"
-version = "1.2.1"
-description = "Python library for calculating contours of 2D quadrilateral grids"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "contourpy-1.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bd7c23df857d488f418439686d3b10ae2fbf9bc256cd045b37a8c16575ea1040"},
-    {file = "contourpy-1.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5b9eb0ca724a241683c9685a484da9d35c872fd42756574a7cfbf58af26677fd"},
-    {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4c75507d0a55378240f781599c30e7776674dbaf883a46d1c90f37e563453480"},
-    {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11959f0ce4a6f7b76ec578576a0b61a28bdc0696194b6347ba3f1c53827178b9"},
-    {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eb3315a8a236ee19b6df481fc5f997436e8ade24a9f03dfdc6bd490fea20c6da"},
-    {file = "contourpy-1.2.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39f3ecaf76cd98e802f094e0d4fbc6dc9c45a8d0c4d185f0f6c2234e14e5f75b"},
-    {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94b34f32646ca0414237168d68a9157cb3889f06b096612afdd296003fdd32fd"},
-    {file = "contourpy-1.2.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:457499c79fa84593f22454bbd27670227874cd2ff5d6c84e60575c8b50a69619"},
-    {file = "contourpy-1.2.1-cp310-cp310-win32.whl", hash = "sha256:ac58bdee53cbeba2ecad824fa8159493f0bf3b8ea4e93feb06c9a465d6c87da8"},
-    {file = "contourpy-1.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:9cffe0f850e89d7c0012a1fb8730f75edd4320a0a731ed0c183904fe6ecfc3a9"},
-    {file = "contourpy-1.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6022cecf8f44e36af10bd9118ca71f371078b4c168b6e0fab43d4a889985dbb5"},
-    {file = "contourpy-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef5adb9a3b1d0c645ff694f9bca7702ec2c70f4d734f9922ea34de02294fdf72"},
-    {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6150ffa5c767bc6332df27157d95442c379b7dce3a38dff89c0f39b63275696f"},
-    {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c863140fafc615c14a4bf4efd0f4425c02230eb8ef02784c9a156461e62c965"},
-    {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:00e5388f71c1a0610e6fe56b5c44ab7ba14165cdd6d695429c5cd94021e390b2"},
-    {file = "contourpy-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4492d82b3bc7fbb7e3610747b159869468079fe149ec5c4d771fa1f614a14df"},
-    {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49e70d111fee47284d9dd867c9bb9a7058a3c617274900780c43e38d90fe1205"},
-    {file = "contourpy-1.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b59c0ffceff8d4d3996a45f2bb6f4c207f94684a96bf3d9728dbb77428dd8cb8"},
-    {file = "contourpy-1.2.1-cp311-cp311-win32.whl", hash = "sha256:7b4182299f251060996af5249c286bae9361fa8c6a9cda5efc29fe8bfd6062ec"},
-    {file = "contourpy-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2855c8b0b55958265e8b5888d6a615ba02883b225f2227461aa9127c578a4922"},
-    {file = "contourpy-1.2.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:62828cada4a2b850dbef89c81f5a33741898b305db244904de418cc957ff05dc"},
-    {file = "contourpy-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:309be79c0a354afff9ff7da4aaed7c3257e77edf6c1b448a779329431ee79d7e"},
-    {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e785e0f2ef0d567099b9ff92cbfb958d71c2d5b9259981cd9bee81bd194c9a4"},
-    {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1cac0a8f71a041aa587410424ad46dfa6a11f6149ceb219ce7dd48f6b02b87a7"},
-    {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:af3f4485884750dddd9c25cb7e3915d83c2db92488b38ccb77dd594eac84c4a0"},
-    {file = "contourpy-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ce6889abac9a42afd07a562c2d6d4b2b7134f83f18571d859b25624a331c90b"},
-    {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a1eea9aecf761c661d096d39ed9026574de8adb2ae1c5bd7b33558af884fb2ce"},
-    {file = "contourpy-1.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:187fa1d4c6acc06adb0fae5544c59898ad781409e61a926ac7e84b8f276dcef4"},
-    {file = "contourpy-1.2.1-cp312-cp312-win32.whl", hash = "sha256:c2528d60e398c7c4c799d56f907664673a807635b857df18f7ae64d3e6ce2d9f"},
-    {file = "contourpy-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:1a07fc092a4088ee952ddae19a2b2a85757b923217b7eed584fdf25f53a6e7ce"},
-    {file = "contourpy-1.2.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bb6834cbd983b19f06908b45bfc2dad6ac9479ae04abe923a275b5f48f1a186b"},
-    {file = "contourpy-1.2.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1d59e739ab0e3520e62a26c60707cc3ab0365d2f8fecea74bfe4de72dc56388f"},
-    {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd3db01f59fdcbce5b22afad19e390260d6d0222f35a1023d9adc5690a889364"},
-    {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a12a813949e5066148712a0626895c26b2578874e4cc63160bb007e6df3436fe"},
-    {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fe0ccca550bb8e5abc22f530ec0466136379c01321fd94f30a22231e8a48d985"},
-    {file = "contourpy-1.2.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1d59258c3c67c865435d8fbeb35f8c59b8bef3d6f46c1f29f6123556af28445"},
-    {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f32c38afb74bd98ce26de7cc74a67b40afb7b05aae7b42924ea990d51e4dac02"},
-    {file = "contourpy-1.2.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d31a63bc6e6d87f77d71e1abbd7387ab817a66733734883d1fc0021ed9bfa083"},
-    {file = "contourpy-1.2.1-cp39-cp39-win32.whl", hash = "sha256:ddcb8581510311e13421b1f544403c16e901c4e8f09083c881fab2be80ee31ba"},
-    {file = "contourpy-1.2.1-cp39-cp39-win_amd64.whl", hash = "sha256:10a37ae557aabf2509c79715cd20b62e4c7c28b8cd62dd7d99e5ed3ce28c3fd9"},
-    {file = "contourpy-1.2.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a31f94983fecbac95e58388210427d68cd30fe8a36927980fab9c20062645609"},
-    {file = "contourpy-1.2.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef2b055471c0eb466033760a521efb9d8a32b99ab907fc8358481a1dd29e3bd3"},
-    {file = "contourpy-1.2.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:b33d2bc4f69caedcd0a275329eb2198f560b325605810895627be5d4b876bf7f"},
-    {file = "contourpy-1.2.1.tar.gz", hash = "sha256:4d8908b3bee1c889e547867ca4cdc54e5ab6be6d3e078556814a22457f49423c"},
-]
-
-[package.dependencies]
-numpy = ">=1.20"
-
-[package.extras]
-bokeh = ["bokeh", "selenium"]
-docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"]
-mypy = ["contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.8.0)", "types-Pillow"]
-test = ["Pillow", "contourpy[test-no-images]", "matplotlib"]
-test-no-images = ["pytest", "pytest-cov", "pytest-xdist", "wurlitzer"]
-
 [[package]]
 name = "coverage"
-version = "7.4.4"
+version = "7.5.4"
 description = "Code coverage measurement for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"},
-    {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"},
-    {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"},
-    {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"},
-    {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"},
-    {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"},
-    {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"},
-    {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"},
-    {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"},
-    {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"},
-    {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"},
-    {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"},
-    {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"},
-    {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"},
-    {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"},
-    {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"},
-    {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"},
-    {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"},
-    {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"},
-    {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"},
-    {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"},
-    {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"},
-    {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"},
-    {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"},
-    {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"},
-    {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"},
-    {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"},
-    {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"},
-    {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"},
-    {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"},
-    {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"},
-    {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"},
-    {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"},
-    {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"},
-    {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"},
-    {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"},
-    {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"},
-    {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"},
-    {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"},
-    {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"},
-    {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"},
-    {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"},
-    {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"},
-    {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"},
-    {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"},
-    {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"},
-    {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"},
-    {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"},
-    {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"},
-    {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"},
-    {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"},
-    {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"},
+    {file = "coverage-7.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6cfb5a4f556bb51aba274588200a46e4dd6b505fb1a5f8c5ae408222eb416f99"},
+    {file = "coverage-7.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2174e7c23e0a454ffe12267a10732c273243b4f2d50d07544a91198f05c48f47"},
+    {file = "coverage-7.5.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2214ee920787d85db1b6a0bd9da5f8503ccc8fcd5814d90796c2f2493a2f4d2e"},
+    {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1137f46adb28e3813dec8c01fefadcb8c614f33576f672962e323b5128d9a68d"},
+    {file = "coverage-7.5.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b385d49609f8e9efc885790a5a0e89f2e3ae042cdf12958b6034cc442de428d3"},
+    {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b4a474f799456e0eb46d78ab07303286a84a3140e9700b9e154cfebc8f527016"},
+    {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5cd64adedf3be66f8ccee418473c2916492d53cbafbfcff851cbec5a8454b136"},
+    {file = "coverage-7.5.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e564c2cf45d2f44a9da56f4e3a26b2236504a496eb4cb0ca7221cd4cc7a9aca9"},
+    {file = "coverage-7.5.4-cp310-cp310-win32.whl", hash = "sha256:7076b4b3a5f6d2b5d7f1185fde25b1e54eb66e647a1dfef0e2c2bfaf9b4c88c8"},
+    {file = "coverage-7.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:018a12985185038a5b2bcafab04ab833a9a0f2c59995b3cec07e10074c78635f"},
+    {file = "coverage-7.5.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:db14f552ac38f10758ad14dd7b983dbab424e731588d300c7db25b6f89e335b5"},
+    {file = "coverage-7.5.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3257fdd8e574805f27bb5342b77bc65578e98cbc004a92232106344053f319ba"},
+    {file = "coverage-7.5.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a6612c99081d8d6134005b1354191e103ec9705d7ba2754e848211ac8cacc6b"},
+    {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d45d3cbd94159c468b9b8c5a556e3f6b81a8d1af2a92b77320e887c3e7a5d080"},
+    {file = "coverage-7.5.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed550e7442f278af76d9d65af48069f1fb84c9f745ae249c1a183c1e9d1b025c"},
+    {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7a892be37ca35eb5019ec85402c3371b0f7cda5ab5056023a7f13da0961e60da"},
+    {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:8192794d120167e2a64721d88dbd688584675e86e15d0569599257566dec9bf0"},
+    {file = "coverage-7.5.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:820bc841faa502e727a48311948e0461132a9c8baa42f6b2b84a29ced24cc078"},
+    {file = "coverage-7.5.4-cp311-cp311-win32.whl", hash = "sha256:6aae5cce399a0f065da65c7bb1e8abd5c7a3043da9dceb429ebe1b289bc07806"},
+    {file = "coverage-7.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:d2e344d6adc8ef81c5a233d3a57b3c7d5181f40e79e05e1c143da143ccb6377d"},
+    {file = "coverage-7.5.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:54317c2b806354cbb2dc7ac27e2b93f97096912cc16b18289c5d4e44fc663233"},
+    {file = "coverage-7.5.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:042183de01f8b6d531e10c197f7f0315a61e8d805ab29c5f7b51a01d62782747"},
+    {file = "coverage-7.5.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a6bb74ed465d5fb204b2ec41d79bcd28afccf817de721e8a807d5141c3426638"},
+    {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3d45ff86efb129c599a3b287ae2e44c1e281ae0f9a9bad0edc202179bcc3a2e"},
+    {file = "coverage-7.5.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5013ed890dc917cef2c9f765c4c6a8ae9df983cd60dbb635df8ed9f4ebc9f555"},
+    {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1014fbf665fef86cdfd6cb5b7371496ce35e4d2a00cda501cf9f5b9e6fced69f"},
+    {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3684bc2ff328f935981847082ba4fdc950d58906a40eafa93510d1b54c08a66c"},
+    {file = "coverage-7.5.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:581ea96f92bf71a5ec0974001f900db495488434a6928a2ca7f01eee20c23805"},
+    {file = "coverage-7.5.4-cp312-cp312-win32.whl", hash = "sha256:73ca8fbc5bc622e54627314c1a6f1dfdd8db69788f3443e752c215f29fa87a0b"},
+    {file = "coverage-7.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:cef4649ec906ea7ea5e9e796e68b987f83fa9a718514fe147f538cfeda76d7a7"},
+    {file = "coverage-7.5.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cdd31315fc20868c194130de9ee6bfd99755cc9565edff98ecc12585b90be882"},
+    {file = "coverage-7.5.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:02ff6e898197cc1e9fa375581382b72498eb2e6d5fc0b53f03e496cfee3fac6d"},
+    {file = "coverage-7.5.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d05c16cf4b4c2fc880cb12ba4c9b526e9e5d5bb1d81313d4d732a5b9fe2b9d53"},
+    {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5986ee7ea0795a4095ac4d113cbb3448601efca7f158ec7f7087a6c705304e4"},
+    {file = "coverage-7.5.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5df54843b88901fdc2f598ac06737f03d71168fd1175728054c8f5a2739ac3e4"},
+    {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:ab73b35e8d109bffbda9a3e91c64e29fe26e03e49addf5b43d85fc426dde11f9"},
+    {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:aea072a941b033813f5e4814541fc265a5c12ed9720daef11ca516aeacd3bd7f"},
+    {file = "coverage-7.5.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:16852febd96acd953b0d55fc842ce2dac1710f26729b31c80b940b9afcd9896f"},
+    {file = "coverage-7.5.4-cp38-cp38-win32.whl", hash = "sha256:8f894208794b164e6bd4bba61fc98bf6b06be4d390cf2daacfa6eca0a6d2bb4f"},
+    {file = "coverage-7.5.4-cp38-cp38-win_amd64.whl", hash = "sha256:e2afe743289273209c992075a5a4913e8d007d569a406ffed0bd080ea02b0633"},
+    {file = "coverage-7.5.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b95c3a8cb0463ba9f77383d0fa8c9194cf91f64445a63fc26fb2327e1e1eb088"},
+    {file = "coverage-7.5.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3d7564cc09dd91b5a6001754a5b3c6ecc4aba6323baf33a12bd751036c998be4"},
+    {file = "coverage-7.5.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:44da56a2589b684813f86d07597fdf8a9c6ce77f58976727329272f5a01f99f7"},
+    {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e16f3d6b491c48c5ae726308e6ab1e18ee830b4cdd6913f2d7f77354b33f91c8"},
+    {file = "coverage-7.5.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dbc5958cb471e5a5af41b0ddaea96a37e74ed289535e8deca404811f6cb0bc3d"},
+    {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:a04e990a2a41740b02d6182b498ee9796cf60eefe40cf859b016650147908029"},
+    {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ddbd2f9713a79e8e7242d7c51f1929611e991d855f414ca9996c20e44a895f7c"},
+    {file = "coverage-7.5.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b1ccf5e728ccf83acd313c89f07c22d70d6c375a9c6f339233dcf792094bcbf7"},
+    {file = "coverage-7.5.4-cp39-cp39-win32.whl", hash = "sha256:56b4eafa21c6c175b3ede004ca12c653a88b6f922494b023aeb1e836df953ace"},
+    {file = "coverage-7.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:65e528e2e921ba8fd67d9055e6b9f9e34b21ebd6768ae1c1723f4ea6ace1234d"},
+    {file = "coverage-7.5.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:79b356f3dd5b26f3ad23b35c75dbdaf1f9e2450b6bcefc6d0825ea0aa3f86ca5"},
+    {file = "coverage-7.5.4.tar.gz", hash = "sha256:a44963520b069e12789d0faea4e9fdb1e410cdc4aab89d94f7f55cbb7fef0353"},
 ]
 
 [package.dependencies]
@@ -362,21 +340,6 @@ tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.1
 [package.extras]
 toml = ["tomli"]
 
-[[package]]
-name = "cycler"
-version = "0.12.1"
-description = "Composable style cycles"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"},
-    {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"},
-]
-
-[package.extras]
-docs = ["ipython", "matplotlib", "numpydoc", "sphinx"]
-tests = ["pytest", "pytest-cov", "pytest-xdist"]
-
 [[package]]
 name = "distlib"
 version = "0.3.8"
@@ -390,13 +353,13 @@ files = [
 
 [[package]]
 name = "exceptiongroup"
-version = "1.2.0"
+version = "1.2.1"
 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.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"},
+    {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"},
 ]
 
 [package.extras]
@@ -404,85 +367,20 @@ test = ["pytest (>=6)"]
 
 [[package]]
 name = "filelock"
-version = "3.13.4"
+version = "3.15.4"
 description = "A platform independent file lock."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "filelock-3.13.4-py3-none-any.whl", hash = "sha256:404e5e9253aa60ad457cae1be07c0f0ca90a63931200a47d9b6a6af84fd7b45f"},
-    {file = "filelock-3.13.4.tar.gz", hash = "sha256:d13f466618bfde72bd2c18255e269f72542c6e70e7bac83a0232d6b1cc5c8cf4"},
+    {file = "filelock-3.15.4-py3-none-any.whl", hash = "sha256:6ca1fffae96225dab4c6eaf1c4f4f28cd2568d3ec2a44e15a08520504de468e7"},
+    {file = "filelock-3.15.4.tar.gz", hash = "sha256:2207938cbc1844345cb01a5a95524dae30f0ce089eba5b00378295a17e3e90cb"},
 ]
 
 [package.extras]
 docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
-testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)", "virtualenv (>=20.26.2)"]
 typing = ["typing-extensions (>=4.8)"]
 
-[[package]]
-name = "fonttools"
-version = "4.51.0"
-description = "Tools to manipulate font files"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:84d7751f4468dd8cdd03ddada18b8b0857a5beec80bce9f435742abc9a851a74"},
-    {file = "fonttools-4.51.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8b4850fa2ef2cfbc1d1f689bc159ef0f45d8d83298c1425838095bf53ef46308"},
-    {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5b48a1121117047d82695d276c2af2ee3a24ffe0f502ed581acc2673ecf1037"},
-    {file = "fonttools-4.51.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:180194c7fe60c989bb627d7ed5011f2bef1c4d36ecf3ec64daec8302f1ae0716"},
-    {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:96a48e137c36be55e68845fc4284533bda2980f8d6f835e26bca79d7e2006438"},
-    {file = "fonttools-4.51.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:806e7912c32a657fa39d2d6eb1d3012d35f841387c8fc6cf349ed70b7c340039"},
-    {file = "fonttools-4.51.0-cp310-cp310-win32.whl", hash = "sha256:32b17504696f605e9e960647c5f64b35704782a502cc26a37b800b4d69ff3c77"},
-    {file = "fonttools-4.51.0-cp310-cp310-win_amd64.whl", hash = "sha256:c7e91abdfae1b5c9e3a543f48ce96013f9a08c6c9668f1e6be0beabf0a569c1b"},
-    {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a8feca65bab31479d795b0d16c9a9852902e3a3c0630678efb0b2b7941ea9c74"},
-    {file = "fonttools-4.51.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8ac27f436e8af7779f0bb4d5425aa3535270494d3bc5459ed27de3f03151e4c2"},
-    {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e19bd9e9964a09cd2433a4b100ca7f34e34731e0758e13ba9a1ed6e5468cc0f"},
-    {file = "fonttools-4.51.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b2b92381f37b39ba2fc98c3a45a9d6383bfc9916a87d66ccb6553f7bdd129097"},
-    {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5f6bc991d1610f5c3bbe997b0233cbc234b8e82fa99fc0b2932dc1ca5e5afec0"},
-    {file = "fonttools-4.51.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9696fe9f3f0c32e9a321d5268208a7cc9205a52f99b89479d1b035ed54c923f1"},
-    {file = "fonttools-4.51.0-cp311-cp311-win32.whl", hash = "sha256:3bee3f3bd9fa1d5ee616ccfd13b27ca605c2b4270e45715bd2883e9504735034"},
-    {file = "fonttools-4.51.0-cp311-cp311-win_amd64.whl", hash = "sha256:0f08c901d3866a8905363619e3741c33f0a83a680d92a9f0e575985c2634fcc1"},
-    {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4060acc2bfa2d8e98117828a238889f13b6f69d59f4f2d5857eece5277b829ba"},
-    {file = "fonttools-4.51.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1250e818b5f8a679ad79660855528120a8f0288f8f30ec88b83db51515411fcc"},
-    {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76f1777d8b3386479ffb4a282e74318e730014d86ce60f016908d9801af9ca2a"},
-    {file = "fonttools-4.51.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b5ad456813d93b9c4b7ee55302208db2b45324315129d85275c01f5cb7e61a2"},
-    {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:68b3fb7775a923be73e739f92f7e8a72725fd333eab24834041365d2278c3671"},
-    {file = "fonttools-4.51.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8e2f1a4499e3b5ee82c19b5ee57f0294673125c65b0a1ff3764ea1f9db2f9ef5"},
-    {file = "fonttools-4.51.0-cp312-cp312-win32.whl", hash = "sha256:278e50f6b003c6aed19bae2242b364e575bcb16304b53f2b64f6551b9c000e15"},
-    {file = "fonttools-4.51.0-cp312-cp312-win_amd64.whl", hash = "sha256:b3c61423f22165541b9403ee39874dcae84cd57a9078b82e1dce8cb06b07fa2e"},
-    {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:1621ee57da887c17312acc4b0e7ac30d3a4fb0fec6174b2e3754a74c26bbed1e"},
-    {file = "fonttools-4.51.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e9d9298be7a05bb4801f558522adbe2feea1b0b103d5294ebf24a92dd49b78e5"},
-    {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee1af4be1c5afe4c96ca23badd368d8dc75f611887fb0c0dac9f71ee5d6f110e"},
-    {file = "fonttools-4.51.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c18b49adc721a7d0b8dfe7c3130c89b8704baf599fb396396d07d4aa69b824a1"},
-    {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:de7c29bdbdd35811f14493ffd2534b88f0ce1b9065316433b22d63ca1cd21f14"},
-    {file = "fonttools-4.51.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:cadf4e12a608ef1d13e039864f484c8a968840afa0258b0b843a0556497ea9ed"},
-    {file = "fonttools-4.51.0-cp38-cp38-win32.whl", hash = "sha256:aefa011207ed36cd280babfaa8510b8176f1a77261833e895a9d96e57e44802f"},
-    {file = "fonttools-4.51.0-cp38-cp38-win_amd64.whl", hash = "sha256:865a58b6e60b0938874af0968cd0553bcd88e0b2cb6e588727117bd099eef836"},
-    {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:60a3409c9112aec02d5fb546f557bca6efa773dcb32ac147c6baf5f742e6258b"},
-    {file = "fonttools-4.51.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f7e89853d8bea103c8e3514b9f9dc86b5b4120afb4583b57eb10dfa5afbe0936"},
-    {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56fc244f2585d6c00b9bcc59e6593e646cf095a96fe68d62cd4da53dd1287b55"},
-    {file = "fonttools-4.51.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d145976194a5242fdd22df18a1b451481a88071feadf251221af110ca8f00ce"},
-    {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c5b8cab0c137ca229433570151b5c1fc6af212680b58b15abd797dcdd9dd5051"},
-    {file = "fonttools-4.51.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:54dcf21a2f2d06ded676e3c3f9f74b2bafded3a8ff12f0983160b13e9f2fb4a7"},
-    {file = "fonttools-4.51.0-cp39-cp39-win32.whl", hash = "sha256:0118ef998a0699a96c7b28457f15546815015a2710a1b23a7bf6c1be60c01636"},
-    {file = "fonttools-4.51.0-cp39-cp39-win_amd64.whl", hash = "sha256:599bdb75e220241cedc6faebfafedd7670335d2e29620d207dd0378a4e9ccc5a"},
-    {file = "fonttools-4.51.0-py3-none-any.whl", hash = "sha256:15c94eeef6b095831067f72c825eb0e2d48bb4cea0647c1b05c981ecba2bf39f"},
-    {file = "fonttools-4.51.0.tar.gz", hash = "sha256:dc0673361331566d7a663d7ce0f6fdcbfbdc1f59c6e3ed1165ad7202ca183c68"},
-]
-
-[package.extras]
-all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "fs (>=2.2.0,<3)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"]
-graphite = ["lz4 (>=1.7.4.2)"]
-interpolatable = ["munkres", "pycairo", "scipy"]
-lxml = ["lxml (>=4.0)"]
-pathops = ["skia-pathops (>=0.5.0)"]
-plot = ["matplotlib"]
-repacker = ["uharfbuzz (>=0.23.0)"]
-symfont = ["sympy"]
-type1 = ["xattr"]
-ufo = ["fs (>=2.2.0,<3)"]
-unicode = ["unicodedata2 (>=15.1.0)"]
-woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"]
-
 [[package]]
 name = "ghp-import"
 version = "2.1.0"
@@ -502,13 +400,13 @@ dev = ["flake8", "markdown", "twine", "wheel"]
 
 [[package]]
 name = "griffe"
-version = "0.42.1"
+version = "0.47.0"
 description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "griffe-0.42.1-py3-none-any.whl", hash = "sha256:7e805e35617601355edcac0d3511cedc1ed0cb1f7645e2d336ae4b05bbae7b3b"},
-    {file = "griffe-0.42.1.tar.gz", hash = "sha256:57046131384043ed078692b85d86b76568a686266cc036b9b56b704466f803ce"},
+    {file = "griffe-0.47.0-py3-none-any.whl", hash = "sha256:07a2fd6a8c3d21d0bbb0decf701d62042ccc8a576645c7f8799fe1f10de2b2de"},
+    {file = "griffe-0.47.0.tar.gz", hash = "sha256:95119a440a3c932b13293538bdbc405bee4c36428547553dc6b327e7e7d35e5a"},
 ]
 
 [package.dependencies]
@@ -516,44 +414,47 @@ colorama = ">=0.4"
 
 [[package]]
 name = "healpy"
-version = "1.16.6"
+version = "1.17.1"
 description = "Healpix tools package for Python"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "healpy-1.16.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:1cad7ed1d7030eeb06081240f669c9107db1282678512c85f5cf6eef1bea01bb"},
-    {file = "healpy-1.16.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6be3ca85cb78f3f243f660f3105d362615843f32c1ff53ffdead5ea1ffe07eb6"},
-    {file = "healpy-1.16.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4469864f465fc4bc3752543812ab1161d84eff4a8aead3842d56ea1b2585bb"},
-    {file = "healpy-1.16.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:366a269b239eeabf924d4a069cce5f2f1d72c7af24132d59f95fec09f670a38a"},
-    {file = "healpy-1.16.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3324fc5aa737188db8017819d4ecb18c11315873481f31d97aceb29958591f40"},
-    {file = "healpy-1.16.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5adc827a90dffa72158b8a576143459d06dbd3afc52c2ff5c4a566aafcb049"},
-    {file = "healpy-1.16.6-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:12b5c5ea0c767b7ae7659b18bd4c42b11384765b44b9205b27147ab4686657bf"},
-    {file = "healpy-1.16.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f72338a29fc776a4031cb2e9b7bd9306cbe2f0b6355e9541716b38ad23ba880d"},
-    {file = "healpy-1.16.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d13dd979e804d2d7e80f5121b0acf46f94dcddfac757ebb01cecbcb3304b285"},
-    {file = "healpy-1.16.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:0a0e0b725c9fa2777f184c70beed87ff7c8dfa1869aca1fc370b40057db5c739"},
-    {file = "healpy-1.16.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7e5a284e7ec4cc06b7b222098ab0961a3b31f72014f0194b3cfd62e4dcd2963e"},
-    {file = "healpy-1.16.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:754fed0f2313f805535ea44e37a538b8d7b58b377cdfe8f36accdf8fec417f4a"},
-    {file = "healpy-1.16.6.tar.gz", hash = "sha256:0ab26e828fcd251a141095af6d9bf3dba43cec6f0f5cd48b65bf0af8f56329f1"},
+    {file = "healpy-1.17.1-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:19a5eea85f348896eb58e572ce25845cc0dc40661508b6b4b6e8fb4f34d50d74"},
+    {file = "healpy-1.17.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:e06abb17b17e2ce586de37b0327dccd334d60802a4e20d7b67f349e248e6fd0f"},
+    {file = "healpy-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:574c714c17a23ae21668df4587b63f25d977436be685df51a1d11be217079ee9"},
+    {file = "healpy-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610fe0255a5dd72108ad9778f0ca90422ebfa34ace098cace93b470186c9f6cc"},
+    {file = "healpy-1.17.1-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:63fbd4dbdac0d869aacb65d952ce0415c20393107fbe3ded18f682ba6b5e4a51"},
+    {file = "healpy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:50b17c796fa90679d9618d6cbeb048ad4c82f226d97b0575f6f62c2ef7f8a788"},
+    {file = "healpy-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80dee19bec6029df43ab606c4caefae945f787edc8a9e551a3189d437c0b52ba"},
+    {file = "healpy-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b05ba7ae36708371cce9b01934d86d22d17b0a81898b7b7c83e6f99b64933b62"},
+    {file = "healpy-1.17.1-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:8a9db38eca26764b671156529edd6a19f9b5d5c9ed3f2bf190c8b1b93575a4ef"},
+    {file = "healpy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:c66f35a77def27cfc444f1f9a36521c0a954a88eba7d11bec8c10616e0bb4a5b"},
+    {file = "healpy-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378e36660d3df4ef5c85c1b0d782d5993c86daefafe0a1e4594e7f7a71792583"},
+    {file = "healpy-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f5ea378e683f877f70870744abfbeb900d20e43ba024074763e3999d4db5402"},
+    {file = "healpy-1.17.1-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:26fc9a22939c04b09bdda3a9e72fb6969dd8e446b420ea7947b7e146d4c6c075"},
+    {file = "healpy-1.17.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:36e4699ed00aaec7dad9d22d8de635900e4a7b1bb4e6ce2dc9c0cb3ef47e6ec1"},
+    {file = "healpy-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f521714cf325bc2886c95a7cd9e6a81d4e4583c6d478b0ffbe8d08d4da7d7954"},
+    {file = "healpy-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f18548572b6a702e57071b70cc9818fe7ed74edaccbab5987ff55c6e9fd9b3a"},
+    {file = "healpy-1.17.1.tar.gz", hash = "sha256:6469fc9669e4cc6068c1509ba67228f215db57b3c2d6f00bd1eb481d97732e88"},
 ]
 
 [package.dependencies]
 astropy = "*"
-matplotlib = "*"
 numpy = ">=1.19"
-scipy = "*"
 
 [package.extras]
-test = ["pytest", "pytest-cython", "pytest-doctestplus", "requests"]
+all = ["matplotlib", "scipy"]
+test = ["matplotlib", "pytest", "pytest-astropy-header", "pytest-cython", "pytest-doctestplus", "requests", "scipy"]
 
 [[package]]
 name = "hypothesis"
-version = "6.100.1"
+version = "6.104.2"
 description = "A library for property-based testing"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "hypothesis-6.100.1-py3-none-any.whl", hash = "sha256:3dacf6ec90e8d14aaee02cde081ac9a17d5b70105e45e6ac822db72052c0195b"},
-    {file = "hypothesis-6.100.1.tar.gz", hash = "sha256:ebff09d7fa4f1fb6a855a812baf17e578b4481b7b70ec6d96496210d1a4c6c35"},
+    {file = "hypothesis-6.104.2-py3-none-any.whl", hash = "sha256:8b52b7e2462e552c75b819495d5cb6251a2b840accc79cf2ce52588004c915d9"},
+    {file = "hypothesis-6.104.2.tar.gz", hash = "sha256:6f2a1489bc8fe1c87ffd202707319b66ec46b2bc11faf6e0161e957b8b9b1eab"},
 ]
 
 [package.dependencies]
@@ -562,10 +463,10 @@ exceptiongroup = {version = ">=1.0.0", markers = "python_version < \"3.11\""}
 sortedcontainers = ">=2.1.0,<3.0.0"
 
 [package.extras]
-all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.54)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.2)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.1)"]
+all = ["backports.zoneinfo (>=0.2.1)", "black (>=19.10b0)", "click (>=7.0)", "crosshair-tool (>=0.0.55)", "django (>=3.2)", "dpcontracts (>=0.4)", "hypothesis-crosshair (>=0.0.4)", "lark (>=0.10.1)", "libcst (>=0.3.16)", "numpy (>=1.17.3)", "pandas (>=1.1)", "pytest (>=4.6)", "python-dateutil (>=1.4)", "pytz (>=2014.1)", "redis (>=3.0.0)", "rich (>=9.0.0)", "tzdata (>=2024.1)"]
 cli = ["black (>=19.10b0)", "click (>=7.0)", "rich (>=9.0.0)"]
 codemods = ["libcst (>=0.3.16)"]
-crosshair = ["crosshair-tool (>=0.0.54)", "hypothesis-crosshair (>=0.0.2)"]
+crosshair = ["crosshair-tool (>=0.0.55)", "hypothesis-crosshair (>=0.0.4)"]
 dateutil = ["python-dateutil (>=1.4)"]
 django = ["django (>=3.2)"]
 dpcontracts = ["dpcontracts (>=0.4)"]
@@ -580,13 +481,13 @@ zoneinfo = ["backports.zoneinfo (>=0.2.1)", "tzdata (>=2024.1)"]
 
 [[package]]
 name = "identify"
-version = "2.5.35"
+version = "2.5.36"
 description = "File identification library for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "identify-2.5.35-py2.py3-none-any.whl", hash = "sha256:c4de0081837b211594f8e877a6b4fad7ca32bbfc1a9307fdd61c28bfe923f13e"},
-    {file = "identify-2.5.35.tar.gz", hash = "sha256:10a7ca245cfcd756a554a7288159f72ff105ad233c7c4b9c6f0f4d108f5f6791"},
+    {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"},
+    {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"},
 ]
 
 [package.extras]
@@ -605,40 +506,22 @@ files = [
 
 [[package]]
 name = "importlib-metadata"
-version = "7.1.0"
+version = "8.0.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.0.0-py3-none-any.whl", hash = "sha256:15584cf2b1bf449d98ff8a6ff1abef57bf20f3ac6454f431736cd3e660921b2f"},
+    {file = "importlib_metadata-8.0.0.tar.gz", hash = "sha256:188bd24e4c346d3f0a933f275c2fec67050326a856b9a359881d7c2a697e8812"},
 ]
 
 [package.dependencies]
 zipp = ">=0.5"
 
 [package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
 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)"]
-
-[[package]]
-name = "importlib-resources"
-version = "6.4.0"
-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"},
-]
-
-[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)"]
+test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"]
 
 [[package]]
 name = "iniconfig"
@@ -653,13 +536,13 @@ files = [
 
 [[package]]
 name = "jinja2"
-version = "3.1.3"
+version = "3.1.4"
 description = "A very fast and expressive template engine."
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"},
-    {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"},
+    {file = "jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d"},
+    {file = "jinja2-3.1.4.tar.gz", hash = "sha256:4a3aee7acbbe7303aede8e9648d13b8bf88a429282aa6122a993f0ac800cb369"},
 ]
 
 [package.dependencies]
@@ -670,160 +553,52 @@ i18n = ["Babel (>=2.7)"]
 
 [[package]]
 name = "jplephem"
-version = "2.21"
+version = "2.22"
 description = "Use a JPL ephemeris to predict planet positions."
 optional = false
 python-versions = "*"
 files = [
-    {file = "jplephem-2.21-py3-none-any.whl", hash = "sha256:487fd8c94c458bba74f43bcf332702bbcb2b8e5a4e3fb15c36159fc92e69e6a1"},
-    {file = "jplephem-2.21.tar.gz", hash = "sha256:34194b610695f21b89217b9852b8dabadbce80848cb369d9567ef12dc4828d55"},
+    {file = "jplephem-2.22-py3-none-any.whl", hash = "sha256:efe4b3590f3eef1a5dfcf7078e8e93803f8ce3276c3306e93bfd20b3536b2c60"},
+    {file = "jplephem-2.22.tar.gz", hash = "sha256:0d9acc7217b4806feba93e72974ceead5611104bce6789af38d4f27dcf7a592c"},
 ]
 
 [package.dependencies]
 numpy = "*"
 
-[[package]]
-name = "kiwisolver"
-version = "1.4.5"
-description = "A fast implementation of the Cassowary constraint solver"
-optional = false
-python-versions = ">=3.7"
-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"},
-]
-
 [[package]]
 name = "markdown"
-version = "3.3.7"
-description = "Python implementation of Markdown."
+version = "3.6"
+description = "Python implementation of John Gruber's Markdown."
 optional = false
-python-versions = ">=3.6"
+python-versions = ">=3.8"
 files = [
-    {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"},
-    {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"},
+    {file = "Markdown-3.6-py3-none-any.whl", hash = "sha256:48f276f4d8cfb8ce6527c8f79e2ee29708508bf4d40aa410fbc3b4ee832c850f"},
+    {file = "Markdown-3.6.tar.gz", hash = "sha256:ed4f41f6daecbeeb96e576ce414c41d2d876daa9a16cb35fa8ed8c2ddfad0224"},
 ]
 
 [package.dependencies]
 importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
 
 [package.extras]
+docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"]
 testing = ["coverage", "pyyaml"]
 
 [[package]]
 name = "markdown-include"
-version = "0.6.0"
-description = "This is an extension to Python-Markdown which provides an \"include\" function, similar to that found in LaTeX (and also the C pre-processor and Fortran). I originally wrote it for my FORD Fortran auto-documentation generator."
+version = "0.8.1"
+description = "A Python-Markdown extension which provides an 'include' function"
 optional = false
-python-versions = "*"
+python-versions = ">=3.7"
 files = [
-    {file = "markdown-include-0.6.0.tar.gz", hash = "sha256:6f5d680e36f7780c7f0f61dca53ca581bd50d1b56137ddcd6353efafa0c3e4a2"},
+    {file = "markdown-include-0.8.1.tar.gz", hash = "sha256:1d0623e0fc2757c38d35df53752768356162284259d259c486b4ab6285cdbbe3"},
+    {file = "markdown_include-0.8.1-py3-none-any.whl", hash = "sha256:32f0635b9cfef46997b307e2430022852529f7a5b87c0075c504283e7cc7db53"},
 ]
 
 [package.dependencies]
-markdown = "*"
+markdown = ">=3.0"
+
+[package.extras]
+tests = ["pytest"]
 
 [[package]]
 name = "markupsafe"
@@ -894,55 +669,6 @@ files = [
     {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"},
 ]
 
-[[package]]
-name = "matplotlib"
-version = "3.8.4"
-description = "Python plotting package"
-optional = false
-python-versions = ">=3.9"
-files = [
-    {file = "matplotlib-3.8.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:abc9d838f93583650c35eca41cfcec65b2e7cb50fd486da6f0c49b5e1ed23014"},
-    {file = "matplotlib-3.8.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f65c9f002d281a6e904976007b2d46a1ee2bcea3a68a8c12dda24709ddc9106"},
-    {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce1edd9f5383b504dbc26eeea404ed0a00656c526638129028b758fd43fc5f10"},
-    {file = "matplotlib-3.8.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecd79298550cba13a43c340581a3ec9c707bd895a6a061a78fa2524660482fc0"},
-    {file = "matplotlib-3.8.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:90df07db7b599fe7035d2f74ab7e438b656528c68ba6bb59b7dc46af39ee48ef"},
-    {file = "matplotlib-3.8.4-cp310-cp310-win_amd64.whl", hash = "sha256:ac24233e8f2939ac4fd2919eed1e9c0871eac8057666070e94cbf0b33dd9c338"},
-    {file = "matplotlib-3.8.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:72f9322712e4562e792b2961971891b9fbbb0e525011e09ea0d1f416c4645661"},
-    {file = "matplotlib-3.8.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:232ce322bfd020a434caaffbd9a95333f7c2491e59cfc014041d95e38ab90d1c"},
-    {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6addbd5b488aedb7f9bc19f91cd87ea476206f45d7116fcfe3d31416702a82fa"},
-    {file = "matplotlib-3.8.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc4ccdc64e3039fc303defd119658148f2349239871db72cd74e2eeaa9b80b71"},
-    {file = "matplotlib-3.8.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b7a2a253d3b36d90c8993b4620183b55665a429da8357a4f621e78cd48b2b30b"},
-    {file = "matplotlib-3.8.4-cp311-cp311-win_amd64.whl", hash = "sha256:8080d5081a86e690d7688ffa542532e87f224c38a6ed71f8fbed34dd1d9fedae"},
-    {file = "matplotlib-3.8.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6485ac1f2e84676cff22e693eaa4fbed50ef5dc37173ce1f023daef4687df616"},
-    {file = "matplotlib-3.8.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c89ee9314ef48c72fe92ce55c4e95f2f39d70208f9f1d9db4e64079420d8d732"},
-    {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50bac6e4d77e4262c4340d7a985c30912054745ec99756ce213bfbc3cb3808eb"},
-    {file = "matplotlib-3.8.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f51c4c869d4b60d769f7b4406eec39596648d9d70246428745a681c327a8ad30"},
-    {file = "matplotlib-3.8.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b12ba985837e4899b762b81f5b2845bd1a28f4fdd1a126d9ace64e9c4eb2fb25"},
-    {file = "matplotlib-3.8.4-cp312-cp312-win_amd64.whl", hash = "sha256:7a6769f58ce51791b4cb8b4d7642489df347697cd3e23d88266aaaee93b41d9a"},
-    {file = "matplotlib-3.8.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:843cbde2f0946dadd8c5c11c6d91847abd18ec76859dc319362a0964493f0ba6"},
-    {file = "matplotlib-3.8.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1c13f041a7178f9780fb61cc3a2b10423d5e125480e4be51beaf62b172413b67"},
-    {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb44f53af0a62dc80bba4443d9b27f2fde6acfdac281d95bc872dc148a6509cc"},
-    {file = "matplotlib-3.8.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:606e3b90897554c989b1e38a258c626d46c873523de432b1462f295db13de6f9"},
-    {file = "matplotlib-3.8.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9bb0189011785ea794ee827b68777db3ca3f93f3e339ea4d920315a0e5a78d54"},
-    {file = "matplotlib-3.8.4-cp39-cp39-win_amd64.whl", hash = "sha256:6209e5c9aaccc056e63b547a8152661324404dd92340a6e479b3a7f24b42a5d0"},
-    {file = "matplotlib-3.8.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c7064120a59ce6f64103c9cefba8ffe6fba87f2c61d67c401186423c9a20fd35"},
-    {file = "matplotlib-3.8.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0e47eda4eb2614300fc7bb4657fced3e83d6334d03da2173b09e447418d499f"},
-    {file = "matplotlib-3.8.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:493e9f6aa5819156b58fce42b296ea31969f2aab71c5b680b4ea7a3cb5c07d94"},
-    {file = "matplotlib-3.8.4.tar.gz", hash = "sha256:8aac397d5e9ec158960e31c381c5ffc52ddd52bd9a47717e2a694038167dffea"},
-]
-
-[package.dependencies]
-contourpy = ">=1.0.1"
-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"
-packaging = ">=20.0"
-pillow = ">=8"
-pyparsing = ">=2.3.1"
-python-dateutil = ">=2.7"
-
 [[package]]
 name = "mergedeep"
 version = "1.3.4"
@@ -956,34 +682,34 @@ files = [
 
 [[package]]
 name = "mkdocs"
-version = "1.5.3"
+version = "1.6.0"
 description = "Project documentation with Markdown."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"},
-    {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"},
+    {file = "mkdocs-1.6.0-py3-none-any.whl", hash = "sha256:1eb5cb7676b7d89323e62b56235010216319217d4af5ddc543a91beb8d125ea7"},
+    {file = "mkdocs-1.6.0.tar.gz", hash = "sha256:a73f735824ef83a4f3bcb7a231dcab23f5a838f88b7efc54a0eef5fbdbc3c512"},
 ]
 
 [package.dependencies]
 click = ">=7.0"
 colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""}
 ghp-import = ">=1.0"
-importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""}
+importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""}
 jinja2 = ">=2.11.1"
-markdown = ">=3.2.1"
+markdown = ">=3.3.6"
 markupsafe = ">=2.0.1"
 mergedeep = ">=1.3.4"
+mkdocs-get-deps = ">=0.2.0"
 packaging = ">=20.5"
 pathspec = ">=0.11.1"
-platformdirs = ">=2.2.0"
 pyyaml = ">=5.1"
 pyyaml-env-tag = ">=0.1"
 watchdog = ">=2.0"
 
 [package.extras]
 i18n = ["babel (>=2.9.0)"]
-min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"]
+min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.4)", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"]
 
 [[package]]
 name = "mkdocs-autorefs"
@@ -1001,27 +727,51 @@ Markdown = ">=3.3"
 markupsafe = ">=2.0.1"
 mkdocs = ">=1.1"
 
+[[package]]
+name = "mkdocs-get-deps"
+version = "0.2.0"
+description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"},
+    {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"},
+]
+
+[package.dependencies]
+importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""}
+mergedeep = ">=1.3.4"
+platformdirs = ">=2.2.0"
+pyyaml = ">=5.1"
+
 [[package]]
 name = "mkdocs-material"
-version = "9.1.21"
+version = "9.5.28"
 description = "Documentation that simply works"
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "mkdocs_material-9.1.21-py3-none-any.whl", hash = "sha256:58bb2f11ef240632e176d6f0f7d1cff06be1d11c696a5a1b553b808b4280ed47"},
-    {file = "mkdocs_material-9.1.21.tar.gz", hash = "sha256:71940cdfca84ab296b6362889c25395b1621273fb16c93deda257adb7ff44ec8"},
+    {file = "mkdocs_material-9.5.28-py3-none-any.whl", hash = "sha256:ff48b11b2a9f705dd210409ec3b418ab443dd36d96915bcba45a41f10ea27bfd"},
+    {file = "mkdocs_material-9.5.28.tar.gz", hash = "sha256:9cba305283ad1600e3d0a67abe72d7a058b54793b47be39930911a588fe0336b"},
 ]
 
 [package.dependencies]
-colorama = ">=0.4"
-jinja2 = ">=3.0"
-markdown = ">=3.2"
-mkdocs = ">=1.5.0"
-mkdocs-material-extensions = ">=1.1"
-pygments = ">=2.14"
-pymdown-extensions = ">=9.9.1"
-regex = ">=2022.4.24"
-requests = ">=2.26"
+babel = ">=2.10,<3.0"
+colorama = ">=0.4,<1.0"
+jinja2 = ">=3.0,<4.0"
+markdown = ">=3.2,<4.0"
+mkdocs = ">=1.6,<2.0"
+mkdocs-material-extensions = ">=1.3,<2.0"
+paginate = ">=0.5,<1.0"
+pygments = ">=2.16,<3.0"
+pymdown-extensions = ">=10.2,<11.0"
+regex = ">=2022.4"
+requests = ">=2.26,<3.0"
+
+[package.extras]
+git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"]
+imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"]
+recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"]
 
 [[package]]
 name = "mkdocs-material-extensions"
@@ -1036,22 +786,24 @@ files = [
 
 [[package]]
 name = "mkdocstrings"
-version = "0.23.0"
+version = "0.25.1"
 description = "Automatic documentation from sources, for MkDocs."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "mkdocstrings-0.23.0-py3-none-any.whl", hash = "sha256:051fa4014dfcd9ed90254ae91de2dbb4f24e166347dae7be9a997fe16316c65e"},
-    {file = "mkdocstrings-0.23.0.tar.gz", hash = "sha256:d9c6a37ffbe7c14a7a54ef1258c70b8d394e6a33a1c80832bce40b9567138d1c"},
+    {file = "mkdocstrings-0.25.1-py3-none-any.whl", hash = "sha256:da01fcc2670ad61888e8fe5b60afe9fee5781017d67431996832d63e887c2e51"},
+    {file = "mkdocstrings-0.25.1.tar.gz", hash = "sha256:c3a2515f31577f311a9ee58d089e4c51fc6046dbd9e9b4c3de4c3194667fe9bf"},
 ]
 
 [package.dependencies]
+click = ">=7.0"
 importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""}
 Jinja2 = ">=2.11.1"
 Markdown = ">=3.3"
 MarkupSafe = ">=1.1"
-mkdocs = ">=1.2"
+mkdocs = ">=1.4"
 mkdocs-autorefs = ">=0.3.1"
+platformdirs = ">=2.2.0"
 pymdown-extensions = ">=6.3"
 typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""}
 
@@ -1062,54 +814,53 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"]
 
 [[package]]
 name = "mkdocstrings-python"
-version = "1.9.1"
+version = "1.10.5"
 description = "A Python handler for mkdocstrings."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "mkdocstrings_python-1.9.1-py3-none-any.whl", hash = "sha256:bf2406ed37ff19c9f8e0acc9d72c41953fb789bfb4ae10eb00ee17e537eeb220"},
-    {file = "mkdocstrings_python-1.9.1.tar.gz", hash = "sha256:077188fa43eab3b689826b15da7da6753501224b2482e4eca3ce4412ce3b71cb"},
+    {file = "mkdocstrings_python-1.10.5-py3-none-any.whl", hash = "sha256:92e3c588ef1b41151f55281d075de7558dd8092e422cb07a65b18ee2b0863ebb"},
+    {file = "mkdocstrings_python-1.10.5.tar.gz", hash = "sha256:acdc2a98cd9d46c7ece508193a16ca03ccabcb67520352b7449f84b57c162bdf"},
 ]
 
 [package.dependencies]
-griffe = ">=0.37"
-markdown = ">=3.3,<3.6"
-mkdocstrings = ">=0.20"
+griffe = ">=0.47"
+mkdocstrings = ">=0.25"
 
 [[package]]
 name = "mypy"
-version = "1.9.0"
+version = "1.10.1"
 description = "Optional static typing for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"},
-    {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"},
-    {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"},
-    {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"},
-    {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"},
-    {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"},
-    {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"},
-    {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"},
-    {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"},
-    {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"},
-    {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"},
-    {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"},
-    {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"},
-    {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"},
-    {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"},
-    {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"},
-    {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"},
-    {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"},
-    {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"},
-    {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"},
-    {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"},
-    {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"},
-    {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"},
-    {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"},
-    {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"},
-    {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"},
-    {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"},
+    {file = "mypy-1.10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e36f229acfe250dc660790840916eb49726c928e8ce10fbdf90715090fe4ae02"},
+    {file = "mypy-1.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:51a46974340baaa4145363b9e051812a2446cf583dfaeba124af966fa44593f7"},
+    {file = "mypy-1.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:901c89c2d67bba57aaaca91ccdb659aa3a312de67f23b9dfb059727cce2e2e0a"},
+    {file = "mypy-1.10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0cd62192a4a32b77ceb31272d9e74d23cd88c8060c34d1d3622db3267679a5d9"},
+    {file = "mypy-1.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a2cbc68cb9e943ac0814c13e2452d2046c2f2b23ff0278e26599224cf164e78d"},
+    {file = "mypy-1.10.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:bd6f629b67bb43dc0d9211ee98b96d8dabc97b1ad38b9b25f5e4c4d7569a0c6a"},
+    {file = "mypy-1.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a1bbb3a6f5ff319d2b9d40b4080d46cd639abe3516d5a62c070cf0114a457d84"},
+    {file = "mypy-1.10.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8edd4e9bbbc9d7b79502eb9592cab808585516ae1bcc1446eb9122656c6066f"},
+    {file = "mypy-1.10.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6166a88b15f1759f94a46fa474c7b1b05d134b1b61fca627dd7335454cc9aa6b"},
+    {file = "mypy-1.10.1-cp311-cp311-win_amd64.whl", hash = "sha256:5bb9cd11c01c8606a9d0b83ffa91d0b236a0e91bc4126d9ba9ce62906ada868e"},
+    {file = "mypy-1.10.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d8681909f7b44d0b7b86e653ca152d6dff0eb5eb41694e163c6092124f8246d7"},
+    {file = "mypy-1.10.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:378c03f53f10bbdd55ca94e46ec3ba255279706a6aacaecac52ad248f98205d3"},
+    {file = "mypy-1.10.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bacf8f3a3d7d849f40ca6caea5c055122efe70e81480c8328ad29c55c69e93e"},
+    {file = "mypy-1.10.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:701b5f71413f1e9855566a34d6e9d12624e9e0a8818a5704d74d6b0402e66c04"},
+    {file = "mypy-1.10.1-cp312-cp312-win_amd64.whl", hash = "sha256:3c4c2992f6ea46ff7fce0072642cfb62af7a2484efe69017ed8b095f7b39ef31"},
+    {file = "mypy-1.10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:604282c886497645ffb87b8f35a57ec773a4a2721161e709a4422c1636ddde5c"},
+    {file = "mypy-1.10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37fd87cab83f09842653f08de066ee68f1182b9b5282e4634cdb4b407266bade"},
+    {file = "mypy-1.10.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8addf6313777dbb92e9564c5d32ec122bf2c6c39d683ea64de6a1fd98b90fe37"},
+    {file = "mypy-1.10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5cc3ca0a244eb9a5249c7c583ad9a7e881aa5d7b73c35652296ddcdb33b2b9c7"},
+    {file = "mypy-1.10.1-cp38-cp38-win_amd64.whl", hash = "sha256:1b3a2ffce52cc4dbaeee4df762f20a2905aa171ef157b82192f2e2f368eec05d"},
+    {file = "mypy-1.10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fe85ed6836165d52ae8b88f99527d3d1b2362e0cb90b005409b8bed90e9059b3"},
+    {file = "mypy-1.10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c2ae450d60d7d020d67ab440c6e3fae375809988119817214440033f26ddf7bf"},
+    {file = "mypy-1.10.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6be84c06e6abd72f960ba9a71561c14137a583093ffcf9bbfaf5e613d63fa531"},
+    {file = "mypy-1.10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2189ff1e39db399f08205e22a797383613ce1cb0cb3b13d8bcf0170e45b96cc3"},
+    {file = "mypy-1.10.1-cp39-cp39-win_amd64.whl", hash = "sha256:97a131ee36ac37ce9581f4220311247ab6cba896b4395b9c87af0675a13a755f"},
+    {file = "mypy-1.10.1-py3-none-any.whl", hash = "sha256:71d8ac0b906354ebda8ef1673e5fde785936ac1f29ff6987c7483cfbd5a4235a"},
+    {file = "mypy-1.10.1.tar.gz", hash = "sha256:1f8f492d7db9e3593ef42d4f115f04e556130f2819ad33ab84551403e97dd4c0"},
 ]
 
 [package.dependencies]
@@ -1136,18 +887,15 @@ files = [
 
 [[package]]
 name = "nodeenv"
-version = "1.8.0"
+version = "1.9.1"
 description = "Node.js virtual environment builder"
 optional = false
-python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*"
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
 files = [
-    {file = "nodeenv-1.8.0-py2.py3-none-any.whl", hash = "sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec"},
-    {file = "nodeenv-1.8.0.tar.gz", hash = "sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2"},
+    {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
+    {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
 ]
 
-[package.dependencies]
-setuptools = "*"
-
 [[package]]
 name = "numpy"
 version = "1.26.4"
@@ -1195,136 +943,61 @@ files = [
 
 [[package]]
 name = "packaging"
-version = "24.0"
+version = "24.1"
 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.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"},
+    {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"},
 ]
 
 [[package]]
-name = "pathspec"
-version = "0.12.1"
-description = "Utility library for gitignore style pattern matching of file paths."
+name = "paginate"
+version = "0.5.6"
+description = "Divides large result sets into pages for easier browsing"
 optional = false
-python-versions = ">=3.8"
+python-versions = "*"
 files = [
-    {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
-    {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
+    {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"},
 ]
 
 [[package]]
-name = "pillow"
-version = "10.3.0"
-description = "Python Imaging Library (Fork)"
+name = "pathspec"
+version = "0.12.1"
+description = "Utility library for gitignore style pattern matching of file paths."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "pillow-10.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:90b9e29824800e90c84e4022dd5cc16eb2d9605ee13f05d47641eb183cd73d45"},
-    {file = "pillow-10.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a2c405445c79c3f5a124573a051062300936b0281fee57637e706453e452746c"},
-    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78618cdbccaa74d3f88d0ad6cb8ac3007f1a6fa5c6f19af64b55ca170bfa1edf"},
-    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:261ddb7ca91fcf71757979534fb4c128448b5b4c55cb6152d280312062f69599"},
-    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:ce49c67f4ea0609933d01c0731b34b8695a7a748d6c8d186f95e7d085d2fe475"},
-    {file = "pillow-10.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b14f16f94cbc61215115b9b1236f9c18403c15dd3c52cf629072afa9d54c1cbf"},
-    {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d33891be6df59d93df4d846640f0e46f1a807339f09e79a8040bc887bdcd7ed3"},
-    {file = "pillow-10.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b50811d664d392f02f7761621303eba9d1b056fb1868c8cdf4231279645c25f5"},
-    {file = "pillow-10.3.0-cp310-cp310-win32.whl", hash = "sha256:ca2870d5d10d8726a27396d3ca4cf7976cec0f3cb706debe88e3a5bd4610f7d2"},
-    {file = "pillow-10.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f0d0591a0aeaefdaf9a5e545e7485f89910c977087e7de2b6c388aec32011e9f"},
-    {file = "pillow-10.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:ccce24b7ad89adb5a1e34a6ba96ac2530046763912806ad4c247356a8f33a67b"},
-    {file = "pillow-10.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:5f77cf66e96ae734717d341c145c5949c63180842a545c47a0ce7ae52ca83795"},
-    {file = "pillow-10.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4b878386c4bf293578b48fc570b84ecfe477d3b77ba39a6e87150af77f40c57"},
-    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fdcbb4068117dfd9ce0138d068ac512843c52295ed996ae6dd1faf537b6dbc27"},
-    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9797a6c8fe16f25749b371c02e2ade0efb51155e767a971c61734b1bf6293994"},
-    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:9e91179a242bbc99be65e139e30690e081fe6cb91a8e77faf4c409653de39451"},
-    {file = "pillow-10.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:1b87bd9d81d179bd8ab871603bd80d8645729939f90b71e62914e816a76fc6bd"},
-    {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:81d09caa7b27ef4e61cb7d8fbf1714f5aec1c6b6c5270ee53504981e6e9121ad"},
-    {file = "pillow-10.3.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:048ad577748b9fa4a99a0548c64f2cb8d672d5bf2e643a739ac8faff1164238c"},
-    {file = "pillow-10.3.0-cp311-cp311-win32.whl", hash = "sha256:7161ec49ef0800947dc5570f86568a7bb36fa97dd09e9827dc02b718c5643f09"},
-    {file = "pillow-10.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8eb0908e954d093b02a543dc963984d6e99ad2b5e36503d8a0aaf040505f747d"},
-    {file = "pillow-10.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:4e6f7d1c414191c1199f8996d3f2282b9ebea0945693fb67392c75a3a320941f"},
-    {file = "pillow-10.3.0-cp312-cp312-macosx_10_10_x86_64.whl", hash = "sha256:e46f38133e5a060d46bd630faa4d9fa0202377495df1f068a8299fd78c84de84"},
-    {file = "pillow-10.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:50b8eae8f7334ec826d6eeffaeeb00e36b5e24aa0b9df322c247539714c6df19"},
-    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d3bea1c75f8c53ee4d505c3e67d8c158ad4df0d83170605b50b64025917f338"},
-    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19aeb96d43902f0a783946a0a87dbdad5c84c936025b8419da0a0cd7724356b1"},
-    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:74d28c17412d9caa1066f7a31df8403ec23d5268ba46cd0ad2c50fb82ae40462"},
-    {file = "pillow-10.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:ff61bfd9253c3915e6d41c651d5f962da23eda633cf02262990094a18a55371a"},
-    {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d886f5d353333b4771d21267c7ecc75b710f1a73d72d03ca06df49b09015a9ef"},
-    {file = "pillow-10.3.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4b5ec25d8b17217d635f8935dbc1b9aa5907962fae29dff220f2659487891cd3"},
-    {file = "pillow-10.3.0-cp312-cp312-win32.whl", hash = "sha256:51243f1ed5161b9945011a7360e997729776f6e5d7005ba0c6879267d4c5139d"},
-    {file = "pillow-10.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:412444afb8c4c7a6cc11a47dade32982439925537e483be7c0ae0cf96c4f6a0b"},
-    {file = "pillow-10.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:798232c92e7665fe82ac085f9d8e8ca98826f8e27859d9a96b41d519ecd2e49a"},
-    {file = "pillow-10.3.0-cp38-cp38-macosx_10_10_x86_64.whl", hash = "sha256:4eaa22f0d22b1a7e93ff0a596d57fdede2e550aecffb5a1ef1106aaece48e96b"},
-    {file = "pillow-10.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cd5e14fbf22a87321b24c88669aad3a51ec052eb145315b3da3b7e3cc105b9a2"},
-    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1530e8f3a4b965eb6a7785cf17a426c779333eb62c9a7d1bbcf3ffd5bf77a4aa"},
-    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5d512aafa1d32efa014fa041d38868fda85028e3f930a96f85d49c7d8ddc0383"},
-    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:339894035d0ede518b16073bdc2feef4c991ee991a29774b33e515f1d308e08d"},
-    {file = "pillow-10.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:aa7e402ce11f0885305bfb6afb3434b3cd8f53b563ac065452d9d5654c7b86fd"},
-    {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:0ea2a783a2bdf2a561808fe4a7a12e9aa3799b701ba305de596bc48b8bdfce9d"},
-    {file = "pillow-10.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c78e1b00a87ce43bb37642c0812315b411e856a905d58d597750eb79802aaaa3"},
-    {file = "pillow-10.3.0-cp38-cp38-win32.whl", hash = "sha256:72d622d262e463dfb7595202d229f5f3ab4b852289a1cd09650362db23b9eb0b"},
-    {file = "pillow-10.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:2034f6759a722da3a3dbd91a81148cf884e91d1b747992ca288ab88c1de15999"},
-    {file = "pillow-10.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:2ed854e716a89b1afcedea551cd85f2eb2a807613752ab997b9974aaa0d56936"},
-    {file = "pillow-10.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dc1a390a82755a8c26c9964d457d4c9cbec5405896cba94cf51f36ea0d855002"},
-    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4203efca580f0dd6f882ca211f923168548f7ba334c189e9eab1178ab840bf60"},
-    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3102045a10945173d38336f6e71a8dc71bcaeed55c3123ad4af82c52807b9375"},
-    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:6fb1b30043271ec92dc65f6d9f0b7a830c210b8a96423074b15c7bc999975f57"},
-    {file = "pillow-10.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:1dfc94946bc60ea375cc39cff0b8da6c7e5f8fcdc1d946beb8da5c216156ddd8"},
-    {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b09b86b27a064c9624d0a6c54da01c1beaf5b6cadfa609cf63789b1d08a797b9"},
-    {file = "pillow-10.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d3b2348a78bc939b4fed6552abfd2e7988e0f81443ef3911a4b8498ca084f6eb"},
-    {file = "pillow-10.3.0-cp39-cp39-win32.whl", hash = "sha256:45ebc7b45406febf07fef35d856f0293a92e7417ae7933207e90bf9090b70572"},
-    {file = "pillow-10.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:0ba26351b137ca4e0db0342d5d00d2e355eb29372c05afd544ebf47c0956ffeb"},
-    {file = "pillow-10.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:50fd3f6b26e3441ae07b7c979309638b72abc1a25da31a81a7fbd9495713ef4f"},
-    {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_10_10_x86_64.whl", hash = "sha256:6b02471b72526ab8a18c39cb7967b72d194ec53c1fd0a70b050565a0f366d355"},
-    {file = "pillow-10.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:8ab74c06ffdab957d7670c2a5a6e1a70181cd10b727cd788c4dd9005b6a8acd9"},
-    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:048eeade4c33fdf7e08da40ef402e748df113fd0b4584e32c4af74fe78baaeb2"},
-    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2ec1e921fd07c7cda7962bad283acc2f2a9ccc1b971ee4b216b75fad6f0463"},
-    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c8e73e99da7db1b4cad7f8d682cf6abad7844da39834c288fbfa394a47bbced"},
-    {file = "pillow-10.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:16563993329b79513f59142a6b02055e10514c1a8e86dca8b48a893e33cf91e3"},
-    {file = "pillow-10.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:dd78700f5788ae180b5ee8902c6aea5a5726bac7c364b202b4b3e3ba2d293170"},
-    {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_10_10_x86_64.whl", hash = "sha256:aff76a55a8aa8364d25400a210a65ff59d0168e0b4285ba6bf2bd83cf675ba32"},
-    {file = "pillow-10.3.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b7bc2176354defba3edc2b9a777744462da2f8e921fbaf61e52acb95bafa9828"},
-    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:793b4e24db2e8742ca6423d3fde8396db336698c55cd34b660663ee9e45ed37f"},
-    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d93480005693d247f8346bc8ee28c72a2191bdf1f6b5db469c096c0c867ac015"},
-    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c83341b89884e2b2e55886e8fbbf37c3fa5efd6c8907124aeb72f285ae5696e5"},
-    {file = "pillow-10.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1a1d1915db1a4fdb2754b9de292642a39a7fb28f1736699527bb649484fb966a"},
-    {file = "pillow-10.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a0eaa93d054751ee9964afa21c06247779b90440ca41d184aeb5d410f20ff591"},
-    {file = "pillow-10.3.0.tar.gz", hash = "sha256:9d2455fbf44c914840c793e89aa82d0e1763a14253a000743719ae5946814b2d"},
+    {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
+    {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
 ]
 
-[package.extras]
-docs = ["furo", "olefile", "sphinx (>=2.4)", "sphinx-copybutton", "sphinx-inline-tabs", "sphinx-removed-in", "sphinxext-opengraph"]
-fpx = ["olefile"]
-mic = ["olefile"]
-tests = ["check-manifest", "coverage", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout"]
-typing = ["typing-extensions"]
-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.2.2"
+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.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
+    {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
 ]
 
 [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)"]
+type = ["mypy (>=1.8)"]
 
 [[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]
@@ -1378,47 +1051,35 @@ test = ["pytest", "pytest-doctestplus (>=0.7)"]
 
 [[package]]
 name = "pygments"
-version = "2.17.2"
+version = "2.18.0"
 description = "Pygments is a syntax highlighting package written in Python."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"},
-    {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"},
+    {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
+    {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
 ]
 
 [package.extras]
-plugins = ["importlib-metadata"]
 windows-terminal = ["colorama (>=0.4.6)"]
 
 [[package]]
 name = "pymdown-extensions"
-version = "9.11"
+version = "10.8.1"
 description = "Extension pack for Python Markdown."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "pymdown_extensions-9.11-py3-none-any.whl", hash = "sha256:a499191d8d869f30339de86fcf072a787e86c42b6f16f280f5c2cf174182b7f3"},
-    {file = "pymdown_extensions-9.11.tar.gz", hash = "sha256:f7e86c1d3981f23d9dc43294488ecb54abadd05b0be4bf8f0e15efc90f7853ff"},
+    {file = "pymdown_extensions-10.8.1-py3-none-any.whl", hash = "sha256:f938326115884f48c6059c67377c46cf631c733ef3629b6eed1349989d1b30cb"},
+    {file = "pymdown_extensions-10.8.1.tar.gz", hash = "sha256:3ab1db5c9e21728dabf75192d71471f8e50f216627e9a1fa9535ecb0231b9940"},
 ]
 
 [package.dependencies]
-markdown = ">=3.2"
+markdown = ">=3.6"
 pyyaml = "*"
 
-[[package]]
-name = "pyparsing"
-version = "3.1.2"
-description = "pyparsing module - Classes and methods to define and execute parsing grammars"
-optional = false
-python-versions = ">=3.6.8"
-files = [
-    {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"},
-    {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"},
-]
-
 [package.extras]
-diagrams = ["jinja2", "railroad-diagrams"]
+extra = ["pygments (>=2.12)"]
 
 [[package]]
 name = "pytest"
@@ -1549,115 +1210,101 @@ pyyaml = "*"
 
 [[package]]
 name = "regex"
-version = "2023.12.25"
+version = "2024.5.15"
 description = "Alternative regular expression module, to replace re."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:0694219a1d54336fd0445ea382d49d36882415c0134ee1e8332afd1529f0baa5"},
-    {file = "regex-2023.12.25-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b014333bd0217ad3d54c143de9d4b9a3ca1c5a29a6d0d554952ea071cff0f1f8"},
-    {file = "regex-2023.12.25-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d865984b3f71f6d0af64d0d88f5733521698f6c16f445bb09ce746c92c97c586"},
-    {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1e0eabac536b4cc7f57a5f3d095bfa557860ab912f25965e08fe1545e2ed8b4c"},
-    {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c25a8ad70e716f96e13a637802813f65d8a6760ef48672aa3502f4c24ea8b400"},
-    {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a9b6d73353f777630626f403b0652055ebfe8ff142a44ec2cf18ae470395766e"},
-    {file = "regex-2023.12.25-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a9cc99d6946d750eb75827cb53c4371b8b0fe89c733a94b1573c9dd16ea6c9e4"},
-    {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88d1f7bef20c721359d8675f7d9f8e414ec5003d8f642fdfd8087777ff7f94b5"},
-    {file = "regex-2023.12.25-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cb3fe77aec8f1995611f966d0c656fdce398317f850d0e6e7aebdfe61f40e1cd"},
-    {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7aa47c2e9ea33a4a2a05f40fcd3ea36d73853a2aae7b4feab6fc85f8bf2c9704"},
-    {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:df26481f0c7a3f8739fecb3e81bc9da3fcfae34d6c094563b9d4670b047312e1"},
-    {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:c40281f7d70baf6e0db0c2f7472b31609f5bc2748fe7275ea65a0b4601d9b392"},
-    {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:d94a1db462d5690ebf6ae86d11c5e420042b9898af5dcf278bd97d6bda065423"},
-    {file = "regex-2023.12.25-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ba1b30765a55acf15dce3f364e4928b80858fa8f979ad41f862358939bdd1f2f"},
-    {file = "regex-2023.12.25-cp310-cp310-win32.whl", hash = "sha256:150c39f5b964e4d7dba46a7962a088fbc91f06e606f023ce57bb347a3b2d4630"},
-    {file = "regex-2023.12.25-cp310-cp310-win_amd64.whl", hash = "sha256:09da66917262d9481c719599116c7dc0c321ffcec4b1f510c4f8a066f8768105"},
-    {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1b9d811f72210fa9306aeb88385b8f8bcef0dfbf3873410413c00aa94c56c2b6"},
-    {file = "regex-2023.12.25-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d902a43085a308cef32c0d3aea962524b725403fd9373dea18110904003bac97"},
-    {file = "regex-2023.12.25-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d166eafc19f4718df38887b2bbe1467a4f74a9830e8605089ea7a30dd4da8887"},
-    {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7ad32824b7f02bb3c9f80306d405a1d9b7bb89362d68b3c5a9be53836caebdb"},
-    {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:636ba0a77de609d6510235b7f0e77ec494d2657108f777e8765efc060094c98c"},
-    {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fda75704357805eb953a3ee15a2b240694a9a514548cd49b3c5124b4e2ad01b"},
-    {file = "regex-2023.12.25-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f72cbae7f6b01591f90814250e636065850c5926751af02bb48da94dfced7baa"},
-    {file = "regex-2023.12.25-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:db2a0b1857f18b11e3b0e54ddfefc96af46b0896fb678c85f63fb8c37518b3e7"},
-    {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7502534e55c7c36c0978c91ba6f61703faf7ce733715ca48f499d3dbbd7657e0"},
-    {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:e8c7e08bb566de4faaf11984af13f6bcf6a08f327b13631d41d62592681d24fe"},
-    {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:283fc8eed679758de38fe493b7d7d84a198b558942b03f017b1f94dda8efae80"},
-    {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:f44dd4d68697559d007462b0a3a1d9acd61d97072b71f6d1968daef26bc744bd"},
-    {file = "regex-2023.12.25-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:67d3ccfc590e5e7197750fcb3a2915b416a53e2de847a728cfa60141054123d4"},
-    {file = "regex-2023.12.25-cp311-cp311-win32.whl", hash = "sha256:68191f80a9bad283432385961d9efe09d783bcd36ed35a60fb1ff3f1ec2efe87"},
-    {file = "regex-2023.12.25-cp311-cp311-win_amd64.whl", hash = "sha256:7d2af3f6b8419661a0c421584cfe8aaec1c0e435ce7e47ee2a97e344b98f794f"},
-    {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8a0ccf52bb37d1a700375a6b395bff5dd15c50acb745f7db30415bae3c2b0715"},
-    {file = "regex-2023.12.25-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c3c4a78615b7762740531c27cf46e2f388d8d727d0c0c739e72048beb26c8a9d"},
-    {file = "regex-2023.12.25-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ad83e7545b4ab69216cef4cc47e344d19622e28aabec61574b20257c65466d6a"},
-    {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b7a635871143661feccce3979e1727c4e094f2bdfd3ec4b90dfd4f16f571a87a"},
-    {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d498eea3f581fbe1b34b59c697512a8baef88212f92e4c7830fcc1499f5b45a5"},
-    {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:43f7cd5754d02a56ae4ebb91b33461dc67be8e3e0153f593c509e21d219c5060"},
-    {file = "regex-2023.12.25-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51f4b32f793812714fd5307222a7f77e739b9bc566dc94a18126aba3b92b98a3"},
-    {file = "regex-2023.12.25-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ba99d8077424501b9616b43a2d208095746fb1284fc5ba490139651f971d39d9"},
-    {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4bfc2b16e3ba8850e0e262467275dd4d62f0d045e0e9eda2bc65078c0110a11f"},
-    {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8c2c19dae8a3eb0ea45a8448356ed561be843b13cbc34b840922ddf565498c1c"},
-    {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:60080bb3d8617d96f0fb7e19796384cc2467447ef1c491694850ebd3670bc457"},
-    {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b77e27b79448e34c2c51c09836033056a0547aa360c45eeeb67803da7b0eedaf"},
-    {file = "regex-2023.12.25-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:518440c991f514331f4850a63560321f833979d145d7d81186dbe2f19e27ae3d"},
-    {file = "regex-2023.12.25-cp312-cp312-win32.whl", hash = "sha256:e2610e9406d3b0073636a3a2e80db05a02f0c3169b5632022b4e81c0364bcda5"},
-    {file = "regex-2023.12.25-cp312-cp312-win_amd64.whl", hash = "sha256:cc37b9aeebab425f11f27e5e9e6cf580be7206c6582a64467a14dda211abc232"},
-    {file = "regex-2023.12.25-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:da695d75ac97cb1cd725adac136d25ca687da4536154cdc2815f576e4da11c69"},
-    {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d126361607b33c4eb7b36debc173bf25d7805847346dd4d99b5499e1fef52bc7"},
-    {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4719bb05094d7d8563a450cf8738d2e1061420f79cfcc1fa7f0a44744c4d8f73"},
-    {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5dd58946bce44b53b06d94aa95560d0b243eb2fe64227cba50017a8d8b3cd3e2"},
-    {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22a86d9fff2009302c440b9d799ef2fe322416d2d58fc124b926aa89365ec482"},
-    {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2aae8101919e8aa05ecfe6322b278f41ce2994c4a430303c4cd163fef746e04f"},
-    {file = "regex-2023.12.25-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:e692296c4cc2873967771345a876bcfc1c547e8dd695c6b89342488b0ea55cd8"},
-    {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:263ef5cc10979837f243950637fffb06e8daed7f1ac1e39d5910fd29929e489a"},
-    {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d6f7e255e5fa94642a0724e35406e6cb7001c09d476ab5fce002f652b36d0c39"},
-    {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:88ad44e220e22b63b0f8f81f007e8abbb92874d8ced66f32571ef8beb0643b2b"},
-    {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3a17d3ede18f9cedcbe23d2daa8a2cd6f59fe2bf082c567e43083bba3fb00347"},
-    {file = "regex-2023.12.25-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:d15b274f9e15b1a0b7a45d2ac86d1f634d983ca40d6b886721626c47a400bf39"},
-    {file = "regex-2023.12.25-cp37-cp37m-win32.whl", hash = "sha256:ed19b3a05ae0c97dd8f75a5d8f21f7723a8c33bbc555da6bbe1f96c470139d3c"},
-    {file = "regex-2023.12.25-cp37-cp37m-win_amd64.whl", hash = "sha256:a6d1047952c0b8104a1d371f88f4ab62e6275567d4458c1e26e9627ad489b445"},
-    {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:b43523d7bc2abd757119dbfb38af91b5735eea45537ec6ec3a5ec3f9562a1c53"},
-    {file = "regex-2023.12.25-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:efb2d82f33b2212898f1659fb1c2e9ac30493ac41e4d53123da374c3b5541e64"},
-    {file = "regex-2023.12.25-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b7fca9205b59c1a3d5031f7e64ed627a1074730a51c2a80e97653e3e9fa0d415"},
-    {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086dd15e9435b393ae06f96ab69ab2d333f5d65cbe65ca5a3ef0ec9564dfe770"},
-    {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e81469f7d01efed9b53740aedd26085f20d49da65f9c1f41e822a33992cb1590"},
-    {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:34e4af5b27232f68042aa40a91c3b9bb4da0eeb31b7632e0091afc4310afe6cb"},
-    {file = "regex-2023.12.25-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9852b76ab558e45b20bf1893b59af64a28bd3820b0c2efc80e0a70a4a3ea51c1"},
-    {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff100b203092af77d1a5a7abe085b3506b7eaaf9abf65b73b7d6905b6cb76988"},
-    {file = "regex-2023.12.25-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cc038b2d8b1470364b1888a98fd22d616fba2b6309c5b5f181ad4483e0017861"},
-    {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:094ba386bb5c01e54e14434d4caabf6583334090865b23ef58e0424a6286d3dc"},
-    {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:5cd05d0f57846d8ba4b71d9c00f6f37d6b97d5e5ef8b3c3840426a475c8f70f4"},
-    {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:9aa1a67bbf0f957bbe096375887b2505f5d8ae16bf04488e8b0f334c36e31360"},
-    {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:98a2636994f943b871786c9e82bfe7883ecdaba2ef5df54e1450fa9869d1f756"},
-    {file = "regex-2023.12.25-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:37f8e93a81fc5e5bd8db7e10e62dc64261bcd88f8d7e6640aaebe9bc180d9ce2"},
-    {file = "regex-2023.12.25-cp38-cp38-win32.whl", hash = "sha256:d78bd484930c1da2b9679290a41cdb25cc127d783768a0369d6b449e72f88beb"},
-    {file = "regex-2023.12.25-cp38-cp38-win_amd64.whl", hash = "sha256:b521dcecebc5b978b447f0f69b5b7f3840eac454862270406a39837ffae4e697"},
-    {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f7bc09bc9c29ebead055bcba136a67378f03d66bf359e87d0f7c759d6d4ffa31"},
-    {file = "regex-2023.12.25-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e14b73607d6231f3cc4622809c196b540a6a44e903bcfad940779c80dffa7be7"},
-    {file = "regex-2023.12.25-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9eda5f7a50141291beda3edd00abc2d4a5b16c29c92daf8d5bd76934150f3edc"},
-    {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cc6bb9aa69aacf0f6032c307da718f61a40cf970849e471254e0e91c56ffca95"},
-    {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:298dc6354d414bc921581be85695d18912bea163a8b23cac9a2562bbcd5088b1"},
-    {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f4e475a80ecbd15896a976aa0b386c5525d0ed34d5c600b6d3ebac0a67c7ddf"},
-    {file = "regex-2023.12.25-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:531ac6cf22b53e0696f8e1d56ce2396311254eb806111ddd3922c9d937151dae"},
-    {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22f3470f7524b6da61e2020672df2f3063676aff444db1daa283c2ea4ed259d6"},
-    {file = "regex-2023.12.25-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:89723d2112697feaa320c9d351e5f5e7b841e83f8b143dba8e2d2b5f04e10923"},
-    {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0ecf44ddf9171cd7566ef1768047f6e66975788258b1c6c6ca78098b95cf9a3d"},
-    {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:905466ad1702ed4acfd67a902af50b8db1feeb9781436372261808df7a2a7bca"},
-    {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:4558410b7a5607a645e9804a3e9dd509af12fb72b9825b13791a37cd417d73a5"},
-    {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:7e316026cc1095f2a3e8cc012822c99f413b702eaa2ca5408a513609488cb62f"},
-    {file = "regex-2023.12.25-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3b1de218d5375cd6ac4b5493e0b9f3df2be331e86520f23382f216c137913d20"},
-    {file = "regex-2023.12.25-cp39-cp39-win32.whl", hash = "sha256:11a963f8e25ab5c61348d090bf1b07f1953929c13bd2309a0662e9ff680763c9"},
-    {file = "regex-2023.12.25-cp39-cp39-win_amd64.whl", hash = "sha256:e693e233ac92ba83a87024e1d32b5f9ab15ca55ddd916d878146f4e3406b5c91"},
-    {file = "regex-2023.12.25.tar.gz", hash = "sha256:29171aa128da69afdf4bde412d5bedc335f2ca8fcfe4489038577d05f16181e5"},
+    {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a81e3cfbae20378d75185171587cbf756015ccb14840702944f014e0d93ea09f"},
+    {file = "regex-2024.5.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7b59138b219ffa8979013be7bc85bb60c6f7b7575df3d56dc1e403a438c7a3f6"},
+    {file = "regex-2024.5.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0bd000c6e266927cb7a1bc39d55be95c4b4f65c5be53e659537537e019232b1"},
+    {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5eaa7ddaf517aa095fa8da0b5015c44d03da83f5bd49c87961e3c997daed0de7"},
+    {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ba68168daedb2c0bab7fd7e00ced5ba90aebf91024dea3c88ad5063c2a562cca"},
+    {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6e8d717bca3a6e2064fc3a08df5cbe366369f4b052dcd21b7416e6d71620dca1"},
+    {file = "regex-2024.5.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1337b7dbef9b2f71121cdbf1e97e40de33ff114801263b275aafd75303bd62b5"},
+    {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f9ebd0a36102fcad2f03696e8af4ae682793a5d30b46c647eaf280d6cfb32796"},
+    {file = "regex-2024.5.15-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:9efa1a32ad3a3ea112224897cdaeb6aa00381627f567179c0314f7b65d354c62"},
+    {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1595f2d10dff3d805e054ebdc41c124753631b6a471b976963c7b28543cf13b0"},
+    {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b802512f3e1f480f41ab5f2cfc0e2f761f08a1f41092d6718868082fc0d27143"},
+    {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:a0981022dccabca811e8171f913de05720590c915b033b7e601f35ce4ea7019f"},
+    {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:19068a6a79cf99a19ccefa44610491e9ca02c2be3305c7760d3831d38a467a6f"},
+    {file = "regex-2024.5.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:1b5269484f6126eee5e687785e83c6b60aad7663dafe842b34691157e5083e53"},
+    {file = "regex-2024.5.15-cp310-cp310-win32.whl", hash = "sha256:ada150c5adfa8fbcbf321c30c751dc67d2f12f15bd183ffe4ec7cde351d945b3"},
+    {file = "regex-2024.5.15-cp310-cp310-win_amd64.whl", hash = "sha256:ac394ff680fc46b97487941f5e6ae49a9f30ea41c6c6804832063f14b2a5a145"},
+    {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:f5b1dff3ad008dccf18e652283f5e5339d70bf8ba7c98bf848ac33db10f7bc7a"},
+    {file = "regex-2024.5.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c6a2b494a76983df8e3d3feea9b9ffdd558b247e60b92f877f93a1ff43d26656"},
+    {file = "regex-2024.5.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a32b96f15c8ab2e7d27655969a23895eb799de3665fa94349f3b2fbfd547236f"},
+    {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10002e86e6068d9e1c91eae8295ef690f02f913c57db120b58fdd35a6bb1af35"},
+    {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec54d5afa89c19c6dd8541a133be51ee1017a38b412b1321ccb8d6ddbeb4cf7d"},
+    {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:10e4ce0dca9ae7a66e6089bb29355d4432caed736acae36fef0fdd7879f0b0cb"},
+    {file = "regex-2024.5.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e507ff1e74373c4d3038195fdd2af30d297b4f0950eeda6f515ae3d84a1770f"},
+    {file = "regex-2024.5.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d1f059a4d795e646e1c37665b9d06062c62d0e8cc3c511fe01315973a6542e40"},
+    {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0721931ad5fe0dda45d07f9820b90b2148ccdd8e45bb9e9b42a146cb4f695649"},
+    {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:833616ddc75ad595dee848ad984d067f2f31be645d603e4d158bba656bbf516c"},
+    {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:287eb7f54fc81546346207c533ad3c2c51a8d61075127d7f6d79aaf96cdee890"},
+    {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:19dfb1c504781a136a80ecd1fff9f16dddf5bb43cec6871778c8a907a085bb3d"},
+    {file = "regex-2024.5.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:119af6e56dce35e8dfb5222573b50c89e5508d94d55713c75126b753f834de68"},
+    {file = "regex-2024.5.15-cp311-cp311-win32.whl", hash = "sha256:1c1c174d6ec38d6c8a7504087358ce9213d4332f6293a94fbf5249992ba54efa"},
+    {file = "regex-2024.5.15-cp311-cp311-win_amd64.whl", hash = "sha256:9e717956dcfd656f5055cc70996ee2cc82ac5149517fc8e1b60261b907740201"},
+    {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:632b01153e5248c134007209b5c6348a544ce96c46005d8456de1d552455b014"},
+    {file = "regex-2024.5.15-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e64198f6b856d48192bf921421fdd8ad8eb35e179086e99e99f711957ffedd6e"},
+    {file = "regex-2024.5.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68811ab14087b2f6e0fc0c2bae9ad689ea3584cad6917fc57be6a48bbd012c49"},
+    {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8ec0c2fea1e886a19c3bee0cd19d862b3aa75dcdfb42ebe8ed30708df64687a"},
+    {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d0c0c0003c10f54a591d220997dd27d953cd9ccc1a7294b40a4be5312be8797b"},
+    {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2431b9e263af1953c55abbd3e2efca67ca80a3de8a0437cb58e2421f8184717a"},
+    {file = "regex-2024.5.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a605586358893b483976cffc1723fb0f83e526e8f14c6e6614e75919d9862cf"},
+    {file = "regex-2024.5.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391d7f7f1e409d192dba8bcd42d3e4cf9e598f3979cdaed6ab11288da88cb9f2"},
+    {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9ff11639a8d98969c863d4617595eb5425fd12f7c5ef6621a4b74b71ed8726d5"},
+    {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4eee78a04e6c67e8391edd4dad3279828dd66ac4b79570ec998e2155d2e59fd5"},
+    {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8fe45aa3f4aa57faabbc9cb46a93363edd6197cbc43523daea044e9ff2fea83e"},
+    {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:d0a3d8d6acf0c78a1fff0e210d224b821081330b8524e3e2bc5a68ef6ab5803d"},
+    {file = "regex-2024.5.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c486b4106066d502495b3025a0a7251bf37ea9540433940a23419461ab9f2a80"},
+    {file = "regex-2024.5.15-cp312-cp312-win32.whl", hash = "sha256:c49e15eac7c149f3670b3e27f1f28a2c1ddeccd3a2812cba953e01be2ab9b5fe"},
+    {file = "regex-2024.5.15-cp312-cp312-win_amd64.whl", hash = "sha256:673b5a6da4557b975c6c90198588181029c60793835ce02f497ea817ff647cb2"},
+    {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:87e2a9c29e672fc65523fb47a90d429b70ef72b901b4e4b1bd42387caf0d6835"},
+    {file = "regex-2024.5.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c3bea0ba8b73b71b37ac833a7f3fd53825924165da6a924aec78c13032f20850"},
+    {file = "regex-2024.5.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bfc4f82cabe54f1e7f206fd3d30fda143f84a63fe7d64a81558d6e5f2e5aaba9"},
+    {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5bb9425fe881d578aeca0b2b4b3d314ec88738706f66f219c194d67179337cb"},
+    {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64c65783e96e563103d641760664125e91bd85d8e49566ee560ded4da0d3e704"},
+    {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cf2430df4148b08fb4324b848672514b1385ae3807651f3567871f130a728cc3"},
+    {file = "regex-2024.5.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5397de3219a8b08ae9540c48f602996aa6b0b65d5a61683e233af8605c42b0f2"},
+    {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:455705d34b4154a80ead722f4f185b04c4237e8e8e33f265cd0798d0e44825fa"},
+    {file = "regex-2024.5.15-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b2b6f1b3bb6f640c1a92be3bbfbcb18657b125b99ecf141fb3310b5282c7d4ed"},
+    {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:3ad070b823ca5890cab606c940522d05d3d22395d432f4aaaf9d5b1653e47ced"},
+    {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:5b5467acbfc153847d5adb21e21e29847bcb5870e65c94c9206d20eb4e99a384"},
+    {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:e6662686aeb633ad65be2a42b4cb00178b3fbf7b91878f9446075c404ada552f"},
+    {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:2b4c884767504c0e2401babe8b5b7aea9148680d2e157fa28f01529d1f7fcf67"},
+    {file = "regex-2024.5.15-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:3cd7874d57f13bf70078f1ff02b8b0aa48d5b9ed25fc48547516c6aba36f5741"},
+    {file = "regex-2024.5.15-cp38-cp38-win32.whl", hash = "sha256:e4682f5ba31f475d58884045c1a97a860a007d44938c4c0895f41d64481edbc9"},
+    {file = "regex-2024.5.15-cp38-cp38-win_amd64.whl", hash = "sha256:d99ceffa25ac45d150e30bd9ed14ec6039f2aad0ffa6bb87a5936f5782fc1569"},
+    {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13cdaf31bed30a1e1c2453ef6015aa0983e1366fad2667657dbcac7b02f67133"},
+    {file = "regex-2024.5.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cac27dcaa821ca271855a32188aa61d12decb6fe45ffe3e722401fe61e323cd1"},
+    {file = "regex-2024.5.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7dbe2467273b875ea2de38ded4eba86cbcbc9a1a6d0aa11dcf7bd2e67859c435"},
+    {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f18a9a3513a99c4bef0e3efd4c4a5b11228b48aa80743be822b71e132ae4f5"},
+    {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d347a741ea871c2e278fde6c48f85136c96b8659b632fb57a7d1ce1872547600"},
+    {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1878b8301ed011704aea4c806a3cadbd76f84dece1ec09cc9e4dc934cfa5d4da"},
+    {file = "regex-2024.5.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4babf07ad476aaf7830d77000874d7611704a7fcf68c9c2ad151f5d94ae4bfc4"},
+    {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:35cb514e137cb3488bce23352af3e12fb0dbedd1ee6e60da053c69fb1b29cc6c"},
+    {file = "regex-2024.5.15-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:cdd09d47c0b2efee9378679f8510ee6955d329424c659ab3c5e3a6edea696294"},
+    {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:72d7a99cd6b8f958e85fc6ca5b37c4303294954eac1376535b03c2a43eb72629"},
+    {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:a094801d379ab20c2135529948cb84d417a2169b9bdceda2a36f5f10977ebc16"},
+    {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:c0c18345010870e58238790a6779a1219b4d97bd2e77e1140e8ee5d14df071aa"},
+    {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:16093f563098448ff6b1fa68170e4acbef94e6b6a4e25e10eae8598bb1694b5d"},
+    {file = "regex-2024.5.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:e38a7d4e8f633a33b4c7350fbd8bad3b70bf81439ac67ac38916c4a86b465456"},
+    {file = "regex-2024.5.15-cp39-cp39-win32.whl", hash = "sha256:71a455a3c584a88f654b64feccc1e25876066c4f5ef26cd6dd711308aa538694"},
+    {file = "regex-2024.5.15-cp39-cp39-win_amd64.whl", hash = "sha256:cab12877a9bdafde5500206d1020a584355a97884dfd388af3699e9137bf7388"},
+    {file = "regex-2024.5.15.tar.gz", hash = "sha256:d3ee02d9e5f482cc8309134a91eeaacbdd2261ba111b0fef3748eeb4913e6a2c"},
 ]
 
 [[package]]
 name = "requests"
-version = "2.31.0"
+version = "2.32.3"
 description = "Python HTTP for Humans."
 optional = false
-python-versions = ">=3.7"
+python-versions = ">=3.8"
 files = [
-    {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"},
-    {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"},
+    {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
+    {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
 ]
 
 [package.dependencies]
@@ -1698,36 +1345,36 @@ files = [
 
 [[package]]
 name = "scipy"
-version = "1.13.0"
+version = "1.13.1"
 description = "Fundamental algorithms for scientific computing in Python"
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "scipy-1.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba419578ab343a4e0a77c0ef82f088238a93eef141b2b8017e46149776dfad4d"},
-    {file = "scipy-1.13.0-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:22789b56a999265431c417d462e5b7f2b487e831ca7bef5edeb56efe4c93f86e"},
-    {file = "scipy-1.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05f1432ba070e90d42d7fd836462c50bf98bd08bed0aa616c359eed8a04e3922"},
-    {file = "scipy-1.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8434f6f3fa49f631fae84afee424e2483289dfc30a47755b4b4e6b07b2633a4"},
-    {file = "scipy-1.13.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:dcbb9ea49b0167de4167c40eeee6e167caeef11effb0670b554d10b1e693a8b9"},
-    {file = "scipy-1.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:1d2f7bb14c178f8b13ebae93f67e42b0a6b0fc50eba1cd8021c9b6e08e8fb1cd"},
-    {file = "scipy-1.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fbcf8abaf5aa2dc8d6400566c1a727aed338b5fe880cde64907596a89d576fa"},
-    {file = "scipy-1.13.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:5e4a756355522eb60fcd61f8372ac2549073c8788f6114449b37e9e8104f15a5"},
-    {file = "scipy-1.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5acd8e1dbd8dbe38d0004b1497019b2dbbc3d70691e65d69615f8a7292865d7"},
-    {file = "scipy-1.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ff7dad5d24a8045d836671e082a490848e8639cabb3dbdacb29f943a678683d"},
-    {file = "scipy-1.13.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4dca18c3ffee287ddd3bc8f1dabaf45f5305c5afc9f8ab9cbfab855e70b2df5c"},
-    {file = "scipy-1.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:a2f471de4d01200718b2b8927f7d76b5d9bde18047ea0fa8bd15c5ba3f26a1d6"},
-    {file = "scipy-1.13.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0de696f589681c2802f9090fff730c218f7c51ff49bf252b6a97ec4a5d19e8b"},
-    {file = "scipy-1.13.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:b2a3ff461ec4756b7e8e42e1c681077349a038f0686132d623fa404c0bee2551"},
-    {file = "scipy-1.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6bf9fe63e7a4bf01d3645b13ff2aa6dea023d38993f42aaac81a18b1bda7a82a"},
-    {file = "scipy-1.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e7626dfd91cdea5714f343ce1176b6c4745155d234f1033584154f60ef1ff42"},
-    {file = "scipy-1.13.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:109d391d720fcebf2fbe008621952b08e52907cf4c8c7efc7376822151820820"},
-    {file = "scipy-1.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:8930ae3ea371d6b91c203b1032b9600d69c568e537b7988a3073dfe4d4774f21"},
-    {file = "scipy-1.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5407708195cb38d70fd2d6bb04b1b9dd5c92297d86e9f9daae1576bd9e06f602"},
-    {file = "scipy-1.13.0-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:ac38c4c92951ac0f729c4c48c9e13eb3675d9986cc0c83943784d7390d540c78"},
-    {file = "scipy-1.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09c74543c4fbeb67af6ce457f6a6a28e5d3739a87f62412e4a16e46f164f0ae5"},
-    {file = "scipy-1.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:28e286bf9ac422d6beb559bc61312c348ca9b0f0dae0d7c5afde7f722d6ea13d"},
-    {file = "scipy-1.13.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:33fde20efc380bd23a78a4d26d59fc8704e9b5fd9b08841693eb46716ba13d86"},
-    {file = "scipy-1.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:45c08bec71d3546d606989ba6e7daa6f0992918171e2a6f7fbedfa7361c2de1e"},
-    {file = "scipy-1.13.0.tar.gz", hash = "sha256:58569af537ea29d3f78e5abd18398459f195546bb3be23d16677fb26616cc11e"},
+    {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]
@@ -1738,22 +1385,6 @@ dev = ["cython-lint (>=0.12.2)", "doit (>=0.36.0)", "mypy", "pycodestyle", "pyde
 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 = "setuptools"
-version = "69.2.0"
-description = "Easily download, build, install, upgrade, and uninstall Python packages"
-optional = false
-python-versions = ">=3.8"
-files = [
-    {file = "setuptools-69.2.0-py3-none-any.whl", hash = "sha256:c21c49fb1042386df081cb5d86759792ab89efca84cf114889191cd09aacc80c"},
-    {file = "setuptools-69.2.0.tar.gz", hash = "sha256:0ff4183f8f42cd8fa3acea16c45205521a4ef28f73c6391d8a25e92893134f2e"},
-]
-
-[package.extras]
-docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"]
-testing = ["build[virtualenv]", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mypy (==1.9)", "packaging (>=23.2)", "pip (>=19.1)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff (>=0.2.1)", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"]
-testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.2)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"]
-
 [[package]]
 name = "six"
 version = "1.16.0"
@@ -1789,24 +1420,24 @@ files = [
 
 [[package]]
 name = "typing-extensions"
-version = "4.11.0"
+version = "4.12.2"
 description = "Backported and Experimental Type Hints for Python 3.8+"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"},
-    {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"},
+    {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"},
+    {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"},
 ]
 
 [[package]]
 name = "urllib3"
-version = "2.2.1"
+version = "2.2.2"
 description = "HTTP library with thread-safe connection pooling, file post, and more."
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"},
-    {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"},
+    {file = "urllib3-2.2.2-py3-none-any.whl", hash = "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472"},
+    {file = "urllib3-2.2.2.tar.gz", hash = "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168"},
 ]
 
 [package.extras]
@@ -1817,13 +1448,13 @@ zstd = ["zstandard (>=0.18.0)"]
 
 [[package]]
 name = "virtualenv"
-version = "20.25.1"
+version = "20.26.3"
 description = "Virtual Python Environment builder"
 optional = false
 python-versions = ">=3.7"
 files = [
-    {file = "virtualenv-20.25.1-py3-none-any.whl", hash = "sha256:961c026ac520bac5f69acb8ea063e8a4f071bcc9457b9c1f28f6b085c511583a"},
-    {file = "virtualenv-20.25.1.tar.gz", hash = "sha256:e08e13ecdca7a0bd53798f356d5831434afa5b07b93f0abdf0797b7a06ffe197"},
+    {file = "virtualenv-20.26.3-py3-none-any.whl", hash = "sha256:8cc4a31139e796e9a7de2cd5cf2489de1217193116a8fd42328f1bd65f434589"},
+    {file = "virtualenv-20.26.3.tar.gz", hash = "sha256:4c43a2a236279d9ea36a0d76f98d84bd6ca94ac4e0f4a3b9d46d05e10fea542a"},
 ]
 
 [package.dependencies]
@@ -1832,45 +1463,48 @@ filelock = ">=3.12.2,<4"
 platformdirs = ">=3.9.1,<5"
 
 [package.extras]
-docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
 test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
 
 [[package]]
 name = "watchdog"
-version = "4.0.0"
+version = "4.0.1"
 description = "Filesystem events monitoring"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"},
-    {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"},
-    {file = "watchdog-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c"},
-    {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b"},
-    {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935"},
-    {file = "watchdog-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b"},
-    {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257"},
-    {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19"},
-    {file = "watchdog-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b"},
-    {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85"},
-    {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4"},
-    {file = "watchdog-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605"},
-    {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101"},
-    {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca"},
-    {file = "watchdog-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8"},
-    {file = "watchdog-4.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b"},
-    {file = "watchdog-4.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92"},
-    {file = "watchdog-4.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07"},
-    {file = "watchdog-4.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3"},
-    {file = "watchdog-4.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f"},
-    {file = "watchdog-4.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50"},
-    {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927"},
-    {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"},
-    {file = "watchdog-4.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87"},
-    {file = "watchdog-4.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269"},
-    {file = "watchdog-4.0.0-py3-none-win32.whl", hash = "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c"},
-    {file = "watchdog-4.0.0-py3-none-win_amd64.whl", hash = "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245"},
-    {file = "watchdog-4.0.0-py3-none-win_ia64.whl", hash = "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7"},
-    {file = "watchdog-4.0.0.tar.gz", hash = "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec"},
+    {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:da2dfdaa8006eb6a71051795856bedd97e5b03e57da96f98e375682c48850645"},
+    {file = "watchdog-4.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e93f451f2dfa433d97765ca2634628b789b49ba8b504fdde5837cdcf25fdb53b"},
+    {file = "watchdog-4.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ef0107bbb6a55f5be727cfc2ef945d5676b97bffb8425650dadbb184be9f9a2b"},
+    {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:17e32f147d8bf9657e0922c0940bcde863b894cd871dbb694beb6704cfbd2fb5"},
+    {file = "watchdog-4.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:03e70d2df2258fb6cb0e95bbdbe06c16e608af94a3ffbd2b90c3f1e83eb10767"},
+    {file = "watchdog-4.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123587af84260c991dc5f62a6e7ef3d1c57dfddc99faacee508c71d287248459"},
+    {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:093b23e6906a8b97051191a4a0c73a77ecc958121d42346274c6af6520dec175"},
+    {file = "watchdog-4.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:611be3904f9843f0529c35a3ff3fd617449463cb4b73b1633950b3d97fa4bfb7"},
+    {file = "watchdog-4.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:62c613ad689ddcb11707f030e722fa929f322ef7e4f18f5335d2b73c61a85c28"},
+    {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d4925e4bf7b9bddd1c3de13c9b8a2cdb89a468f640e66fbfabaf735bd85b3e35"},
+    {file = "watchdog-4.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cad0bbd66cd59fc474b4a4376bc5ac3fc698723510cbb64091c2a793b18654db"},
+    {file = "watchdog-4.0.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a3c2c317a8fb53e5b3d25790553796105501a235343f5d2bf23bb8649c2c8709"},
+    {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c9904904b6564d4ee8a1ed820db76185a3c96e05560c776c79a6ce5ab71888ba"},
+    {file = "watchdog-4.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:667f3c579e813fcbad1b784db7a1aaa96524bed53437e119f6a2f5de4db04235"},
+    {file = "watchdog-4.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:d10a681c9a1d5a77e75c48a3b8e1a9f2ae2928eda463e8d33660437705659682"},
+    {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0144c0ea9997b92615af1d94afc0c217e07ce2c14912c7b1a5731776329fcfc7"},
+    {file = "watchdog-4.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:998d2be6976a0ee3a81fb8e2777900c28641fb5bfbd0c84717d89bca0addcdc5"},
+    {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:e7921319fe4430b11278d924ef66d4daa469fafb1da679a2e48c935fa27af193"},
+    {file = "watchdog-4.0.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f0de0f284248ab40188f23380b03b59126d1479cd59940f2a34f8852db710625"},
+    {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:bca36be5707e81b9e6ce3208d92d95540d4ca244c006b61511753583c81c70dd"},
+    {file = "watchdog-4.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:ab998f567ebdf6b1da7dc1e5accfaa7c6992244629c0fdaef062f43249bd8dee"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_aarch64.whl", hash = "sha256:dddba7ca1c807045323b6af4ff80f5ddc4d654c8bce8317dde1bd96b128ed253"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_armv7l.whl", hash = "sha256:4513ec234c68b14d4161440e07f995f231be21a09329051e67a2118a7a612d2d"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_i686.whl", hash = "sha256:4107ac5ab936a63952dea2a46a734a23230aa2f6f9db1291bf171dac3ebd53c6"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64.whl", hash = "sha256:6e8c70d2cd745daec2a08734d9f63092b793ad97612470a0ee4cbb8f5f705c57"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f27279d060e2ab24c0aa98363ff906d2386aa6c4dc2f1a374655d4e02a6c5e5e"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_s390x.whl", hash = "sha256:f8affdf3c0f0466e69f5b3917cdd042f89c8c63aebdb9f7c078996f607cdb0f5"},
+    {file = "watchdog-4.0.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:ac7041b385f04c047fcc2951dc001671dee1b7e0615cde772e84b01fbf68ee84"},
+    {file = "watchdog-4.0.1-py3-none-win32.whl", hash = "sha256:206afc3d964f9a233e6ad34618ec60b9837d0582b500b63687e34011e15bb429"},
+    {file = "watchdog-4.0.1-py3-none-win_amd64.whl", hash = "sha256:7577b3c43e5909623149f76b099ac49a1a01ca4e167d1785c76eb52fa585745a"},
+    {file = "watchdog-4.0.1-py3-none-win_ia64.whl", hash = "sha256:d7b9f5f3299e8dd230880b6c55504a1f69cf1e4316275d1b215ebdd8187ec88d"},
+    {file = "watchdog-4.0.1.tar.gz", hash = "sha256:eebaacf674fa25511e8867028d281e602ee6500045b57f43b08778082f7f8b44"},
 ]
 
 [package.extras]
@@ -1878,20 +1512,20 @@ watchmedo = ["PyYAML (>=3.10)"]
 
 [[package]]
 name = "zipp"
-version = "3.18.1"
+version = "3.19.2"
 description = "Backport of pathlib-compatible object wrapper for zip files"
 optional = false
 python-versions = ">=3.8"
 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.19.2-py3-none-any.whl", hash = "sha256:f091755f667055f2d02b32c53771a7a6c8b47e1fdbc4b72a8b9072b3eef8015c"},
+    {file = "zipp-3.19.2.tar.gz", hash = "sha256:bf1dcf6450f873a13e952a29504887c89e6de7506209e5b1bcc3460135d4de19"},
 ]
 
 [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)"]
+doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
+test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"]
 
 [metadata]
 lock-version = "2.0"
 python-versions = ">=3.9"
-content-hash = "e8cbfe076ced3accc312f14cb6b0b4a45bc46cd36b87c1027074b45dce5f6ea7"
\ No newline at end of file
+content-hash = "8e0c958ac0f3adc48589ad778b182822a7d7f7e5661fa7e5c8aadca29a190aab"
diff --git a/pyproject.toml b/pyproject.toml
index af76c4c..dbcdb6f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [tool.poetry]
 name = "zodipy"
-version = "0.9.2"
+version = "1.0.0"
 description = "A Python package for zodiacal light simulations"
 authors = ["Metin San <metinisan@gmail.com>"]
 readme = "README.md"
@@ -29,7 +29,6 @@ repository = "https://github.com/cosmoglobe/zodipy"
 [tool.poetry.dependencies]
 python = ">=3.9"
 numpy = "^1.26.4"
-healpy = "^1.16.6"
 astropy = ">=5.0.1"
 jplephem = "^2.17"
 scipy = "^1.13.0"
@@ -38,17 +37,19 @@ scipy = "^1.13.0"
 pytest = "^7.2.1"
 mypy = "^1.0"
 mkdocs = "^1.5.3"
-pymdown-extensions = "^9.4"
-markdown-include = "^0.6.0"
+pymdown-extensions = "^10.8.1"
+markdown-include = "^0.8.1"
 pre-commit = "^2.19.0"
 coverage = "^7.1.0"
 pytest-cov = "^4.0.0"
-mkdocs-material = "^9.0.1"
-mkdocstrings = "^0.23.0"
+mkdocs-material = "^9.5.21"
+mkdocstrings = "^0.25.1"
 mkdocstrings-python = "^1.7.3"
 ruff = "^0.3.7"
-markdown = "<3.4.0"
+markdown = "^3.6.0"
 hypothesis = "^6.99.11"
+healpy = "^1.17.1"
+astropy-healpix = "^1.0.3"
 
 [build-system]
 requires = ["poetry-core>=1.0.0"]
@@ -58,11 +59,12 @@ build-backend = "poetry.core.masonry.api"
 disable_error_code = ["misc"]
 plugins = "numpy.typing.mypy_plugin"
 overrides = [
-    { module = "healpy.*", ignore_missing_imports = true },
     { module = "astropy.*", ignore_missing_imports = true },
+    { module = "astropy_healpix.*", ignore_missing_imports = true },
     { module = "scipy.*", ignore_missing_imports = true },
     { module = "pkg_resources.*", ignore_missing_imports = true },
 ]
+exclude = ["_contour.py"]
 
 [tool.pytest.ini_options]
 filterwarnings = [
@@ -100,6 +102,7 @@ lint.select = [
     "PLW",
     "TRY",
     "RUF",
+    "NPY201"
 ]
 line-length = 100
 lint.ignore = [
@@ -113,7 +116,7 @@ lint.ignore = [
     "ISC001",
     "S311",
 ]
-# exclude = ["docs/*"]
+exclude = ["work_in_progress/*"]
 lint.pydocstyle.convention = "google"
 
 [tool.ruff.lint.per-file-ignores]
diff --git a/requirements-dev.txt b/requirements-dev.txt
deleted file mode 100644
index e3dd267..0000000
--- a/requirements-dev.txt
+++ /dev/null
@@ -1,1069 +0,0 @@
-astropy==5.2.2 ; python_version >= "3.8" \
-    --hash=sha256:04464e664a22382626ce9750ebe943b80a718dc8347134b9d138b63a2029f67a \
-    --hash=sha256:093782b1f0177c3dd2c04181ec016d8e569bd9e862b48236e40b14e2a7399170 \
-    --hash=sha256:0aad85604cad40189b13d66bb46fb2a95df1a9095992071b31c3fa35b476fdbc \
-    --hash=sha256:0c664f9194a4a3cece6215f651a9bc22c3cbd1f52dd450bd4d94eaf36f13c06c \
-    --hash=sha256:0ccf6f16cf7e520247ecc9d1a66dd4c3927fd60622203bdd1d06655ad81fa18f \
-    --hash=sha256:124ef2a9f9b1cdbc1a5d514f7e57538253bb67ad031215f5f5405fc4cd31a4cd \
-    --hash=sha256:185ade8c33cea34ba791b282e937686d98b4e205d4f343e686a4666efab2f6e7 \
-    --hash=sha256:1ef9acc55c5fd70c7c78370389e79fb044321e531ac1facb7bddeef89d3132e3 \
-    --hash=sha256:22396592aa9b1653d37d552d3c52a8bb27ef072d077fad43b64faf841b1dcbf3 \
-    --hash=sha256:35ce00bb3dbc8bf7c842a0635354a5023cb64ae9c1925aa9b54629cf7fed2abe \
-    --hash=sha256:3881e933ea870a27e5d6896443401fbf51e3b7e57c6356f333553f5ff0070c72 \
-    --hash=sha256:3d0c37da922cdcb81e74437118fabd64171cbfefa06c7ea697a270e82a8164f2 \
-    --hash=sha256:4f60cea0fa7cb6ebbd90373e48c07f5d459e95dfd6363f50e316e2db7755bead \
-    --hash=sha256:530e6911a54a42e9f15b1a75dc3c699be3946c0b6ffdcfdcf4e14ae5fcfcd236 \
-    --hash=sha256:66522e897daf3766775c00ef5c63b69beb0eb359e1f45d18745d0f0ca7f29cc1 \
-    --hash=sha256:6703860deecd384bba2d2e338f77a0e7b46672812d27ed15f95e8faaa89fcd35 \
-    --hash=sha256:6768b3a670cdfff6c2416b3d7d1e4231839608299b32367e8b095959fc6733a6 \
-    --hash=sha256:6c3abb2fa8ebaaad77875a02e664c1011f35bd0c0ef7d35a39b03c859de1129a \
-    --hash=sha256:800501cc626aef0780dfb66156619699e98cb48854ed710f1ae3708aaab79f6e \
-    --hash=sha256:8304b590b20f9c161db85d5eb65d4c6323b3370a17c96ae163b18a0071cbd68a \
-    --hash=sha256:855748c2f1aedee5d770dfec8334109f1bcd1c1cee97f5915d3e888f43c04acf \
-    --hash=sha256:ac944158794a88789a007892ad91db35da14f689da1ab37c33c8de770a27f717 \
-    --hash=sha256:ae3b383ac84fe6765e275f897f4010cc6afe6933607b7468561414dffdc4d915 \
-    --hash=sha256:b00a4cd49f8264a338b0020717bff104fbcca800bd50bf0a415d952078258a39 \
-    --hash=sha256:b7167b9965ebd78b7c9da7e98a943381b25e23d041bd304ec2e35e8ec811cefc \
-    --hash=sha256:df81b8f23c5e906d799b47d2d8462707c745df38cafae0cd6674ef09e9a41789 \
-    --hash=sha256:e14b5a22f24ae5cf0404f21a4de135e26ca3c9cf55aefc5b0264a9ce24b53b0b \
-    --hash=sha256:e6a9e34716bda5945788353c63f0644721ee7e5447d16b1cdcb58c48a96b0d9c \
-    --hash=sha256:f210b5b4062030388437b9aca4bbf68f9063b2b27184006814a09fab41ac270e \
-    --hash=sha256:f30b5d153b9d119783b96b948a3e0c4eb668820c06d2e8ba72f6ea989e4af5c1 \
-    --hash=sha256:f61c612e90e3dd3c075e99a61dedd53331c4577016c1d571aab00b95ca1731ab
-attrs==23.1.0 ; python_version >= "3.8" \
-    --hash=sha256:1f28b4522cdc2fb4256ac1a020c78acf9cba2c6b461ccd2c126f3aa8e8335d04 \
-    --hash=sha256:6279836d581513a26f1bf235f9acd333bc9115683f14f7e8fae46c98fc50e015
-certifi==2023.7.22 ; python_version >= "3.8" \
-    --hash=sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082 \
-    --hash=sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9
-cfgv==3.4.0 ; python_version >= "3.8" \
-    --hash=sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9 \
-    --hash=sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560
-charset-normalizer==3.3.2 ; python_version >= "3.8" \
-    --hash=sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027 \
-    --hash=sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087 \
-    --hash=sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786 \
-    --hash=sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8 \
-    --hash=sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09 \
-    --hash=sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185 \
-    --hash=sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574 \
-    --hash=sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e \
-    --hash=sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519 \
-    --hash=sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898 \
-    --hash=sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269 \
-    --hash=sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3 \
-    --hash=sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f \
-    --hash=sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6 \
-    --hash=sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8 \
-    --hash=sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a \
-    --hash=sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73 \
-    --hash=sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc \
-    --hash=sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714 \
-    --hash=sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2 \
-    --hash=sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc \
-    --hash=sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce \
-    --hash=sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d \
-    --hash=sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e \
-    --hash=sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6 \
-    --hash=sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269 \
-    --hash=sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96 \
-    --hash=sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d \
-    --hash=sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a \
-    --hash=sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4 \
-    --hash=sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77 \
-    --hash=sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d \
-    --hash=sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0 \
-    --hash=sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed \
-    --hash=sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068 \
-    --hash=sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac \
-    --hash=sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25 \
-    --hash=sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8 \
-    --hash=sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab \
-    --hash=sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26 \
-    --hash=sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2 \
-    --hash=sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db \
-    --hash=sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f \
-    --hash=sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5 \
-    --hash=sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99 \
-    --hash=sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c \
-    --hash=sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d \
-    --hash=sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811 \
-    --hash=sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa \
-    --hash=sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a \
-    --hash=sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03 \
-    --hash=sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b \
-    --hash=sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04 \
-    --hash=sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c \
-    --hash=sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001 \
-    --hash=sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458 \
-    --hash=sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389 \
-    --hash=sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99 \
-    --hash=sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985 \
-    --hash=sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537 \
-    --hash=sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238 \
-    --hash=sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f \
-    --hash=sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d \
-    --hash=sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796 \
-    --hash=sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a \
-    --hash=sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143 \
-    --hash=sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8 \
-    --hash=sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c \
-    --hash=sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5 \
-    --hash=sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5 \
-    --hash=sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711 \
-    --hash=sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4 \
-    --hash=sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6 \
-    --hash=sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c \
-    --hash=sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7 \
-    --hash=sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4 \
-    --hash=sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b \
-    --hash=sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae \
-    --hash=sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12 \
-    --hash=sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c \
-    --hash=sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae \
-    --hash=sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8 \
-    --hash=sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887 \
-    --hash=sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b \
-    --hash=sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4 \
-    --hash=sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f \
-    --hash=sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5 \
-    --hash=sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33 \
-    --hash=sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519 \
-    --hash=sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561
-click==8.1.7 ; python_version >= "3.8" \
-    --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \
-    --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de
-colorama==0.4.6 ; python_version >= "3.8" \
-    --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \
-    --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6
-contourpy==1.1.1 ; python_version >= "3.8" \
-    --hash=sha256:059c3d2a94b930f4dafe8105bcdc1b21de99b30b51b5bce74c753686de858cb6 \
-    --hash=sha256:0683e1ae20dc038075d92e0e0148f09ffcefab120e57f6b4c9c0f477ec171f33 \
-    --hash=sha256:07d6f11dfaf80a84c97f1a5ba50d129d9303c5b4206f776e94037332e298dda8 \
-    --hash=sha256:081f3c0880712e40effc5f4c3b08feca6d064cb8cfbb372ca548105b86fd6c3d \
-    --hash=sha256:0e48694d6a9c5a26ee85b10130c77a011a4fedf50a7279fa0bdaf44bafb4299d \
-    --hash=sha256:11b836b7dbfb74e049c302bbf74b4b8f6cb9d0b6ca1bf86cfa8ba144aedadd9c \
-    --hash=sha256:19557fa407e70f20bfaba7d55b4d97b14f9480856c4fb65812e8a05fe1c6f9bf \
-    --hash=sha256:229a25f68046c5cf8067d6d6351c8b99e40da11b04d8416bf8d2b1d75922521e \
-    --hash=sha256:24216552104ae8f3b34120ef84825400b16eb6133af2e27a190fdc13529f023e \
-    --hash=sha256:3b53d5769aa1f2d4ea407c65f2d1d08002952fac1d9e9d307aa2e1023554a163 \
-    --hash=sha256:3de23ca4f381c3770dee6d10ead6fff524d540c0f662e763ad1530bde5112532 \
-    --hash=sha256:407d864db716a067cc696d61fa1ef6637fedf03606e8417fe2aeed20a061e6b2 \
-    --hash=sha256:41339b24471c58dc1499e56783fedc1afa4bb018bcd035cfb0ee2ad2a7501ef8 \
-    --hash=sha256:462c59914dc6d81e0b11f37e560b8a7c2dbab6aca4f38be31519d442d6cde1a1 \
-    --hash=sha256:46e24f5412c948d81736509377e255f6040e94216bf1a9b5ea1eaa9d29f6ec1b \
-    --hash=sha256:498e53573e8b94b1caeb9e62d7c2d053c263ebb6aa259c81050766beb50ff8d9 \
-    --hash=sha256:4ebf42695f75ee1a952f98ce9775c873e4971732a87334b099dde90b6af6a916 \
-    --hash=sha256:4f9147051cb8fdb29a51dc2482d792b3b23e50f8f57e3720ca2e3d438b7adf23 \
-    --hash=sha256:549174b0713d49871c6dee90a4b499d3f12f5e5f69641cd23c50a4542e2ca1eb \
-    --hash=sha256:560f1d68a33e89c62da5da4077ba98137a5e4d3a271b29f2f195d0fba2adcb6a \
-    --hash=sha256:566f0e41df06dfef2431defcfaa155f0acfa1ca4acbf8fd80895b1e7e2ada40e \
-    --hash=sha256:56de98a2fb23025882a18b60c7f0ea2d2d70bbbcfcf878f9067234b1c4818442 \
-    --hash=sha256:66544f853bfa85c0d07a68f6c648b2ec81dafd30f272565c37ab47a33b220684 \
-    --hash=sha256:6c06e4c6e234fcc65435223c7b2a90f286b7f1b2733058bdf1345d218cc59e34 \
-    --hash=sha256:6d0a8efc258659edc5299f9ef32d8d81de8b53b45d67bf4bfa3067f31366764d \
-    --hash=sha256:70e5a10f8093d228bb2b552beeb318b8928b8a94763ef03b858ef3612b29395d \
-    --hash=sha256:8394e652925a18ef0091115e3cc191fef350ab6dc3cc417f06da66bf98071ae9 \
-    --hash=sha256:8636cd2fc5da0fb102a2504fa2c4bea3cbc149533b345d72cdf0e7a924decc45 \
-    --hash=sha256:93df44ab351119d14cd1e6b52a5063d3336f0754b72736cc63db59307dabb718 \
-    --hash=sha256:96ba37c2e24b7212a77da85004c38e7c4d155d3e72a45eeaf22c1f03f607e8ab \
-    --hash=sha256:a10dab5ea1bd4401c9483450b5b0ba5416be799bbd50fc7a6cc5e2a15e03e8a3 \
-    --hash=sha256:a66045af6cf00e19d02191ab578a50cb93b2028c3eefed999793698e9ea768ae \
-    --hash=sha256:a75cc163a5f4531a256f2c523bd80db509a49fc23721b36dd1ef2f60ff41c3cb \
-    --hash=sha256:b04c2f0adaf255bf756cf08ebef1be132d3c7a06fe6f9877d55640c5e60c72c5 \
-    --hash=sha256:ba42e3810999a0ddd0439e6e5dbf6d034055cdc72b7c5c839f37a7c274cb4eba \
-    --hash=sha256:bfc8a5e9238232a45ebc5cb3bfee71f1167064c8d382cadd6076f0d51cff1da0 \
-    --hash=sha256:c5bd5680f844c3ff0008523a71949a3ff5e4953eb7701b28760805bc9bcff217 \
-    --hash=sha256:c84fdf3da00c2827d634de4fcf17e3e067490c4aea82833625c4c8e6cdea0887 \
-    --hash=sha256:ca6fab080484e419528e98624fb5c4282148b847e3602dc8dbe0cb0669469887 \
-    --hash=sha256:d0c188ae66b772d9d61d43c6030500344c13e3f73a00d1dc241da896f379bb62 \
-    --hash=sha256:d6ab42f223e58b7dac1bb0af32194a7b9311065583cc75ff59dcf301afd8a431 \
-    --hash=sha256:dfe80c017973e6a4c367e037cb31601044dd55e6bfacd57370674867d15a899b \
-    --hash=sha256:e0c02b75acfea5cab07585d25069207e478d12309557f90a61b5a3b4f77f46ce \
-    --hash=sha256:e30aaf2b8a2bac57eb7e1650df1b3a4130e8d0c66fc2f861039d507a11760e1b \
-    --hash=sha256:eafbef886566dc1047d7b3d4b14db0d5b7deb99638d8e1be4e23a7c7ac59ff0f \
-    --hash=sha256:efe0fab26d598e1ec07d72cf03eaeeba8e42b4ecf6b9ccb5a356fde60ff08b85 \
-    --hash=sha256:f08e469821a5e4751c97fcd34bcb586bc243c39c2e39321822060ba902eac49e \
-    --hash=sha256:f1eaac5257a8f8a047248d60e8f9315c6cff58f7803971170d952555ef6344a7 \
-    --hash=sha256:f29fb0b3f1217dfe9362ec55440d0743fe868497359f2cf93293f4b2701b8251 \
-    --hash=sha256:f44d78b61740e4e8c71db1cf1fd56d9050a4747681c59ec1094750a658ceb970 \
-    --hash=sha256:f6aec19457617ef468ff091669cca01fa7ea557b12b59a7908b9474bb9674cf0 \
-    --hash=sha256:f9dc7f933975367251c1b34da882c4f0e0b2e24bb35dc906d2f598a40b72bfc7
-coverage==7.3.2 ; python_version >= "3.8" \
-    --hash=sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1 \
-    --hash=sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63 \
-    --hash=sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9 \
-    --hash=sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312 \
-    --hash=sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3 \
-    --hash=sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb \
-    --hash=sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25 \
-    --hash=sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92 \
-    --hash=sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda \
-    --hash=sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148 \
-    --hash=sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6 \
-    --hash=sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216 \
-    --hash=sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a \
-    --hash=sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640 \
-    --hash=sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836 \
-    --hash=sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c \
-    --hash=sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f \
-    --hash=sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2 \
-    --hash=sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901 \
-    --hash=sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed \
-    --hash=sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a \
-    --hash=sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074 \
-    --hash=sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc \
-    --hash=sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84 \
-    --hash=sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083 \
-    --hash=sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f \
-    --hash=sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c \
-    --hash=sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c \
-    --hash=sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637 \
-    --hash=sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2 \
-    --hash=sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82 \
-    --hash=sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f \
-    --hash=sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce \
-    --hash=sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef \
-    --hash=sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f \
-    --hash=sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611 \
-    --hash=sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c \
-    --hash=sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76 \
-    --hash=sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9 \
-    --hash=sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce \
-    --hash=sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9 \
-    --hash=sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf \
-    --hash=sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf \
-    --hash=sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9 \
-    --hash=sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6 \
-    --hash=sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2 \
-    --hash=sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a \
-    --hash=sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a \
-    --hash=sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf \
-    --hash=sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738 \
-    --hash=sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a \
-    --hash=sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4
-coverage[toml]==7.3.2 ; python_version >= "3.8" \
-    --hash=sha256:0cbf38419fb1a347aaf63481c00f0bdc86889d9fbf3f25109cf96c26b403fda1 \
-    --hash=sha256:12d15ab5833a997716d76f2ac1e4b4d536814fc213c85ca72756c19e5a6b3d63 \
-    --hash=sha256:149de1d2401ae4655c436a3dced6dd153f4c3309f599c3d4bd97ab172eaf02d9 \
-    --hash=sha256:1981f785239e4e39e6444c63a98da3a1db8e971cb9ceb50a945ba6296b43f312 \
-    --hash=sha256:2443cbda35df0d35dcfb9bf8f3c02c57c1d6111169e3c85fc1fcc05e0c9f39a3 \
-    --hash=sha256:289fe43bf45a575e3ab10b26d7b6f2ddb9ee2dba447499f5401cfb5ecb8196bb \
-    --hash=sha256:2f11cc3c967a09d3695d2a6f03fb3e6236622b93be7a4b5dc09166a861be6d25 \
-    --hash=sha256:307adb8bd3abe389a471e649038a71b4eb13bfd6b7dd9a129fa856f5c695cf92 \
-    --hash=sha256:310b3bb9c91ea66d59c53fa4989f57d2436e08f18fb2f421a1b0b6b8cc7fffda \
-    --hash=sha256:315a989e861031334d7bee1f9113c8770472db2ac484e5b8c3173428360a9148 \
-    --hash=sha256:3a4006916aa6fee7cd38db3bfc95aa9c54ebb4ffbfc47c677c8bba949ceba0a6 \
-    --hash=sha256:3c7bba973ebee5e56fe9251300c00f1579652587a9f4a5ed8404b15a0471f216 \
-    --hash=sha256:4175e10cc8dda0265653e8714b3174430b07c1dca8957f4966cbd6c2b1b8065a \
-    --hash=sha256:43668cabd5ca8258f5954f27a3aaf78757e6acf13c17604d89648ecc0cc66640 \
-    --hash=sha256:4cbae1051ab791debecc4a5dcc4a1ff45fc27b91b9aee165c8a27514dd160836 \
-    --hash=sha256:5c913b556a116b8d5f6ef834038ba983834d887d82187c8f73dec21049abd65c \
-    --hash=sha256:5f7363d3b6a1119ef05015959ca24a9afc0ea8a02c687fe7e2d557705375c01f \
-    --hash=sha256:630b13e3036e13c7adc480ca42fa7afc2a5d938081d28e20903cf7fd687872e2 \
-    --hash=sha256:72c0cfa5250f483181e677ebc97133ea1ab3eb68645e494775deb6a7f6f83901 \
-    --hash=sha256:7dbc3ed60e8659bc59b6b304b43ff9c3ed858da2839c78b804973f613d3e92ed \
-    --hash=sha256:88ed2c30a49ea81ea3b7f172e0269c182a44c236eb394718f976239892c0a27a \
-    --hash=sha256:89a937174104339e3a3ffcf9f446c00e3a806c28b1841c63edb2b369310fd074 \
-    --hash=sha256:9028a3871280110d6e1aa2df1afd5ef003bab5fb1ef421d6dc748ae1c8ef2ebc \
-    --hash=sha256:99b89d9f76070237975b315b3d5f4d6956ae354a4c92ac2388a5695516e47c84 \
-    --hash=sha256:9f805d62aec8eb92bab5b61c0f07329275b6f41c97d80e847b03eb894f38d083 \
-    --hash=sha256:a889ae02f43aa45032afe364c8ae84ad3c54828c2faa44f3bfcafecb5c96b02f \
-    --hash=sha256:aa72dbaf2c2068404b9870d93436e6d23addd8bbe9295f49cbca83f6e278179c \
-    --hash=sha256:ac8c802fa29843a72d32ec56d0ca792ad15a302b28ca6203389afe21f8fa062c \
-    --hash=sha256:ae97af89f0fbf373400970c0a21eef5aa941ffeed90aee43650b81f7d7f47637 \
-    --hash=sha256:af3d828d2c1cbae52d34bdbb22fcd94d1ce715d95f1a012354a75e5913f1bda2 \
-    --hash=sha256:b4275802d16882cf9c8b3d057a0839acb07ee9379fa2749eca54efbce1535b82 \
-    --hash=sha256:b4767da59464bb593c07afceaddea61b154136300881844768037fd5e859353f \
-    --hash=sha256:b631c92dfe601adf8f5ebc7fc13ced6bb6e9609b19d9a8cd59fa47c4186ad1ce \
-    --hash=sha256:be32ad29341b0170e795ca590e1c07e81fc061cb5b10c74ce7203491484404ef \
-    --hash=sha256:beaa5c1b4777f03fc63dfd2a6bd820f73f036bfb10e925fce067b00a340d0f3f \
-    --hash=sha256:c0ba320de3fb8c6ec16e0be17ee1d3d69adcda99406c43c0409cb5c41788a611 \
-    --hash=sha256:c9eacf273e885b02a0273bb3a2170f30e2d53a6d53b72dbe02d6701b5296101c \
-    --hash=sha256:cb536f0dcd14149425996821a168f6e269d7dcd2c273a8bff8201e79f5104e76 \
-    --hash=sha256:d1bc430677773397f64a5c88cb522ea43175ff16f8bfcc89d467d974cb2274f9 \
-    --hash=sha256:d1c88ec1a7ff4ebca0219f5b1ef863451d828cccf889c173e1253aa84b1e07ce \
-    --hash=sha256:d3d9df4051c4a7d13036524b66ecf7a7537d14c18a384043f30a303b146164e9 \
-    --hash=sha256:d51ac2a26f71da1b57f2dc81d0e108b6ab177e7d30e774db90675467c847bbdf \
-    --hash=sha256:d872145f3a3231a5f20fd48500274d7df222e291d90baa2026cc5152b7ce86bf \
-    --hash=sha256:d8f17966e861ff97305e0801134e69db33b143bbfb36436efb9cfff6ec7b2fd9 \
-    --hash=sha256:dbc1b46b92186cc8074fee9d9fbb97a9dd06c6cbbef391c2f59d80eabdf0faa6 \
-    --hash=sha256:e10c39c0452bf6e694511c901426d6b5ac005acc0f78ff265dbe36bf81f808a2 \
-    --hash=sha256:e267e9e2b574a176ddb983399dec325a80dbe161f1a32715c780b5d14b5f583a \
-    --hash=sha256:f47d39359e2c3779c5331fc740cf4bce6d9d680a7b4b4ead97056a0ae07cb49a \
-    --hash=sha256:f6e9589bd04d0461a417562649522575d8752904d35c12907d8c9dfeba588faf \
-    --hash=sha256:f94b734214ea6a36fe16e96a70d941af80ff3bfd716c141300d95ebc85339738 \
-    --hash=sha256:fa28e909776dc69efb6ed975a63691bc8172b64ff357e663a1bb06ff3c9b589a \
-    --hash=sha256:fe494faa90ce6381770746077243231e0b83ff3f17069d748f645617cefe19d4
-cycler==0.12.1 ; python_version >= "3.8" \
-    --hash=sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30 \
-    --hash=sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c
-distlib==0.3.7 ; python_version >= "3.8" \
-    --hash=sha256:2e24928bc811348f0feb63014e97aaae3037f2cf48712d51ae61df7fd6075057 \
-    --hash=sha256:9dafe54b34a028eafd95039d5e5d4851a13734540f1331060d31c9916e7147a8
-exceptiongroup==1.1.3 ; python_version < "3.11" and python_version >= "3.8" \
-    --hash=sha256:097acd85d473d75af5bb98e41b61ff7fe35efe6675e4f9370ec6ec5126d160e9 \
-    --hash=sha256:343280667a4585d195ca1cf9cef84a4e178c4b6cf2274caef9859782b567d5e3
-filelock==3.13.1 ; python_version >= "3.8" \
-    --hash=sha256:521f5f56c50f8426f5e03ad3b281b490a87ef15bc6c526f168290f0c7148d44e \
-    --hash=sha256:57dbda9b35157b05fb3e58ee91448612eb674172fab98ee235ccb0b5bee19a1c
-fonttools==4.44.0 ; python_version >= "3.8" \
-    --hash=sha256:05d7c4d2c95b9490e669f3cb83918799bf1c838619ac6d3bad9ea017cfc63f2e \
-    --hash=sha256:0f412954275e594f7a51c16f3b3edd850acb0d842fefc33856b63a17e18499a5 \
-    --hash=sha256:22ea8aa7b3712450b42b044702bd3a64fd118006bad09a6f94bd1b227088492e \
-    --hash=sha256:2db63941fee3122e31a21dd0f5b2138ce9906b661a85b63622421d3654a74ae2 \
-    --hash=sha256:2e91e19b583961979e2e5a701269d3cfc07418963bee717f8160b0a24332826b \
-    --hash=sha256:31b38528f25bc662401e6ffae14b3eb7f1e820892fd80369a37155e3b636a2f4 \
-    --hash=sha256:3d29509f6e05e8d725db59c2d8c076223d793e4e35773040be6632a0349f2f97 \
-    --hash=sha256:46c79af80a835410874683b5779b6c1ec1d5a285e11c45b5193e79dd691eb111 \
-    --hash=sha256:4e90dd81b6e0d97ebfe52c0d12a17a9ef7f305d6bfbb93081265057d6092f252 \
-    --hash=sha256:50d25893885e80a5955186791eed5579f1e75921751539cc1dc3ffd1160b48cf \
-    --hash=sha256:518a945dbfe337744bfff31423c1430303b8813c5275dffb0f2577f0734a1189 \
-    --hash=sha256:54efed22b2799a85475e6840e907c402ba49892c614565dc770aa97a53621b2b \
-    --hash=sha256:58af428746fa73a2edcbf26aff33ac4ef3c11c8d75bb200eaea2f7e888d2de4e \
-    --hash=sha256:59b6ad83cce067d10f4790c037a5904424f45bebb5e7be2eb2db90402f288267 \
-    --hash=sha256:63a3112f753baef8c6ac2f5f574bb9ac8001b86c8c0c0380039db47a7f512d20 \
-    --hash=sha256:66bc6efd829382f7a7e6cf33c2fb32b13edc8a239eb15f32acbf197dce7a0165 \
-    --hash=sha256:6999e80a125b0cd8e068d0210b63323f17338038c2ecd2e11b9209ec430fe7f2 \
-    --hash=sha256:6d16d9634ff1e5cea2cf4a8cbda9026f766e4b5f30b48f8180f0e99133d3abfc \
-    --hash=sha256:84f308b7a8d28208d54315d11d35f9888d6d607673dd4d42d60b463682ee0400 \
-    --hash=sha256:9ee8692e23028564c13d924004495f284df8ac016a19f17a87251210e1f1f928 \
-    --hash=sha256:a3da036b016c975c2d8c69005bdc4d5d16266f948a7fab950244e0f58301996a \
-    --hash=sha256:a7aec7f5d14dfcd71fb3ebc299b3f000c21fdc4043079101777ed2042ba5b7c5 \
-    --hash=sha256:a8a1fa9a718de0bc026979c93e1e9b55c5efde60d76f91561fd713387573817d \
-    --hash=sha256:a8b99713d3a0d0e876b6aecfaada5e7dc9fe979fcd90ef9fa0ba1d9b9aed03f2 \
-    --hash=sha256:b63da598d9cbc52e2381f922da0e94d60c0429f92207bd3fb04d112fc82ea7cb \
-    --hash=sha256:b6e6aa2d066f8dafd06d8d0799b4944b5d5a1f015dd52ac01bdf2895ebe169a0 \
-    --hash=sha256:b99fe8ef4093f672d00841569d2d05691e50334d79f4d9c15c1265d76d5580d2 \
-    --hash=sha256:b9beb0fa6ff3ea808ad4a6962d68ac0f140ddab080957b20d9e268e4d67fb335 \
-    --hash=sha256:b9eab7f9837fdaa2a10a524fbcc2ec24bf60637c044b6e4a59c3f835b90f0fae \
-    --hash=sha256:bca49da868e8bde569ef36f0cc1b6de21d56bf9c3be185c503b629c19a185287 \
-    --hash=sha256:c05064f95aacdfc06f21e55096c964b2228d942b8675fa26995a2551f6329d2d \
-    --hash=sha256:c2de1fb18198acd400c45ffe2aef5420c8d55fde903e91cba705596099550f3b \
-    --hash=sha256:c794de4086f06ae609b71ac944ec7deb09f34ecf73316fddc041087dd24bba39 \
-    --hash=sha256:d4fa4f4bc8fd86579b8cdbe5e948f35d82c0eda0091c399d009b2a5a6b61c040 \
-    --hash=sha256:dab3d00d27b1a79ae4d4a240e8ceea8af0ff049fd45f05adb4f860d93744110d \
-    --hash=sha256:dbac86d83d96099890e731cc2af97976ff2c98f4ba432fccde657c5653a32f1c \
-    --hash=sha256:df40daa6c03b98652ffe8110ae014fe695437f6e1cb5a07e16ea37f40e73ac86 \
-    --hash=sha256:e1cd1c6bb097e774d68402499ff66185190baaa2629ae2f18515a2c50b93db0c \
-    --hash=sha256:e8ff7d19a6804bfd561cfcec9b4200dd1788e28f7de4be70189801530c47c1b3 \
-    --hash=sha256:eb01c49c8aa035d5346f46630209923d4927ed15c2493db38d31da9f811eb70d \
-    --hash=sha256:f53526668beccdb3409c6055a4ffe50987a7f05af6436fa55d61f5e7bd450219 \
-    --hash=sha256:f611c97678604e302b725f71626edea113a5745a7fb557c958b39edb6add87d5
-ghp-import==2.1.0 ; python_version >= "3.8" \
-    --hash=sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619 \
-    --hash=sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343
-griffe==0.36.9 ; python_version >= "3.8" \
-    --hash=sha256:7874febe7cd81e8e47eb7b8130ff9d38c8f3656233c01d2d217d2e898a0925f5 \
-    --hash=sha256:b4e510bf0ed1fc91c58453c68018a2247c561adec8f5dadc40275afc01f51eac
-healpy==1.16.5 ; python_version >= "3.8" \
-    --hash=sha256:2d72f2468d9ffee1d78db0bffb6a75b09cfd5064c05c4b17a08fe579a1bbd81d \
-    --hash=sha256:40b0363b781b8c471b2fe40fa54f8b82810b8a80d19c1e86f2e4114c0118ee55 \
-    --hash=sha256:666bf645caf813f53393d60ef388e5daf19d9b0d81bd8a133793b7f61d1c61dd \
-    --hash=sha256:9f99cd5ed2d8791dbfcefe1552a73e550ec85b87637127938756280008d0ed29 \
-    --hash=sha256:a6bd59a2d1fd6ec24a7ddf409b027b31a7b6dedbbb82866142249983a3056ca4 \
-    --hash=sha256:b7b70298346f09382fae55b8b44d338e214239e634f8b3afbf23842f8967e475 \
-    --hash=sha256:ba99a0c262ae389db2bc9a78b107e1d33944608ca492767f636a94144156f464 \
-    --hash=sha256:dca8842bec5c35d793edc8aa3757a57ed32c55c9e175340125fd68cfa4d67086 \
-    --hash=sha256:e7611cbb5d085a1d579d773c7922ac72a67e22686fe5167417ae9c9807b85ed6 \
-    --hash=sha256:e9fc6a94aaeee154630e09b9857890d8eed7e839f740c67c94095d42c8253a51 \
-    --hash=sha256:ec39b58daf8303f7df125fb4fa690b70a6edf88e5644990d940edf656e9e984a \
-    --hash=sha256:f87784a3912fd6253c9aa8118ed0d55c224564e1b94875a1a213feed0395aa1e \
-    --hash=sha256:f988b3505e950e0d63b59d1e0b344e5c0e905f28848309ef3ff51cf5672841d0
-hypothesis==6.88.3 ; python_version >= "3.8" \
-    --hash=sha256:5cfda253e34726c98ab04b9595fca15677ee9f4f6055146aea25a6278d71f6f1 \
-    --hash=sha256:781ce6fd35e11ca77ad132a20cebe66fd215f56678f8efd6b87013b14500151b
-identify==2.5.31 ; python_version >= "3.8" \
-    --hash=sha256:7736b3c7a28233637e3c36550646fc6389bedd74ae84cb788200cc8e2dd60b75 \
-    --hash=sha256:90199cb9e7bd3c5407a9b7e81b4abec4bb9d249991c79439ec8af740afc6293d
-idna==3.4 ; python_version >= "3.8" \
-    --hash=sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4 \
-    --hash=sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2
-importlib-metadata==6.8.0 ; python_version < "3.10" and python_version >= "3.8" \
-    --hash=sha256:3ebb78df84a805d7698245025b975d9d67053cd94c79245ba4b3eb694abe68bb \
-    --hash=sha256:dbace7892d8c0c4ac1ad096662232f831d4e64f4c4545bd53016a3e9d4654743
-importlib-resources==6.1.0 ; python_version < "3.10" and python_version >= "3.8" \
-    --hash=sha256:9d48dcccc213325e810fd723e7fbb45ccb39f6cf5c31f00cf2b965f5f10f3cb9 \
-    --hash=sha256:aa50258bbfa56d4e33fbd8aa3ef48ded10d1735f11532b8df95388cc6bdb7e83
-iniconfig==2.0.0 ; python_version >= "3.8" \
-    --hash=sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3 \
-    --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374
-jinja2==3.1.2 ; python_version >= "3.8" \
-    --hash=sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852 \
-    --hash=sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61
-jplephem==2.19 ; python_version >= "3.8" \
-    --hash=sha256:c162454c656d6e5203ff9701d826671faf9f3209d971cbb68ddb46003dff6dcf \
-    --hash=sha256:eb1ef089050557725f747f9e5c3afc0fb5978261ae620c5b6a1f66a3cc7565df
-kiwisolver==1.4.5 ; python_version >= "3.8" \
-    --hash=sha256:00bd361b903dc4bbf4eb165f24d1acbee754fce22ded24c3d56eec268658a5cf \
-    --hash=sha256:040c1aebeda72197ef477a906782b5ab0d387642e93bda547336b8957c61022e \
-    --hash=sha256:05703cf211d585109fcd72207a31bb170a0f22144d68298dc5e61b3c946518af \
-    --hash=sha256:06f54715b7737c2fecdbf140d1afb11a33d59508a47bf11bb38ecf21dc9ab79f \
-    --hash=sha256:0dc9db8e79f0036e8173c466d21ef18e1befc02de8bf8aa8dc0813a6dc8a7046 \
-    --hash=sha256:0f114aa76dc1b8f636d077979c0ac22e7cd8f3493abbab152f20eb8d3cda71f3 \
-    --hash=sha256:11863aa14a51fd6ec28688d76f1735f8f69ab1fabf388851a595d0721af042f5 \
-    --hash=sha256:11c7de8f692fc99816e8ac50d1d1aef4f75126eefc33ac79aac02c099fd3db71 \
-    --hash=sha256:11d011a7574eb3b82bcc9c1a1d35c1d7075677fdd15de527d91b46bd35e935ee \
-    --hash=sha256:146d14bebb7f1dc4d5fbf74f8a6cb15ac42baadee8912eb84ac0b3b2a3dc6ac3 \
-    --hash=sha256:15568384086b6df3c65353820a4473575dbad192e35010f622c6ce3eebd57af9 \
-    --hash=sha256:19df6e621f6d8b4b9c4d45f40a66839294ff2bb235e64d2178f7522d9170ac5b \
-    --hash=sha256:1b04139c4236a0f3aff534479b58f6f849a8b351e1314826c2d230849ed48985 \
-    --hash=sha256:210ef2c3a1f03272649aff1ef992df2e724748918c4bc2d5a90352849eb40bea \
-    --hash=sha256:2270953c0d8cdab5d422bee7d2007f043473f9d2999631c86a223c9db56cbd16 \
-    --hash=sha256:2400873bccc260b6ae184b2b8a4fec0e4082d30648eadb7c3d9a13405d861e89 \
-    --hash=sha256:2a40773c71d7ccdd3798f6489aaac9eee213d566850a9533f8d26332d626b82c \
-    --hash=sha256:2c5674c4e74d939b9d91dda0fae10597ac7521768fec9e399c70a1f27e2ea2d9 \
-    --hash=sha256:3195782b26fc03aa9c6913d5bad5aeb864bdc372924c093b0f1cebad603dd712 \
-    --hash=sha256:31a82d498054cac9f6d0b53d02bb85811185bcb477d4b60144f915f3b3126342 \
-    --hash=sha256:32d5cf40c4f7c7b3ca500f8985eb3fb3a7dfc023215e876f207956b5ea26632a \
-    --hash=sha256:346f5343b9e3f00b8db8ba359350eb124b98c99efd0b408728ac6ebf38173958 \
-    --hash=sha256:378a214a1e3bbf5ac4a8708304318b4f890da88c9e6a07699c4ae7174c09a68d \
-    --hash=sha256:39b42c68602539407884cf70d6a480a469b93b81b7701378ba5e2328660c847a \
-    --hash=sha256:3a2b053a0ab7a3960c98725cfb0bf5b48ba82f64ec95fe06f1d06c99b552e130 \
-    --hash=sha256:3aba7311af82e335dd1e36ffff68aaca609ca6290c2cb6d821a39aa075d8e3ff \
-    --hash=sha256:3cd32d6c13807e5c66a7cbb79f90b553642f296ae4518a60d8d76243b0ad2898 \
-    --hash=sha256:3edd2fa14e68c9be82c5b16689e8d63d89fe927e56debd6e1dbce7a26a17f81b \
-    --hash=sha256:4c380469bd3f970ef677bf2bcba2b6b0b4d5c75e7a020fb863ef75084efad66f \
-    --hash=sha256:4e66e81a5779b65ac21764c295087de82235597a2293d18d943f8e9e32746265 \
-    --hash=sha256:53abb58632235cd154176ced1ae8f0d29a6657aa1aa9decf50b899b755bc2b93 \
-    --hash=sha256:5794cf59533bc3f1b1c821f7206a3617999db9fbefc345360aafe2e067514929 \
-    --hash=sha256:59415f46a37f7f2efeec758353dd2eae1b07640d8ca0f0c42548ec4125492635 \
-    --hash=sha256:59ec7b7c7e1a61061850d53aaf8e93db63dce0c936db1fda2658b70e4a1be709 \
-    --hash=sha256:59edc41b24031bc25108e210c0def6f6c2191210492a972d585a06ff246bb79b \
-    --hash=sha256:5a580c91d686376f0f7c295357595c5a026e6cbc3d77b7c36e290201e7c11ecb \
-    --hash=sha256:5b94529f9b2591b7af5f3e0e730a4e0a41ea174af35a4fd067775f9bdfeee01a \
-    --hash=sha256:5c7b3b3a728dc6faf3fc372ef24f21d1e3cee2ac3e9596691d746e5a536de920 \
-    --hash=sha256:5c90ae8c8d32e472be041e76f9d2f2dbff4d0b0be8bd4041770eddb18cf49a4e \
-    --hash=sha256:5e7139af55d1688f8b960ee9ad5adafc4ac17c1c473fe07133ac092310d76544 \
-    --hash=sha256:5ff5cf3571589b6d13bfbfd6bcd7a3f659e42f96b5fd1c4830c4cf21d4f5ef45 \
-    --hash=sha256:620ced262a86244e2be10a676b646f29c34537d0d9cc8eb26c08f53d98013390 \
-    --hash=sha256:6512cb89e334e4700febbffaaa52761b65b4f5a3cf33f960213d5656cea36a77 \
-    --hash=sha256:6c08e1312a9cf1074d17b17728d3dfce2a5125b2d791527f33ffbe805200a355 \
-    --hash=sha256:6c3bd3cde54cafb87d74d8db50b909705c62b17c2099b8f2e25b461882e544ff \
-    --hash=sha256:6ef7afcd2d281494c0a9101d5c571970708ad911d028137cd558f02b851c08b4 \
-    --hash=sha256:7269d9e5f1084a653d575c7ec012ff57f0c042258bf5db0954bf551c158466e7 \
-    --hash=sha256:72d40b33e834371fd330fb1472ca19d9b8327acb79a5821d4008391db8e29f20 \
-    --hash=sha256:74d1b44c6cfc897df648cc9fdaa09bc3e7679926e6f96df05775d4fb3946571c \
-    --hash=sha256:74db36e14a7d1ce0986fa104f7d5637aea5c82ca6326ed0ec5694280942d1162 \
-    --hash=sha256:763773d53f07244148ccac5b084da5adb90bfaee39c197554f01b286cf869228 \
-    --hash=sha256:76c6a5964640638cdeaa0c359382e5703e9293030fe730018ca06bc2010c4437 \
-    --hash=sha256:76d9289ed3f7501012e05abb8358bbb129149dbd173f1f57a1bf1c22d19ab7cc \
-    --hash=sha256:7931d8f1f67c4be9ba1dd9c451fb0eeca1a25b89e4d3f89e828fe12a519b782a \
-    --hash=sha256:7b8b454bac16428b22560d0a1cf0a09875339cab69df61d7805bf48919415901 \
-    --hash=sha256:7e5bab140c309cb3a6ce373a9e71eb7e4873c70c2dda01df6820474f9889d6d4 \
-    --hash=sha256:83d78376d0d4fd884e2c114d0621624b73d2aba4e2788182d286309ebdeed770 \
-    --hash=sha256:852542f9481f4a62dbb5dd99e8ab7aedfeb8fb6342349a181d4036877410f525 \
-    --hash=sha256:85267bd1aa8880a9c88a8cb71e18d3d64d2751a790e6ca6c27b8ccc724bcd5ad \
-    --hash=sha256:88a2df29d4724b9237fc0c6eaf2a1adae0cdc0b3e9f4d8e7dc54b16812d2d81a \
-    --hash=sha256:88b9f257ca61b838b6f8094a62418421f87ac2a1069f7e896c36a7d86b5d4c29 \
-    --hash=sha256:8ab3919a9997ab7ef2fbbed0cc99bb28d3c13e6d4b1ad36e97e482558a91be90 \
-    --hash=sha256:92dea1ffe3714fa8eb6a314d2b3c773208d865a0e0d35e713ec54eea08a66250 \
-    --hash=sha256:9407b6a5f0d675e8a827ad8742e1d6b49d9c1a1da5d952a67d50ef5f4170b18d \
-    --hash=sha256:9408acf3270c4b6baad483865191e3e582b638b1654a007c62e3efe96f09a9a3 \
-    --hash=sha256:955e8513d07a283056b1396e9a57ceddbd272d9252c14f154d450d227606eb54 \
-    --hash=sha256:9db8ea4c388fdb0f780fe91346fd438657ea602d58348753d9fb265ce1bca67f \
-    --hash=sha256:9eaa8b117dc8337728e834b9c6e2611f10c79e38f65157c4c38e9400286f5cb1 \
-    --hash=sha256:a51a263952b1429e429ff236d2f5a21c5125437861baeed77f5e1cc2d2c7c6da \
-    --hash=sha256:a6aa6315319a052b4ee378aa171959c898a6183f15c1e541821c5c59beaa0238 \
-    --hash=sha256:aa12042de0171fad672b6c59df69106d20d5596e4f87b5e8f76df757a7c399aa \
-    --hash=sha256:aaf7be1207676ac608a50cd08f102f6742dbfc70e8d60c4db1c6897f62f71523 \
-    --hash=sha256:b0157420efcb803e71d1b28e2c287518b8808b7cf1ab8af36718fd0a2c453eb0 \
-    --hash=sha256:b3f7e75f3015df442238cca659f8baa5f42ce2a8582727981cbfa15fee0ee205 \
-    --hash=sha256:b9098e0049e88c6a24ff64545cdfc50807818ba6c1b739cae221bbbcbc58aad3 \
-    --hash=sha256:ba55dce0a9b8ff59495ddd050a0225d58bd0983d09f87cfe2b6aec4f2c1234e4 \
-    --hash=sha256:bb86433b1cfe686da83ce32a9d3a8dd308e85c76b60896d58f082136f10bffac \
-    --hash=sha256:bbea0db94288e29afcc4c28afbf3a7ccaf2d7e027489c449cf7e8f83c6346eb9 \
-    --hash=sha256:bbf1d63eef84b2e8c89011b7f2235b1e0bf7dacc11cac9431fc6468e99ac77fb \
-    --hash=sha256:c7940c1dc63eb37a67721b10d703247552416f719c4188c54e04334321351ced \
-    --hash=sha256:c9bf3325c47b11b2e51bca0824ea217c7cd84491d8ac4eefd1e409705ef092bd \
-    --hash=sha256:cdc8a402aaee9a798b50d8b827d7ecf75edc5fb35ea0f91f213ff927c15f4ff0 \
-    --hash=sha256:ceec1a6bc6cab1d6ff5d06592a91a692f90ec7505d6463a88a52cc0eb58545da \
-    --hash=sha256:cfe6ab8da05c01ba6fbea630377b5da2cd9bcbc6338510116b01c1bc939a2c18 \
-    --hash=sha256:d099e745a512f7e3bbe7249ca835f4d357c586d78d79ae8f1dcd4d8adeb9bda9 \
-    --hash=sha256:d0ef46024e6a3d79c01ff13801cb19d0cad7fd859b15037aec74315540acc276 \
-    --hash=sha256:d2e5a98f0ec99beb3c10e13b387f8db39106d53993f498b295f0c914328b1333 \
-    --hash=sha256:da4cfb373035def307905d05041c1d06d8936452fe89d464743ae7fb8371078b \
-    --hash=sha256:da802a19d6e15dffe4b0c24b38b3af68e6c1a68e6e1d8f30148c83864f3881db \
-    --hash=sha256:dced8146011d2bc2e883f9bd68618b8247387f4bbec46d7392b3c3b032640126 \
-    --hash=sha256:dfdd7c0b105af050eb3d64997809dc21da247cf44e63dc73ff0fd20b96be55a9 \
-    --hash=sha256:e368f200bbc2e4f905b8e71eb38b3c04333bddaa6a2464a6355487b02bb7fb09 \
-    --hash=sha256:e391b1f0a8a5a10ab3b9bb6afcfd74f2175f24f8975fb87ecae700d1503cdee0 \
-    --hash=sha256:e57e563a57fb22a142da34f38acc2fc1a5c864bc29ca1517a88abc963e60d6ec \
-    --hash=sha256:e5d706eba36b4c4d5bc6c6377bb6568098765e990cfc21ee16d13963fab7b3e7 \
-    --hash=sha256:ec20916e7b4cbfb1f12380e46486ec4bcbaa91a9c448b97023fde0d5bbf9e4ff \
-    --hash=sha256:f1d072c2eb0ad60d4c183f3fb44ac6f73fb7a8f16a2694a91f988275cbf352f9 \
-    --hash=sha256:f846c260f483d1fd217fe5ed7c173fb109efa6b1fc8381c8b7552c5781756192 \
-    --hash=sha256:f91de7223d4c7b793867797bacd1ee53bfe7359bd70d27b7b58a04efbb9436c8 \
-    --hash=sha256:faae4860798c31530dd184046a900e652c95513796ef51a12bc086710c2eec4d \
-    --hash=sha256:fc579bf0f502e54926519451b920e875f433aceb4624a3646b3252b5caa9e0b6 \
-    --hash=sha256:fcc700eadbbccbf6bc1bcb9dbe0786b4b1cb91ca0dcda336eef5c2beed37b797 \
-    --hash=sha256:fd32ea360bcbb92d28933fc05ed09bffcb1704ba3fc7942e81db0fd4f81a7892 \
-    --hash=sha256:fdb7adb641a0d13bdcd4ef48e062363d8a9ad4a182ac7647ec88f695e719ae9f
-markdown-include==0.6.0 ; python_version >= "3.8" \
-    --hash=sha256:6f5d680e36f7780c7f0f61dca53ca581bd50d1b56137ddcd6353efafa0c3e4a2
-markdown==3.3.7 ; python_version >= "3.8" \
-    --hash=sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874 \
-    --hash=sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621
-markupsafe==2.1.3 ; python_version >= "3.8" \
-    --hash=sha256:05fb21170423db021895e1ea1e1f3ab3adb85d1c2333cbc2310f2a26bc77272e \
-    --hash=sha256:0a4e4a1aff6c7ac4cd55792abf96c915634c2b97e3cc1c7129578aa68ebd754e \
-    --hash=sha256:10bbfe99883db80bdbaff2dcf681dfc6533a614f700da1287707e8a5d78a8431 \
-    --hash=sha256:134da1eca9ec0ae528110ccc9e48041e0828d79f24121a1a146161103c76e686 \
-    --hash=sha256:14ff806850827afd6b07a5f32bd917fb7f45b046ba40c57abdb636674a8b559c \
-    --hash=sha256:1577735524cdad32f9f694208aa75e422adba74f1baee7551620e43a3141f559 \
-    --hash=sha256:1b40069d487e7edb2676d3fbdb2b0829ffa2cd63a2ec26c4938b2d34391b4ecc \
-    --hash=sha256:1b8dd8c3fd14349433c79fa8abeb573a55fc0fdd769133baac1f5e07abf54aeb \
-    --hash=sha256:1f67c7038d560d92149c060157d623c542173016c4babc0c1913cca0564b9939 \
-    --hash=sha256:282c2cb35b5b673bbcadb33a585408104df04f14b2d9b01d4c345a3b92861c2c \
-    --hash=sha256:2c1b19b3aaacc6e57b7e25710ff571c24d6c3613a45e905b1fde04d691b98ee0 \
-    --hash=sha256:2ef12179d3a291be237280175b542c07a36e7f60718296278d8593d21ca937d4 \
-    --hash=sha256:338ae27d6b8745585f87218a3f23f1512dbf52c26c28e322dbe54bcede54ccb9 \
-    --hash=sha256:3c0fae6c3be832a0a0473ac912810b2877c8cb9d76ca48de1ed31e1c68386575 \
-    --hash=sha256:3fd4abcb888d15a94f32b75d8fd18ee162ca0c064f35b11134be77050296d6ba \
-    --hash=sha256:42de32b22b6b804f42c5d98be4f7e5e977ecdd9ee9b660fda1a3edf03b11792d \
-    --hash=sha256:47d4f1c5f80fc62fdd7777d0d40a2e9dda0a05883ab11374334f6c4de38adffd \
-    --hash=sha256:504b320cd4b7eff6f968eddf81127112db685e81f7e36e75f9f84f0df46041c3 \
-    --hash=sha256:525808b8019e36eb524b8c68acdd63a37e75714eac50e988180b169d64480a00 \
-    --hash=sha256:56d9f2ecac662ca1611d183feb03a3fa4406469dafe241673d521dd5ae92a155 \
-    --hash=sha256:5bbe06f8eeafd38e5d0a4894ffec89378b6c6a625ff57e3028921f8ff59318ac \
-    --hash=sha256:65c1a9bcdadc6c28eecee2c119465aebff8f7a584dd719facdd9e825ec61ab52 \
-    --hash=sha256:68e78619a61ecf91e76aa3e6e8e33fc4894a2bebe93410754bd28fce0a8a4f9f \
-    --hash=sha256:69c0f17e9f5a7afdf2cc9fb2d1ce6aabdb3bafb7f38017c0b77862bcec2bbad8 \
-    --hash=sha256:6b2b56950d93e41f33b4223ead100ea0fe11f8e6ee5f641eb753ce4b77a7042b \
-    --hash=sha256:715d3562f79d540f251b99ebd6d8baa547118974341db04f5ad06d5ea3eb8007 \
-    --hash=sha256:787003c0ddb00500e49a10f2844fac87aa6ce977b90b0feaaf9de23c22508b24 \
-    --hash=sha256:7ef3cb2ebbf91e330e3bb937efada0edd9003683db6b57bb108c4001f37a02ea \
-    --hash=sha256:8023faf4e01efadfa183e863fefde0046de576c6f14659e8782065bcece22198 \
-    --hash=sha256:8758846a7e80910096950b67071243da3e5a20ed2546e6392603c096778d48e0 \
-    --hash=sha256:8afafd99945ead6e075b973fefa56379c5b5c53fd8937dad92c662da5d8fd5ee \
-    --hash=sha256:8c41976a29d078bb235fea9b2ecd3da465df42a562910f9022f1a03107bd02be \
-    --hash=sha256:8e254ae696c88d98da6555f5ace2279cf7cd5b3f52be2b5cf97feafe883b58d2 \
-    --hash=sha256:8f9293864fe09b8149f0cc42ce56e3f0e54de883a9de90cd427f191c346eb2e1 \
-    --hash=sha256:9402b03f1a1b4dc4c19845e5c749e3ab82d5078d16a2a4c2cd2df62d57bb0707 \
-    --hash=sha256:962f82a3086483f5e5f64dbad880d31038b698494799b097bc59c2edf392fce6 \
-    --hash=sha256:9aad3c1755095ce347e26488214ef77e0485a3c34a50c5a5e2471dff60b9dd9c \
-    --hash=sha256:9dcdfd0eaf283af041973bff14a2e143b8bd64e069f4c383416ecd79a81aab58 \
-    --hash=sha256:aa57bd9cf8ae831a362185ee444e15a93ecb2e344c8e52e4d721ea3ab6ef1823 \
-    --hash=sha256:aa7bd130efab1c280bed0f45501b7c8795f9fdbeb02e965371bbef3523627779 \
-    --hash=sha256:ab4a0df41e7c16a1392727727e7998a467472d0ad65f3ad5e6e765015df08636 \
-    --hash=sha256:ad9e82fb8f09ade1c3e1b996a6337afac2b8b9e365f926f5a61aacc71adc5b3c \
-    --hash=sha256:af598ed32d6ae86f1b747b82783958b1a4ab8f617b06fe68795c7f026abbdcad \
-    --hash=sha256:b076b6226fb84157e3f7c971a47ff3a679d837cf338547532ab866c57930dbee \
-    --hash=sha256:b7ff0f54cb4ff66dd38bebd335a38e2c22c41a8ee45aa608efc890ac3e3931bc \
-    --hash=sha256:bfce63a9e7834b12b87c64d6b155fdd9b3b96191b6bd334bf37db7ff1fe457f2 \
-    --hash=sha256:c011a4149cfbcf9f03994ec2edffcb8b1dc2d2aede7ca243746df97a5d41ce48 \
-    --hash=sha256:c9c804664ebe8f83a211cace637506669e7890fec1b4195b505c214e50dd4eb7 \
-    --hash=sha256:ca379055a47383d02a5400cb0d110cef0a776fc644cda797db0c5696cfd7e18e \
-    --hash=sha256:cb0932dc158471523c9637e807d9bfb93e06a95cbf010f1a38b98623b929ef2b \
-    --hash=sha256:cd0f502fe016460680cd20aaa5a76d241d6f35a1c3350c474bac1273803893fa \
-    --hash=sha256:ceb01949af7121f9fc39f7d27f91be8546f3fb112c608bc4029aef0bab86a2a5 \
-    --hash=sha256:d080e0a5eb2529460b30190fcfcc4199bd7f827663f858a226a81bc27beaa97e \
-    --hash=sha256:dd15ff04ffd7e05ffcb7fe79f1b98041b8ea30ae9234aed2a9168b5797c3effb \
-    --hash=sha256:df0be2b576a7abbf737b1575f048c23fb1d769f267ec4358296f31c2479db8f9 \
-    --hash=sha256:e09031c87a1e51556fdcb46e5bd4f59dfb743061cf93c4d6831bf894f125eb57 \
-    --hash=sha256:e4dd52d80b8c83fdce44e12478ad2e85c64ea965e75d66dbeafb0a3e77308fcc \
-    --hash=sha256:f698de3fd0c4e6972b92290a45bd9b1536bffe8c6759c62471efaa8acb4c37bc \
-    --hash=sha256:fec21693218efe39aa7f8599346e90c705afa52c5b31ae019b2e57e8f6542bb2 \
-    --hash=sha256:ffcc3f7c66b5f5b7931a5aa68fc9cecc51e685ef90282f4a82f0f5e9b704ad11
-matplotlib==3.7.3 ; python_version >= "3.8" \
-    --hash=sha256:085c33b27561d9c04386789d5aa5eb4a932ddef43cfcdd0e01735f9a6e85ce0c \
-    --hash=sha256:0a97af9d22e8ebedc9f00b043d9bbd29a375e9e10b656982012dded44c10fd77 \
-    --hash=sha256:122dcbf9be0086e2a95d9e5e0632dbf3bd5b65eaa68c369363310a6c87753059 \
-    --hash=sha256:12b4f6795efea037ce2d41e7c417ad8bd02d5719c6ad4a8450a0708f4a1cfb89 \
-    --hash=sha256:12f4c0dd8aa280d796c8772ea8265a14f11a04319baa3a16daa5556065e8baea \
-    --hash=sha256:165c8082bf8fc0360c24aa4724a22eaadbfd8c28bf1ccf7e94d685cad48261e4 \
-    --hash=sha256:1990955b11e7918d256cf3b956b10997f405b7917a3f1c7d8e69c1d15c7b1930 \
-    --hash=sha256:1a6094c6f8e8d18db631754df4fe9a34dec3caf074f6869a7db09f18f9b1d6b2 \
-    --hash=sha256:1f9c6c16597af660433ab330b59ee2934b832ee1fabcaf5cbde7b2add840f31e \
-    --hash=sha256:22d65d18b4ee8070a5fea5761d59293f1f9e2fac37ec9ce090463b0e629432fd \
-    --hash=sha256:236024f582e40dac39bca592258888b38ae47a9fed7b8de652d68d3d02d47d2b \
-    --hash=sha256:259999c05285cb993d7f2a419cea547863fa215379eda81f7254c9e932963729 \
-    --hash=sha256:272dba2f1b107790ed78ebf5385b8d14b27ad9e90419de340364b49fe549a993 \
-    --hash=sha256:336e88900c11441e458da01c8414fc57e04e17f9d3bb94958a76faa2652bcf6b \
-    --hash=sha256:39018a2b17592448fbfdf4b8352955e6c3905359939791d4ff429296494d1a0c \
-    --hash=sha256:3bf3a178c6504694cee8b88b353df0051583f2f6f8faa146f67115c27c856881 \
-    --hash=sha256:3f4e7fd5a6157e1d018ce2166ec8e531a481dd4a36f035b5c23edfe05a25419a \
-    --hash=sha256:40e3b9b450c6534f07278310c4e34caff41c2a42377e4b9d47b0f8d3ac1083a2 \
-    --hash=sha256:498a08267dc69dd8f24c4b5d7423fa584d7ce0027ba71f7881df05fc09b89bb7 \
-    --hash=sha256:4aab27d9e33293389e3c1d7c881d414a72bdfda0fedc3a6bf46c6fa88d9b8015 \
-    --hash=sha256:55de4cf7cd0071b8ebf203981b53ab64f988a0a1f897a2dff300a1124e8bcd8b \
-    --hash=sha256:591c123bed1cb4b9996fb60b41a6d89c2ec4943244540776c5f1283fb6960a53 \
-    --hash=sha256:67a410a9c9e07cbc83581eeea144bbe298870bf0ac0ee2f2e10a015ab7efee19 \
-    --hash=sha256:6eaa1cf0e94c936a26b78f6d756c5fbc12e0a58c8a68b7248a2a31456ce4e234 \
-    --hash=sha256:7153453669c9672b52095119fd21dd032d19225d48413a2871519b17db4b0fde \
-    --hash=sha256:747c6191d2e88ae854809e69aa358dbf852ff1a5738401b85c1cc9012309897a \
-    --hash=sha256:74bf57f505efea376097e948b7cdd87191a7ce8180616390aef496639edf601f \
-    --hash=sha256:755bafc10a46918ce9a39980009b54b02dd249594e5adf52f9c56acfddb5d0b7 \
-    --hash=sha256:78b2136cc6c5415b78977e0e8c608647d597204b05b1d9089ccf513c7d913733 \
-    --hash=sha256:7baf98c5ad59c5c4743ea884bb025cbffa52dacdfdac0da3e6021a285a90377e \
-    --hash=sha256:91e36a85ea639a1ba9f91427041eac064b04829945fe331a92617b6cb21d27e5 \
-    --hash=sha256:9c40cde976c36693cc0767e27cf5f443f91c23520060bd9496678364adfafe9c \
-    --hash=sha256:a7240259b4b9cbc62381f6378cff4d57af539162a18e832c1e48042fabc40b6b \
-    --hash=sha256:ac03377fd908aaee2312d0b11735753e907adb6f4d1d102de5e2425249693f6c \
-    --hash=sha256:c568e80e1c17f68a727f30f591926751b97b98314d8e59804f54f86ae6fa6a22 \
-    --hash=sha256:caf5eaaf7c68f8d7df269dfbcaf46f48a70ff482bfcebdcc97519671023f2a7d \
-    --hash=sha256:d48999c4b19b5a0c058c9cd828ff6fc7748390679f6cf9a2ad653a3e802c87d3 \
-    --hash=sha256:d5adc743de91e8e0b13df60deb1b1c285b8effea3d66223afceb14b63c9b05de \
-    --hash=sha256:dfc118642903a23e309b1da32886bb39a4314147d013e820c86b5fb4cb2e36d0 \
-    --hash=sha256:e594ee43c59ea39ca5c6244667cac9d017a3527febc31f5532ad9135cf7469ec \
-    --hash=sha256:e78707b751260b42b721507ad7aa60fe4026d7f51c74cca6b9cd8b123ebb633a \
-    --hash=sha256:ebd8470cc2a3594746ff0513aecbfa2c55ff6f58e6cef2efb1a54eb87c88ffa2 \
-    --hash=sha256:ec726b08a5275d827aa91bb951e68234a4423adb91cf65bc0fcdc0f2777663f7 \
-    --hash=sha256:edf54cac8ee3603f3093616b40a931e8c063969756a4d78a86e82c2fea9659f7 \
-    --hash=sha256:ee152a88a0da527840a426535514b6ed8ac4240eb856b1da92cf48124320e346 \
-    --hash=sha256:f09b3dd6bdeb588de91f853bbb2d6f0ff8ab693485b0c49035eaa510cb4f142e \
-    --hash=sha256:faa3d12d8811d08d14080a8b7b9caea9a457dc495350166b56df0db4b9909ef5
-mergedeep==1.3.4 ; python_version >= "3.8" \
-    --hash=sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8 \
-    --hash=sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307
-mkdocs-autorefs==0.5.0 ; python_version >= "3.8" \
-    --hash=sha256:7930fcb8ac1249f10e683967aeaddc0af49d90702af111a5e390e8b20b3d97ff \
-    --hash=sha256:9a5054a94c08d28855cfab967ada10ed5be76e2bfad642302a610b252c3274c0
-mkdocs-material-extensions==1.3 ; python_version >= "3.8" \
-    --hash=sha256:0297cc48ba68a9fdd1ef3780a3b41b534b0d0df1d1181a44676fda5f464eeadc \
-    --hash=sha256:f0446091503acb110a7cab9349cbc90eeac51b58d1caa92a704a81ca1e24ddbd
-mkdocs-material==9.1.21 ; python_version >= "3.8" \
-    --hash=sha256:58bb2f11ef240632e176d6f0f7d1cff06be1d11c696a5a1b553b808b4280ed47 \
-    --hash=sha256:71940cdfca84ab296b6362889c25395b1621273fb16c93deda257adb7ff44ec8
-mkdocs==1.5.3 ; python_version >= "3.8" \
-    --hash=sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1 \
-    --hash=sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2
-mkdocstrings-python==1.7.3 ; python_version >= "3.8" \
-    --hash=sha256:2439d6ad3e34f0bb4c643b845fb3c06ae9233499a1736f9fa273424b75cc5894 \
-    --hash=sha256:c20128fa96c24dbc6437b10dfedaf33b0415d4503e51ce9ce5e84b271278268e
-mkdocstrings==0.23.0 ; python_version >= "3.8" \
-    --hash=sha256:051fa4014dfcd9ed90254ae91de2dbb4f24e166347dae7be9a997fe16316c65e \
-    --hash=sha256:d9c6a37ffbe7c14a7a54ef1258c70b8d394e6a33a1c80832bce40b9567138d1c
-mypy-extensions==1.0.0 ; python_version >= "3.8" \
-    --hash=sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d \
-    --hash=sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782
-mypy==1.6.1 ; python_version >= "3.8" \
-    --hash=sha256:19f905bcfd9e167159b3d63ecd8cb5e696151c3e59a1742e79bc3bcb540c42c7 \
-    --hash=sha256:21a1ad938fee7d2d96ca666c77b7c494c3c5bd88dff792220e1afbebb2925b5e \
-    --hash=sha256:40b1844d2e8b232ed92e50a4bd11c48d2daa351f9deee6c194b83bf03e418b0c \
-    --hash=sha256:41697773aa0bf53ff917aa077e2cde7aa50254f28750f9b88884acea38a16169 \
-    --hash=sha256:49ae115da099dcc0922a7a895c1eec82c1518109ea5c162ed50e3b3594c71208 \
-    --hash=sha256:4c46b51de523817a0045b150ed11b56f9fff55f12b9edd0f3ed35b15a2809de0 \
-    --hash=sha256:4cbe68ef919c28ea561165206a2dcb68591c50f3bcf777932323bc208d949cf1 \
-    --hash=sha256:4d01c00d09a0be62a4ca3f933e315455bde83f37f892ba4b08ce92f3cf44bcc1 \
-    --hash=sha256:59a0d7d24dfb26729e0a068639a6ce3500e31d6655df8557156c51c1cb874ce7 \
-    --hash=sha256:68351911e85145f582b5aa6cd9ad666c8958bcae897a1bfda8f4940472463c45 \
-    --hash=sha256:7274b0c57737bd3476d2229c6389b2ec9eefeb090bbaf77777e9d6b1b5a9d143 \
-    --hash=sha256:81af8adaa5e3099469e7623436881eff6b3b06db5ef75e6f5b6d4871263547e5 \
-    --hash=sha256:82e469518d3e9a321912955cc702d418773a2fd1e91c651280a1bda10622f02f \
-    --hash=sha256:8b27958f8c76bed8edaa63da0739d76e4e9ad4ed325c814f9b3851425582a3cd \
-    --hash=sha256:8c223fa57cb154c7eab5156856c231c3f5eace1e0bed9b32a24696b7ba3c3245 \
-    --hash=sha256:8f57e6b6927a49550da3d122f0cb983d400f843a8a82e65b3b380d3d7259468f \
-    --hash=sha256:925cd6a3b7b55dfba252b7c4561892311c5358c6b5a601847015a1ad4eb7d332 \
-    --hash=sha256:a43ef1c8ddfdb9575691720b6352761f3f53d85f1b57d7745701041053deff30 \
-    --hash=sha256:a8032e00ce71c3ceb93eeba63963b864bf635a18f6c0c12da6c13c450eedb183 \
-    --hash=sha256:b96ae2c1279d1065413965c607712006205a9ac541895004a1e0d4f281f2ff9f \
-    --hash=sha256:bb8ccb4724f7d8601938571bf3f24da0da791fe2db7be3d9e79849cb64e0ae85 \
-    --hash=sha256:bbaf4662e498c8c2e352da5f5bca5ab29d378895fa2d980630656178bd607c46 \
-    --hash=sha256:cfd13d47b29ed3bbaafaff7d8b21e90d827631afda134836962011acb5904b71 \
-    --hash=sha256:d4473c22cc296425bbbce7e9429588e76e05bc7342da359d6520b6427bf76660 \
-    --hash=sha256:d8fbb68711905f8912e5af474ca8b78d077447d8f3918997fecbf26943ff3cbb \
-    --hash=sha256:e5012e5cc2ac628177eaac0e83d622b2dd499e28253d4107a08ecc59ede3fc2c \
-    --hash=sha256:eb4f18589d196a4cbe5290b435d135dee96567e07c2b2d43b5c4621b6501531a
-nodeenv==1.8.0 ; python_version >= "3.8" \
-    --hash=sha256:d51e0c37e64fbf47d017feac3145cdbb58836d7eee8c6f6d3b6880c5456227d2 \
-    --hash=sha256:df865724bb3c3adc86b3876fa209771517b0cfe596beff01a92700e0e8be4cec
-numpy==1.24.4 ; python_version >= "3.8" \
-    --hash=sha256:04640dab83f7c6c85abf9cd729c5b65f1ebd0ccf9de90b270cd61935eef0197f \
-    --hash=sha256:1452241c290f3e2a312c137a9999cdbf63f78864d63c79039bda65ee86943f61 \
-    --hash=sha256:222e40d0e2548690405b0b3c7b21d1169117391c2e82c378467ef9ab4c8f0da7 \
-    --hash=sha256:2541312fbf09977f3b3ad449c4e5f4bb55d0dbf79226d7724211acc905049400 \
-    --hash=sha256:31f13e25b4e304632a4619d0e0777662c2ffea99fcae2029556b17d8ff958aef \
-    --hash=sha256:4602244f345453db537be5314d3983dbf5834a9701b7723ec28923e2889e0bb2 \
-    --hash=sha256:4979217d7de511a8d57f4b4b5b2b965f707768440c17cb70fbf254c4b225238d \
-    --hash=sha256:4c21decb6ea94057331e111a5bed9a79d335658c27ce2adb580fb4d54f2ad9bc \
-    --hash=sha256:6620c0acd41dbcb368610bb2f4d83145674040025e5536954782467100aa8835 \
-    --hash=sha256:692f2e0f55794943c5bfff12b3f56f99af76f902fc47487bdfe97856de51a706 \
-    --hash=sha256:7215847ce88a85ce39baf9e89070cb860c98fdddacbaa6c0da3ffb31b3350bd5 \
-    --hash=sha256:79fc682a374c4a8ed08b331bef9c5f582585d1048fa6d80bc6c35bc384eee9b4 \
-    --hash=sha256:7ffe43c74893dbf38c2b0a1f5428760a1a9c98285553c89e12d70a96a7f3a4d6 \
-    --hash=sha256:80f5e3a4e498641401868df4208b74581206afbee7cf7b8329daae82676d9463 \
-    --hash=sha256:95f7ac6540e95bc440ad77f56e520da5bf877f87dca58bd095288dce8940532a \
-    --hash=sha256:9667575fb6d13c95f1b36aca12c5ee3356bf001b714fc354eb5465ce1609e62f \
-    --hash=sha256:a5425b114831d1e77e4b5d812b69d11d962e104095a5b9c3b641a218abcc050e \
-    --hash=sha256:b4bea75e47d9586d31e892a7401f76e909712a0fd510f58f5337bea9572c571e \
-    --hash=sha256:b7b1fc9864d7d39e28f41d089bfd6353cb5f27ecd9905348c24187a768c79694 \
-    --hash=sha256:befe2bf740fd8373cf56149a5c23a0f601e82869598d41f8e188a0e9869926f8 \
-    --hash=sha256:c0bfb52d2169d58c1cdb8cc1f16989101639b34c7d3ce60ed70b19c63eba0b64 \
-    --hash=sha256:d11efb4dbecbdf22508d55e48d9c8384db795e1b7b51ea735289ff96613ff74d \
-    --hash=sha256:dd80e219fd4c71fc3699fc1dadac5dcf4fd882bfc6f7ec53d30fa197b8ee22dc \
-    --hash=sha256:e2926dac25b313635e4d6cf4dc4e51c8c0ebfed60b801c799ffc4c32bf3d1254 \
-    --hash=sha256:e98f220aa76ca2a977fe435f5b04d7b3470c0a2e6312907b37ba6068f26787f2 \
-    --hash=sha256:ed094d4f0c177b1b8e7aa9cba7d6ceed51c0e569a5318ac0ca9a090680a6a1b1 \
-    --hash=sha256:f136bab9c2cfd8da131132c2cf6cc27331dd6fae65f95f69dcd4ae3c3639c810 \
-    --hash=sha256:f3a86ed21e4f87050382c7bc96571755193c4c1392490744ac73d660e8f564a9
-packaging==23.2 ; python_version >= "3.8" \
-    --hash=sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5 \
-    --hash=sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7
-pathspec==0.11.2 ; python_version >= "3.8" \
-    --hash=sha256:1d6ed233af05e679efb96b1851550ea95bbb64b7c490b0f5aa52996c11e92a20 \
-    --hash=sha256:e0d8d0ac2f12da61956eb2306b69f9469b42f4deb0f3cb6ed47b9cce9996ced3
-pillow==10.1.0 ; python_version >= "3.8" \
-    --hash=sha256:00f438bb841382b15d7deb9a05cc946ee0f2c352653c7aa659e75e592f6fa17d \
-    --hash=sha256:0248f86b3ea061e67817c47ecbe82c23f9dd5d5226200eb9090b3873d3ca32de \
-    --hash=sha256:04f6f6149f266a100374ca3cc368b67fb27c4af9f1cc8cb6306d849dcdf12616 \
-    --hash=sha256:062a1610e3bc258bff2328ec43f34244fcec972ee0717200cb1425214fe5b839 \
-    --hash=sha256:0a026c188be3b443916179f5d04548092e253beb0c3e2ee0a4e2cdad72f66099 \
-    --hash=sha256:0f7c276c05a9767e877a0b4c5050c8bee6a6d960d7f0c11ebda6b99746068c2a \
-    --hash=sha256:1a8413794b4ad9719346cd9306118450b7b00d9a15846451549314a58ac42219 \
-    --hash=sha256:1ab05f3db77e98f93964697c8efc49c7954b08dd61cff526b7f2531a22410106 \
-    --hash=sha256:1c3ac5423c8c1da5928aa12c6e258921956757d976405e9467c5f39d1d577a4b \
-    --hash=sha256:1c41d960babf951e01a49c9746f92c5a7e0d939d1652d7ba30f6b3090f27e412 \
-    --hash=sha256:1fafabe50a6977ac70dfe829b2d5735fd54e190ab55259ec8aea4aaea412fa0b \
-    --hash=sha256:1fb29c07478e6c06a46b867e43b0bcdb241b44cc52be9bc25ce5944eed4648e7 \
-    --hash=sha256:24fadc71218ad2b8ffe437b54876c9382b4a29e030a05a9879f615091f42ffc2 \
-    --hash=sha256:2cdc65a46e74514ce742c2013cd4a2d12e8553e3a2563c64879f7c7e4d28bce7 \
-    --hash=sha256:2ef6721c97894a7aa77723740a09547197533146fba8355e86d6d9a4a1056b14 \
-    --hash=sha256:3b834f4b16173e5b92ab6566f0473bfb09f939ba14b23b8da1f54fa63e4b623f \
-    --hash=sha256:3d929a19f5469b3f4df33a3df2983db070ebb2088a1e145e18facbc28cae5b27 \
-    --hash=sha256:41f67248d92a5e0a2076d3517d8d4b1e41a97e2df10eb8f93106c89107f38b57 \
-    --hash=sha256:47e5bf85b80abc03be7455c95b6d6e4896a62f6541c1f2ce77a7d2bb832af262 \
-    --hash=sha256:4d0152565c6aa6ebbfb1e5d8624140a440f2b99bf7afaafbdbf6430426497f28 \
-    --hash=sha256:50d08cd0a2ecd2a8657bd3d82c71efd5a58edb04d9308185d66c3a5a5bed9610 \
-    --hash=sha256:61f1a9d247317fa08a308daaa8ee7b3f760ab1809ca2da14ecc88ae4257d6172 \
-    --hash=sha256:6932a7652464746fcb484f7fc3618e6503d2066d853f68a4bd97193a3996e273 \
-    --hash=sha256:7a7e3daa202beb61821c06d2517428e8e7c1aab08943e92ec9e5755c2fc9ba5e \
-    --hash=sha256:7dbaa3c7de82ef37e7708521be41db5565004258ca76945ad74a8e998c30af8d \
-    --hash=sha256:7df5608bc38bd37ef585ae9c38c9cd46d7c81498f086915b0f97255ea60c2818 \
-    --hash=sha256:806abdd8249ba3953c33742506fe414880bad78ac25cc9a9b1c6ae97bedd573f \
-    --hash=sha256:883f216eac8712b83a63f41b76ddfb7b2afab1b74abbb413c5df6680f071a6b9 \
-    --hash=sha256:912e3812a1dbbc834da2b32299b124b5ddcb664ed354916fd1ed6f193f0e2d01 \
-    --hash=sha256:937bdc5a7f5343d1c97dc98149a0be7eb9704e937fe3dc7140e229ae4fc572a7 \
-    --hash=sha256:9882a7451c680c12f232a422730f986a1fcd808da0fd428f08b671237237d651 \
-    --hash=sha256:9a92109192b360634a4489c0c756364c0c3a2992906752165ecb50544c251312 \
-    --hash=sha256:9d7bc666bd8c5a4225e7ac71f2f9d12466ec555e89092728ea0f5c0c2422ea80 \
-    --hash=sha256:a5f63b5a68daedc54c7c3464508d8c12075e56dcfbd42f8c1bf40169061ae666 \
-    --hash=sha256:a646e48de237d860c36e0db37ecaecaa3619e6f3e9d5319e527ccbc8151df061 \
-    --hash=sha256:a89b8312d51715b510a4fe9fc13686283f376cfd5abca8cd1c65e4c76e21081b \
-    --hash=sha256:a92386125e9ee90381c3369f57a2a50fa9e6aa8b1cf1d9c4b200d41a7dd8e992 \
-    --hash=sha256:ae88931f93214777c7a3aa0a8f92a683f83ecde27f65a45f95f22d289a69e593 \
-    --hash=sha256:afc8eef765d948543a4775f00b7b8c079b3321d6b675dde0d02afa2ee23000b4 \
-    --hash=sha256:b0eb01ca85b2361b09480784a7931fc648ed8b7836f01fb9241141b968feb1db \
-    --hash=sha256:b1c25762197144e211efb5f4e8ad656f36c8d214d390585d1d21281f46d556ba \
-    --hash=sha256:b4005fee46ed9be0b8fb42be0c20e79411533d1fd58edabebc0dd24626882cfd \
-    --hash=sha256:b920e4d028f6442bea9a75b7491c063f0b9a3972520731ed26c83e254302eb1e \
-    --hash=sha256:baada14941c83079bf84c037e2d8b7506ce201e92e3d2fa0d1303507a8538212 \
-    --hash=sha256:bb40c011447712d2e19cc261c82655f75f32cb724788df315ed992a4d65696bb \
-    --hash=sha256:c0949b55eb607898e28eaccb525ab104b2d86542a85c74baf3a6dc24002edec2 \
-    --hash=sha256:c9aeea7b63edb7884b031a35305629a7593272b54f429a9869a4f63a1bf04c34 \
-    --hash=sha256:cfe96560c6ce2f4c07d6647af2d0f3c54cc33289894ebd88cfbb3bcd5391e256 \
-    --hash=sha256:d27b5997bdd2eb9fb199982bb7eb6164db0426904020dc38c10203187ae2ff2f \
-    --hash=sha256:d921bc90b1defa55c9917ca6b6b71430e4286fc9e44c55ead78ca1a9f9eba5f2 \
-    --hash=sha256:e6bf8de6c36ed96c86ea3b6e1d5273c53f46ef518a062464cd7ef5dd2cf92e38 \
-    --hash=sha256:eaed6977fa73408b7b8a24e8b14e59e1668cfc0f4c40193ea7ced8e210adf996 \
-    --hash=sha256:fa1d323703cfdac2036af05191b969b910d8f115cf53093125e4058f62012c9a \
-    --hash=sha256:fe1e26e1ffc38be097f0ba1d0d07fcade2bcfd1d023cda5b29935ae8052bd793
-platformdirs==3.11.0 ; python_version >= "3.8" \
-    --hash=sha256:cf8ee52a3afdb965072dcc652433e0c7e3e40cf5ea1477cd4b3b1d2eb75495b3 \
-    --hash=sha256:e9d171d00af68be50e9202731309c4e658fd8bc76f55c11c7dd760d023bda68e
-pluggy==1.3.0 ; python_version >= "3.8" \
-    --hash=sha256:cf61ae8f126ac6f7c451172cf30e3e43d3ca77615509771b3a984a0730651e12 \
-    --hash=sha256:d89c696a773f8bd377d18e5ecda92b7a3793cbe66c87060a6fb58c7b6e1061f7
-pre-commit==2.21.0 ; python_version >= "3.8" \
-    --hash=sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658 \
-    --hash=sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad
-pyerfa==2.0.0.3 ; python_version >= "3.8" \
-    --hash=sha256:06d4f08e96867b1fc3ae9a9e4b38693ed0806463288efc41473ad16e14774504 \
-    --hash=sha256:09af83540e23a7d61a8368b0514b3daa4ed967e1e52d0add4f501f58c500dd7f \
-    --hash=sha256:0b8c5e74d48a505a014e855cd4c7be11604901d94fd6f34b685f6720b7b20ed8 \
-    --hash=sha256:1033fdb890ec70d3a511e20a464afc8abbea2180108f27b14d8f1d1addc38cbe \
-    --hash=sha256:170a83bd0243da518119b846f296cf33fa03f1f884a88578c1a38560182cf64e \
-    --hash=sha256:23e5efcf96ed7161d74f79ca261d255e1f36988843d22cd97d8f60fe9c868d44 \
-    --hash=sha256:24d89ead30edc6038408336ad9b696683e74c4eef550708fca6afef3ecd5b010 \
-    --hash=sha256:2ccba04de166d81bdd3adcf10428d908ce2f3a56ed1c2767d740fec12680edbd \
-    --hash=sha256:2d8c0dbb17119e52def33f9d6dbf2deaf2113ed3e657b6ff692df9b6a3598397 \
-    --hash=sha256:36f42ee01a62c6cbba58103e6f8e600b21ad3a71262dccf03d476efb4a20ea71 \
-    --hash=sha256:3b55f7278c1dd362648d7956e1a5365ade5fed2fe5541b721b3ceb5271128892 \
-    --hash=sha256:3df87743e27588c5bd5e1f3a886629b3277fdd418059ca048420d33169376775 \
-    --hash=sha256:3ecb598924ddb4ea2b06efc6f1e55ca70897ed178a690e2eaa1e290448466c7c \
-    --hash=sha256:3ecd6167b48bb8f1922fae7b49554616f2e7382748a4320ad46ebd7e2cc62f3d \
-    --hash=sha256:4ea7ca03ecc440224c2bed8fb136fadf6cf8aea8ba67d717f635116f30c8cc8c \
-    --hash=sha256:4fa9fceeb78057bfff7ae3aa6cdad3f1b193722de22bdbb75319256f4a9e2f76 \
-    --hash=sha256:51aa6e0faa4aa9ad8f0eef1c47fec76c5bebc0da7023a436089bdd6e5cfd625f \
-    --hash=sha256:5447bb45ddedde3052693c86b941a4908f5dbeb4a697bda45b5b89de92cfb74a \
-    --hash=sha256:61f1097ac2ee8c15a2a636cdfb99340d708574d66f4610456bd457d1e6b852f4 \
-    --hash=sha256:676515861ca3f0cb9d7e693389233e7126413a5ba93a0cc4d36b8ca933951e8d \
-    --hash=sha256:6a07444fd53a5dd18d7955f86f8d9b1be9a68ceb143e1145c0019a310c913c04 \
-    --hash=sha256:7354753addba5261ec1cbf1ba45784ed3a5c42da565ecc6e0aa36b7a17fa4689 \
-    --hash=sha256:73bf7d23f069d47632a2feeb1e73454b10392c4f3c16116017a6983f1f0e9b2b \
-    --hash=sha256:780b0f90adf500b8ba24e9d509a690576a7e8287e354cfb90227c5963690d3fc \
-    --hash=sha256:7c24e7960c6cdd3fa3f4dba5f3444a106ad48c94ff0b19eebaee06a142c18c52 \
-    --hash=sha256:7d01c341c45b860ee5c7585ef003118c8015e9d65c30668d2f5bf657e1dcdd68 \
-    --hash=sha256:7f9eabfefa5317ce58fe22480102902f10f270fc64a5636c010f7c0b7e0fb032 \
-    --hash=sha256:88aa1acedf298d255cc4b0740ee11a3b303b71763dba2f039d48abf0a95cf9df \
-    --hash=sha256:8a1edd2cbe4ead3bf9a51e578d5d83bdd7ab3b3ccb69e09b89a4c42aa5b35ffb \
-    --hash=sha256:96ea688341176ae6220cc4743cda655549d71e3e3b60c5a99d02d5912d0ddf55 \
-    --hash=sha256:a04c3b715c924b6f972dd440a94a701a16a07700bc8ba9e88b1df765bdc36ad0 \
-    --hash=sha256:a438865894d226247dcfcb60d683ae075a52716504537052371b2b73458fe4fc \
-    --hash=sha256:a8a2029fc62ff2369d01219f66a5ce6aed35ef33eddb06118b6c27e8573a9ed8 \
-    --hash=sha256:d56f6b5a0a3ed7b80d630041829463a872946df277259b5453298842d42a54a4 \
-    --hash=sha256:d77fbbfa58350c194ccb99e5d93aa05d3c2b14d5aad8b662d93c6ad9fff41f39 \
-    --hash=sha256:da888da2c8db5a78273fbf0af4e74f04e2d312d371c3c021cf6c3b14fa60fe3b \
-    --hash=sha256:daf7364e475cff1f973e2fcf6962de9df9642c8802b010e29b2c592ae337e3c5 \
-    --hash=sha256:e8458421166f6ffe2e259aaf4aaa6e802d6539649a40e3194a81d30dccdc167a \
-    --hash=sha256:f0e9d0b122c454bcad5dbd0c3283b200783031d3f99ca9c550f49a7a7d4c41ea \
-    --hash=sha256:f1819e0d95ff8dead80614f8063919d82b2dbb55437b6c0109d3393c1ab55954
-pygments==2.16.1 ; python_version >= "3.8" \
-    --hash=sha256:13fc09fa63bc8d8671a6d247e1eb303c4b343eaee81d861f3404db2935653692 \
-    --hash=sha256:1daff0494820c69bc8941e407aa20f577374ee88364ee10a98fdbe0aece96e29
-pymdown-extensions==9.11 ; python_version >= "3.8" \
-    --hash=sha256:a499191d8d869f30339de86fcf072a787e86c42b6f16f280f5c2cf174182b7f3 \
-    --hash=sha256:f7e86c1d3981f23d9dc43294488ecb54abadd05b0be4bf8f0e15efc90f7853ff
-pyparsing==3.1.1 ; python_version >= "3.8" \
-    --hash=sha256:32c7c0b711493c72ff18a981d24f28aaf9c1fb7ed5e9667c9e84e3db623bdbfb \
-    --hash=sha256:ede28a1a32462f5a9705e07aea48001a08f7cf81a021585011deba701581a0db
-pytest-cov==4.1.0 ; python_version >= "3.8" \
-    --hash=sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6 \
-    --hash=sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a
-pytest==7.4.3 ; python_version >= "3.8" \
-    --hash=sha256:0d009c083ea859a71b76adf7c1d502e4bc170b80a8ef002da5806527b9591fac \
-    --hash=sha256:d989d136982de4e3b29dabcc838ad581c64e8ed52c11fbe86ddebd9da0818cd5
-python-dateutil==2.8.2 ; python_version >= "3.8" \
-    --hash=sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86 \
-    --hash=sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9
-pyyaml-env-tag==0.1 ; python_version >= "3.8" \
-    --hash=sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb \
-    --hash=sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069
-pyyaml==6.0.1 ; python_version >= "3.8" \
-    --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
-    --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
-    --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
-    --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
-    --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
-    --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
-    --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
-    --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
-    --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
-    --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
-    --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
-    --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
-    --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
-    --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
-    --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
-    --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
-    --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
-    --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
-    --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
-    --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
-    --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
-    --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
-    --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
-    --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
-    --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
-    --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
-    --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
-    --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
-    --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
-    --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
-    --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
-    --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
-    --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
-    --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
-    --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
-    --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
-    --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
-    --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
-    --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
-    --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
-    --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
-    --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
-    --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
-    --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
-    --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
-    --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
-    --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
-    --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
-    --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
-    --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
-regex==2023.10.3 ; python_version >= "3.8" \
-    --hash=sha256:00ba3c9818e33f1fa974693fb55d24cdc8ebafcb2e4207680669d8f8d7cca79a \
-    --hash=sha256:00e871d83a45eee2f8688d7e6849609c2ca2a04a6d48fba3dff4deef35d14f07 \
-    --hash=sha256:06e9abc0e4c9ab4779c74ad99c3fc10d3967d03114449acc2c2762ad4472b8ca \
-    --hash=sha256:0b9ac09853b2a3e0d0082104036579809679e7715671cfbf89d83c1cb2a30f58 \
-    --hash=sha256:0d47840dc05e0ba04fe2e26f15126de7c755496d5a8aae4a08bda4dd8d646c54 \
-    --hash=sha256:0f649fa32fe734c4abdfd4edbb8381c74abf5f34bc0b3271ce687b23729299ed \
-    --hash=sha256:107ac60d1bfdc3edb53be75e2a52aff7481b92817cfdddd9b4519ccf0e54a6ff \
-    --hash=sha256:11175910f62b2b8c055f2b089e0fedd694fe2be3941b3e2633653bc51064c528 \
-    --hash=sha256:12bd4bc2c632742c7ce20db48e0d99afdc05e03f0b4c1af90542e05b809a03d9 \
-    --hash=sha256:16f8740eb6dbacc7113e3097b0a36065a02e37b47c936b551805d40340fb9971 \
-    --hash=sha256:1c0e8fae5b27caa34177bdfa5a960c46ff2f78ee2d45c6db15ae3f64ecadde14 \
-    --hash=sha256:2c54e23836650bdf2c18222c87f6f840d4943944146ca479858404fedeb9f9af \
-    --hash=sha256:3367007ad1951fde612bf65b0dffc8fd681a4ab98ac86957d16491400d661302 \
-    --hash=sha256:36362386b813fa6c9146da6149a001b7bd063dabc4d49522a1f7aa65b725c7ec \
-    --hash=sha256:39807cbcbe406efca2a233884e169d056c35aa7e9f343d4e78665246a332f597 \
-    --hash=sha256:39cdf8d141d6d44e8d5a12a8569d5a227f645c87df4f92179bd06e2e2705e76b \
-    --hash=sha256:3b2c3502603fab52d7619b882c25a6850b766ebd1b18de3df23b2f939360e1bd \
-    --hash=sha256:3ccf2716add72f80714b9a63899b67fa711b654be3fcdd34fa391d2d274ce767 \
-    --hash=sha256:3fef4f844d2290ee0ba57addcec17eec9e3df73f10a2748485dfd6a3a188cc0f \
-    --hash=sha256:4023e2efc35a30e66e938de5aef42b520c20e7eda7bb5fb12c35e5d09a4c43f6 \
-    --hash=sha256:4a3ee019a9befe84fa3e917a2dd378807e423d013377a884c1970a3c2792d293 \
-    --hash=sha256:4a8bf76e3182797c6b1afa5b822d1d5802ff30284abe4599e1247be4fd6b03be \
-    --hash=sha256:4a992f702c9be9c72fa46f01ca6e18d131906a7180950958f766c2aa294d4b41 \
-    --hash=sha256:4c34d4f73ea738223a094d8e0ffd6d2c1a1b4c175da34d6b0de3d8d69bee6bcc \
-    --hash=sha256:4cd1bccf99d3ef1ab6ba835308ad85be040e6a11b0977ef7ea8c8005f01a3c29 \
-    --hash=sha256:4ef80829117a8061f974b2fda8ec799717242353bff55f8a29411794d635d964 \
-    --hash=sha256:58837f9d221744d4c92d2cf7201c6acd19623b50c643b56992cbd2b745485d3d \
-    --hash=sha256:5a8f91c64f390ecee09ff793319f30a0f32492e99f5dc1c72bc361f23ccd0a9a \
-    --hash=sha256:5addc9d0209a9afca5fc070f93b726bf7003bd63a427f65ef797a931782e7edc \
-    --hash=sha256:6239d4e2e0b52c8bd38c51b760cd870069f0bdf99700a62cd509d7a031749a55 \
-    --hash=sha256:66e2fe786ef28da2b28e222c89502b2af984858091675044d93cb50e6f46d7af \
-    --hash=sha256:69c0771ca5653c7d4b65203cbfc5e66db9375f1078689459fe196fe08b7b4930 \
-    --hash=sha256:6ac965a998e1388e6ff2e9781f499ad1eaa41e962a40d11c7823c9952c77123e \
-    --hash=sha256:6c56c3d47da04f921b73ff9415fbaa939f684d47293f071aa9cbb13c94afc17d \
-    --hash=sha256:6f85739e80d13644b981a88f529d79c5bdf646b460ba190bffcaf6d57b2a9863 \
-    --hash=sha256:706e7b739fdd17cb89e1fbf712d9dc21311fc2333f6d435eac2d4ee81985098c \
-    --hash=sha256:741ba2f511cc9626b7561a440f87d658aabb3d6b744a86a3c025f866b4d19e7f \
-    --hash=sha256:7434a61b158be563c1362d9071358f8ab91b8d928728cd2882af060481244c9e \
-    --hash=sha256:76066d7ff61ba6bf3cb5efe2428fc82aac91802844c022d849a1f0f53820502d \
-    --hash=sha256:7979b834ec7a33aafae34a90aad9f914c41fd6eaa8474e66953f3f6f7cbd4368 \
-    --hash=sha256:7eece6fbd3eae4a92d7c748ae825cbc1ee41a89bb1c3db05b5578ed3cfcfd7cb \
-    --hash=sha256:7ef1e014eed78ab650bef9a6a9cbe50b052c0aebe553fb2881e0453717573f52 \
-    --hash=sha256:81dce2ddc9f6e8f543d94b05d56e70d03a0774d32f6cca53e978dc01e4fc75b8 \
-    --hash=sha256:82fcc1f1cc3ff1ab8a57ba619b149b907072e750815c5ba63e7aa2e1163384a4 \
-    --hash=sha256:8d1f21af4c1539051049796a0f50aa342f9a27cde57318f2fc41ed50b0dbc4ac \
-    --hash=sha256:90a79bce019c442604662d17bf69df99090e24cdc6ad95b18b6725c2988a490e \
-    --hash=sha256:9145f092b5d1977ec8c0ab46e7b3381b2fd069957b9862a43bd383e5c01d18c2 \
-    --hash=sha256:91dc1d531f80c862441d7b66c4505cd6ea9d312f01fb2f4654f40c6fdf5cc37a \
-    --hash=sha256:979c24cbefaf2420c4e377ecd1f165ea08cc3d1fbb44bdc51bccbbf7c66a2cb4 \
-    --hash=sha256:994645a46c6a740ee8ce8df7911d4aee458d9b1bc5639bc968226763d07f00fa \
-    --hash=sha256:9b98b7681a9437262947f41c7fac567c7e1f6eddd94b0483596d320092004533 \
-    --hash=sha256:9c6b4d23c04831e3ab61717a707a5d763b300213db49ca680edf8bf13ab5d91b \
-    --hash=sha256:9c6d0ced3c06d0f183b73d3c5920727268d2201aa0fe6d55c60d68c792ff3588 \
-    --hash=sha256:9fd88f373cb71e6b59b7fa597e47e518282455c2734fd4306a05ca219a1991b0 \
-    --hash=sha256:a8f4e49fc3ce020f65411432183e6775f24e02dff617281094ba6ab079ef0915 \
-    --hash=sha256:a9e908ef5889cda4de038892b9accc36d33d72fb3e12c747e2799a0e806ec841 \
-    --hash=sha256:ad08a69728ff3c79866d729b095872afe1e0557251da4abb2c5faff15a91d19a \
-    --hash=sha256:adbccd17dcaff65704c856bd29951c58a1bd4b2b0f8ad6b826dbd543fe740988 \
-    --hash=sha256:b0c7d2f698e83f15228ba41c135501cfe7d5740181d5903e250e47f617eb4292 \
-    --hash=sha256:b3ab05a182c7937fb374f7e946f04fb23a0c0699c0450e9fb02ef567412d2fa3 \
-    --hash=sha256:b6104f9a46bd8743e4f738afef69b153c4b8b592d35ae46db07fc28ae3d5fb7c \
-    --hash=sha256:ba7cd6dc4d585ea544c1412019921570ebd8a597fabf475acc4528210d7c4a6f \
-    --hash=sha256:bc72c231f5449d86d6c7d9cc7cd819b6eb30134bb770b8cfdc0765e48ef9c420 \
-    --hash=sha256:bce8814b076f0ce5766dc87d5a056b0e9437b8e0cd351b9a6c4e1134a7dfbda9 \
-    --hash=sha256:be5e22bbb67924dea15039c3282fa4cc6cdfbe0cbbd1c0515f9223186fc2ec5f \
-    --hash=sha256:be6b7b8d42d3090b6c80793524fa66c57ad7ee3fe9722b258aec6d0672543fd0 \
-    --hash=sha256:bfe50b61bab1b1ec260fa7cd91106fa9fece57e6beba05630afe27c71259c59b \
-    --hash=sha256:bff507ae210371d4b1fe316d03433ac099f184d570a1a611e541923f78f05037 \
-    --hash=sha256:c148bec483cc4b421562b4bcedb8e28a3b84fcc8f0aa4418e10898f3c2c0eb9b \
-    --hash=sha256:c15ad0aee158a15e17e0495e1e18741573d04eb6da06d8b84af726cfc1ed02ee \
-    --hash=sha256:c2169b2dcabf4e608416f7f9468737583ce5f0a6e8677c4efbf795ce81109d7c \
-    --hash=sha256:c55853684fe08d4897c37dfc5faeff70607a5f1806c8be148f1695be4a63414b \
-    --hash=sha256:c65a3b5330b54103e7d21cac3f6bf3900d46f6d50138d73343d9e5b2900b2353 \
-    --hash=sha256:c7964c2183c3e6cce3f497e3a9f49d182e969f2dc3aeeadfa18945ff7bdd7051 \
-    --hash=sha256:cc3f1c053b73f20c7ad88b0d1d23be7e7b3901229ce89f5000a8399746a6e039 \
-    --hash=sha256:ce615c92d90df8373d9e13acddd154152645c0dc060871abf6bd43809673d20a \
-    --hash=sha256:d29338556a59423d9ff7b6eb0cb89ead2b0875e08fe522f3e068b955c3e7b59b \
-    --hash=sha256:d8a993c0a0ffd5f2d3bda23d0cd75e7086736f8f8268de8a82fbc4bd0ac6791e \
-    --hash=sha256:d9c727bbcf0065cbb20f39d2b4f932f8fa1631c3e01fcedc979bd4f51fe051c5 \
-    --hash=sha256:dac37cf08fcf2094159922edc7a2784cfcc5c70f8354469f79ed085f0328ebdf \
-    --hash=sha256:dd829712de97753367153ed84f2de752b86cd1f7a88b55a3a775eb52eafe8a94 \
-    --hash=sha256:e54ddd0bb8fb626aa1f9ba7b36629564544954fff9669b15da3610c22b9a0991 \
-    --hash=sha256:e77c90ab5997e85901da85131fd36acd0ed2221368199b65f0d11bca44549711 \
-    --hash=sha256:ebedc192abbc7fd13c5ee800e83a6df252bec691eb2c4bedc9f8b2e2903f5e2a \
-    --hash=sha256:ef71561f82a89af6cfcbee47f0fabfdb6e63788a9258e913955d89fdd96902ab \
-    --hash=sha256:f0a47efb1dbef13af9c9a54a94a0b814902e547b7f21acb29434504d18f36e3a \
-    --hash=sha256:f4f2ca6df64cbdd27f27b34f35adb640b5d2d77264228554e68deda54456eb11 \
-    --hash=sha256:fb02e4257376ae25c6dd95a5aec377f9b18c09be6ebdefa7ad209b9137b73d48
-requests==2.31.0 ; python_version >= "3.8" \
-    --hash=sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f \
-    --hash=sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1
-ruff==0.1.4 ; python_version >= "3.8" \
-    --hash=sha256:01206e361021426e3c1b7fba06ddcb20dbc5037d64f6841e5f2b21084dc51800 \
-    --hash=sha256:1dfd6bf8f6ad0a4ac99333f437e0ec168989adc5d837ecd38ddb2cc4a2e3db8a \
-    --hash=sha256:21520ecca4cc555162068d87c747b8f95e1e95f8ecfcbbe59e8dd00710586315 \
-    --hash=sha256:58826efb8b3efbb59bb306f4b19640b7e366967a31c049d49311d9eb3a4c60cb \
-    --hash=sha256:645591a613a42cb7e5c2b667cbefd3877b21e0252b59272ba7212c3d35a5819f \
-    --hash=sha256:6bc02a480d4bfffd163a723698da15d1a9aec2fced4c06f2a753f87f4ce6969c \
-    --hash=sha256:78e8db8ab6f100f02e28b3d713270c857d370b8d61871d5c7d1702ae411df683 \
-    --hash=sha256:80fea754eaae06335784b8ea053d6eb8e9aac75359ebddd6fee0858e87c8d510 \
-    --hash=sha256:864958706b669cce31d629902175138ad8a069d99ca53514611521f532d91495 \
-    --hash=sha256:9862811b403063765b03e716dac0fda8fdbe78b675cd947ed5873506448acea4 \
-    --hash=sha256:99908ca2b3b85bffe7e1414275d004917d1e0dfc99d497ccd2ecd19ad115fd0d \
-    --hash=sha256:9fdd61883bb34317c788af87f4cd75dfee3a73f5ded714b77ba928e418d6e39e \
-    --hash=sha256:a9a1301dc43cbf633fb603242bccd0aaa34834750a14a4c1817e2e5c8d60de17 \
-    --hash=sha256:b4eaca8c9cc39aa7f0f0d7b8fe24ecb51232d1bb620fc4441a61161be4a17539 \
-    --hash=sha256:d98ae9ebf56444e18a3e3652b3383204748f73e247dea6caaf8b52d37e6b32da \
-    --hash=sha256:e8791482d508bd0b36c76481ad3117987301b86072158bdb69d796503e1c84a8 \
-    --hash=sha256:fdfd453fc91d9d86d6aaa33b1bafa69d114cf7421057868f0b79104079d3e66e
-scipy==1.9.3 ; python_version >= "3.8" \
-    --hash=sha256:06d2e1b4c491dc7d8eacea139a1b0b295f74e1a1a0f704c375028f8320d16e31 \
-    --hash=sha256:0d54222d7a3ba6022fdf5773931b5d7c56efe41ede7f7128c7b1637700409108 \
-    --hash=sha256:1884b66a54887e21addf9c16fb588720a8309a57b2e258ae1c7986d4444d3bc0 \
-    --hash=sha256:1a72d885fa44247f92743fc20732ae55564ff2a519e8302fb7e18717c5355a8b \
-    --hash=sha256:2318bef588acc7a574f5bfdff9c172d0b1bf2c8143d9582e05f878e580a3781e \
-    --hash=sha256:4db5b30849606a95dcf519763dd3ab6fe9bd91df49eba517359e450a7d80ce2e \
-    --hash=sha256:545c83ffb518094d8c9d83cce216c0c32f8c04aaf28b92cc8283eda0685162d5 \
-    --hash=sha256:5a04cd7d0d3eff6ea4719371cbc44df31411862b9646db617c99718ff68d4840 \
-    --hash=sha256:5b88e6d91ad9d59478fafe92a7c757d00c59e3bdc3331be8ada76a4f8d683f58 \
-    --hash=sha256:68239b6aa6f9c593da8be1509a05cb7f9efe98b80f43a5861cd24c7557e98523 \
-    --hash=sha256:83b89e9586c62e787f5012e8475fbb12185bafb996a03257e9675cd73d3736dd \
-    --hash=sha256:83c06e62a390a9167da60bedd4575a14c1f58ca9dfde59830fc42e5197283dab \
-    --hash=sha256:90453d2b93ea82a9f434e4e1cba043e779ff67b92f7a0e85d05d286a3625df3c \
-    --hash=sha256:abaf921531b5aeaafced90157db505e10345e45038c39e5d9b6c7922d68085cb \
-    --hash=sha256:b41bc822679ad1c9a5f023bc93f6d0543129ca0f37c1ce294dd9d386f0a21096 \
-    --hash=sha256:c68db6b290cbd4049012990d7fe71a2abd9ffbe82c0056ebe0f01df8be5436b0 \
-    --hash=sha256:cff3a5295234037e39500d35316a4c5794739433528310e117b8a9a0c76d20fc \
-    --hash=sha256:d01e1dd7b15bd2449c8bfc6b7cc67d630700ed655654f0dfcf121600bad205c9 \
-    --hash=sha256:d644a64e174c16cb4b2e41dfea6af722053e83d066da7343f333a54dae9bc31c \
-    --hash=sha256:da8245491d73ed0a994ed9c2e380fd058ce2fa8a18da204681f2fe1f57f98f95 \
-    --hash=sha256:fbc5c05c85c1a02be77b1ff591087c83bc44579c6d2bd9fb798bb64ea5e1a027
-setuptools-scm==8.0.4 ; python_version >= "3.8" \
-    --hash=sha256:b47844cd2a84b83b3187a5782c71128c28b4c94cad8bfb871da2784a5cb54c4f \
-    --hash=sha256:b5f43ff6800669595193fd09891564ee9d1d7dcb196cab4b2506d53a2e1c95c7
-setuptools==68.2.2 ; python_version >= "3.8" \
-    --hash=sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87 \
-    --hash=sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a
-six==1.16.0 ; python_version >= "3.8" \
-    --hash=sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926 \
-    --hash=sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254
-sortedcontainers==2.4.0 ; python_version >= "3.8" \
-    --hash=sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88 \
-    --hash=sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0
-tomli==2.0.1 ; python_full_version <= "3.11.0a6" and python_version >= "3.8" \
-    --hash=sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc \
-    --hash=sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f
-typing-extensions==4.8.0 ; python_version >= "3.8" \
-    --hash=sha256:8f92fc8806f9a6b641eaa5318da32b44d401efaac0f6678c9bc448ba3605faa0 \
-    --hash=sha256:df8e4339e9cb77357558cbdbceca33c303714cf861d1eef15e1070055ae8b7ef
-urllib3==2.0.7 ; python_version >= "3.8" \
-    --hash=sha256:c97dfde1f7bd43a71c8d2a58e369e9b2bf692d1334ea9f9cae55add7d0dd0f84 \
-    --hash=sha256:fdb6d215c776278489906c2f8916e6e7d4f5a9b602ccbcfdf7f016fc8da0596e
-virtualenv==20.24.6 ; python_version >= "3.8" \
-    --hash=sha256:02ece4f56fbf939dbbc33c0715159951d6bf14aaf5457b092e4548e1382455af \
-    --hash=sha256:520d056652454c5098a00c0f073611ccbea4c79089331f60bf9d7ba247bb7381
-watchdog==3.0.0 ; python_version >= "3.8" \
-    --hash=sha256:0e06ab8858a76e1219e68c7573dfeba9dd1c0219476c5a44d5333b01d7e1743a \
-    --hash=sha256:13bbbb462ee42ec3c5723e1205be8ced776f05b100e4737518c67c8325cf6100 \
-    --hash=sha256:233b5817932685d39a7896b1090353fc8efc1ef99c9c054e46c8002561252fb8 \
-    --hash=sha256:25f70b4aa53bd743729c7475d7ec41093a580528b100e9a8c5b5efe8899592fc \
-    --hash=sha256:2b57a1e730af3156d13b7fdddfc23dea6487fceca29fc75c5a868beed29177ae \
-    --hash=sha256:336adfc6f5cc4e037d52db31194f7581ff744b67382eb6021c868322e32eef41 \
-    --hash=sha256:3aa7f6a12e831ddfe78cdd4f8996af9cf334fd6346531b16cec61c3b3c0d8da0 \
-    --hash=sha256:3ed7c71a9dccfe838c2f0b6314ed0d9b22e77d268c67e015450a29036a81f60f \
-    --hash=sha256:4c9956d27be0bb08fc5f30d9d0179a855436e655f046d288e2bcc11adfae893c \
-    --hash=sha256:4d98a320595da7a7c5a18fc48cb633c2e73cda78f93cac2ef42d42bf609a33f9 \
-    --hash=sha256:4f94069eb16657d2c6faada4624c39464f65c05606af50bb7902e036e3219be3 \
-    --hash=sha256:5113334cf8cf0ac8cd45e1f8309a603291b614191c9add34d33075727a967709 \
-    --hash=sha256:51f90f73b4697bac9c9a78394c3acbbd331ccd3655c11be1a15ae6fe289a8c83 \
-    --hash=sha256:5d9f3a10e02d7371cd929b5d8f11e87d4bad890212ed3901f9b4d68767bee759 \
-    --hash=sha256:7ade88d0d778b1b222adebcc0927428f883db07017618a5e684fd03b83342bd9 \
-    --hash=sha256:7c5f84b5194c24dd573fa6472685b2a27cc5a17fe5f7b6fd40345378ca6812e3 \
-    --hash=sha256:7e447d172af52ad204d19982739aa2346245cc5ba6f579d16dac4bfec226d2e7 \
-    --hash=sha256:8ae9cda41fa114e28faf86cb137d751a17ffd0316d1c34ccf2235e8a84365c7f \
-    --hash=sha256:8f3ceecd20d71067c7fd4c9e832d4e22584318983cabc013dbf3f70ea95de346 \
-    --hash=sha256:9fac43a7466eb73e64a9940ac9ed6369baa39b3bf221ae23493a9ec4d0022674 \
-    --hash=sha256:a70a8dcde91be523c35b2bf96196edc5730edb347e374c7de7cd20c43ed95397 \
-    --hash=sha256:adfdeab2da79ea2f76f87eb42a3ab1966a5313e5a69a0213a3cc06ef692b0e96 \
-    --hash=sha256:ba07e92756c97e3aca0912b5cbc4e5ad802f4557212788e72a72a47ff376950d \
-    --hash=sha256:c07253088265c363d1ddf4b3cdb808d59a0468ecd017770ed716991620b8f77a \
-    --hash=sha256:c9d8c8ec7efb887333cf71e328e39cffbf771d8f8f95d308ea4125bf5f90ba64 \
-    --hash=sha256:d00e6be486affb5781468457b21a6cbe848c33ef43f9ea4a73b4882e5f188a44 \
-    --hash=sha256:d429c2430c93b7903914e4db9a966c7f2b068dd2ebdd2fa9b9ce094c7d459f33
-zipp==3.17.0 ; python_version < "3.10" and python_version >= "3.8" \
-    --hash=sha256:0e923e726174922dce09c53c59ad483ff7bbb8e572e00c7f7c46b88556409f31 \
-    --hash=sha256:84e64a1c28cf7e91ed2078bb8cc8c259cb19b76942096c8d7b84947690cabaf0
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..a78a75b
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,165 @@
+astropy-iers-data==0.2024.7.1.0.34.3 ; python_version >= "3.9" \
+    --hash=sha256:0e3b47ae2ed44a03c3bea40a2b73bfce1858cf4059b4612c677a5e4a90f86003 \
+    --hash=sha256:195053a2b198f964ad89440b419fa4e506d9db6a8e1cc18a387bc9c3b480a09e
+astropy==6.0.1 ; python_version >= "3.9" \
+    --hash=sha256:034dff5994428fb89813f40a18600dd8804128c52edf3d1baa8936eca3738de4 \
+    --hash=sha256:129ed1fb1d23e6fbf8b8e697c2e7340d99bc6271b8c59f9572f3f47063a42e6a \
+    --hash=sha256:15a5da8a0a84d75b55fafd56630578131c3c9186e4e486b4d2fb15c349b844d0 \
+    --hash=sha256:1db9e95438472f6ed53fa2f4e2811c2d84f4085eeacc3cb8820d770d1ea61d1c \
+    --hash=sha256:1f183ab42655ad09b064a4e8eb9cd1eaa138b90ca2f0cd82a200afda062063a5 \
+    --hash=sha256:242b8f101301ab303366109d0dfe3cf0db745bf778f7b859fb486105197577d1 \
+    --hash=sha256:2b5ff962b0e586953f95b63ec047e1d7a3b6a12a13d11c6e909e0bcd3e05b445 \
+    --hash=sha256:2c00922548a666b026e2630a563090341d74c8222066e9c84c9673395bca7363 \
+    --hash=sha256:2f53caf9efebcc9040a92c977dcdae78dd0ff4de218fd316e4fcaffd9ace8dc1 \
+    --hash=sha256:34fd2bb39cbfa6a8815b5cc99008d59057b9d341db00c67dbb40a3784a8dfb08 \
+    --hash=sha256:46cbadf360bbadb6a106217e104b91f85dd618658caffdaab5d54a14d0d52563 \
+    --hash=sha256:4fdd54fa57b85d50c4b83ab7ffd90ba2ffcc3d725e3f8d5ffa1ff5f500ef6b97 \
+    --hash=sha256:5208b6f10956ca92efb73375364c81a7df365b441b07f4941a24ee0f1bd9e292 \
+    --hash=sha256:6e998ee0ffa58342b4d44f2843b036015e3a6326b53185c5361fea4430658466 \
+    --hash=sha256:6f28facb5800c0617f233c1db0e622da83de1f74ca28d0ff8646e360d4fda74e \
+    --hash=sha256:764992af1ee1cd6d6f26373d09ddb5ede639d025ce9ff658b3b6580dc2ba4ec6 \
+    --hash=sha256:87ebbae7ba52f4de9b9f45029a3167d6515399138048d0b734c9033fda7fd723 \
+    --hash=sha256:89a975de356d0608e74f1f493442fb3acbbb7a85b739e074460bb0340014b39c \
+    --hash=sha256:8bc267738a85f633142c246dceefa722b653e7ba99f02e86dd9a7b980467eafc \
+    --hash=sha256:8fbd6d88935749ae892445691ac0dbd1923fc6d8094753a35150fc7756118fe3 \
+    --hash=sha256:9b3bf27c51fb46bba993695eebd0c39a4e2a792b707e65b28ac4e8ae703f93d4 \
+    --hash=sha256:c33e3d746c3e7a324dbd76b236fe1e44304d5b6d941a1f724f419d01666d6d88 \
+    --hash=sha256:c682967736228cc4477e63db0e8854375dd31d755de55b30256de98f1f7b7c23 \
+    --hash=sha256:ca9da00bfa95fbf8475d22aba6d7d046f3821a107b733fc7c7c35c74fcfa2bbf \
+    --hash=sha256:d1eb40fe68121753f43fc82d618a2eae53dd0731689e124ef9e002aa2c241c4f \
+    --hash=sha256:d934aff5fe81e84a45098e281f969976963cc16b3401176a8171affd84301a27 \
+    --hash=sha256:e604898ca1790c9fd2e2dc83b38f9185556ea618a3a6e6be31c286fafbebd165 \
+    --hash=sha256:eaff9388a2fed0757bd0b4c41c9346d0edea9e7e938a4bfa8070eaabbb538a23 \
+    --hash=sha256:f18536d6f97faa81ed6c9af7bb2e27b376b41b27399f862e3b13387538c966b9
+jplephem==2.22 ; python_version >= "3.9" \
+    --hash=sha256:0d9acc7217b4806feba93e72974ceead5611104bce6789af38d4f27dcf7a592c \
+    --hash=sha256:efe4b3590f3eef1a5dfcf7078e8e93803f8ce3276c3306e93bfd20b3536b2c60
+numpy==1.26.4 ; python_version >= "3.9" \
+    --hash=sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b \
+    --hash=sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818 \
+    --hash=sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20 \
+    --hash=sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0 \
+    --hash=sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010 \
+    --hash=sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a \
+    --hash=sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea \
+    --hash=sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c \
+    --hash=sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71 \
+    --hash=sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110 \
+    --hash=sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be \
+    --hash=sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a \
+    --hash=sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a \
+    --hash=sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5 \
+    --hash=sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed \
+    --hash=sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd \
+    --hash=sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c \
+    --hash=sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e \
+    --hash=sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0 \
+    --hash=sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c \
+    --hash=sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a \
+    --hash=sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b \
+    --hash=sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0 \
+    --hash=sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6 \
+    --hash=sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2 \
+    --hash=sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a \
+    --hash=sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30 \
+    --hash=sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218 \
+    --hash=sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5 \
+    --hash=sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07 \
+    --hash=sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2 \
+    --hash=sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4 \
+    --hash=sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764 \
+    --hash=sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef \
+    --hash=sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3 \
+    --hash=sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f
+packaging==24.1 ; python_version >= "3.9" \
+    --hash=sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002 \
+    --hash=sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124
+pyerfa==2.0.1.4 ; python_version >= "3.9" \
+    --hash=sha256:39cf838c9a21e40d4e3183bead65b3ce6af763c4a727f87d84909c9be7d3a33c \
+    --hash=sha256:46d3bed0ac666f08d8364b34a00b8c6595358d6c4f4532da8d13fac0e5227baa \
+    --hash=sha256:610d2bc314e140d876b93b1287c7c81685434873c8700cc3e1596193f77d1071 \
+    --hash=sha256:7e4508dd7ffd7b27b7f67168643764454887e990ca9e4584824f0e3ab5884c0f \
+    --hash=sha256:83a44ba84ebfc3244412ecbf1065c087c382da84f1c3eee1f2a0638d9046ac96 \
+    --hash=sha256:88a8d0f3608a66871615bd168fcddf674dce9f7568c239a03cf8d9936161d032 \
+    --hash=sha256:900b266a3862baa9560d6b1b184dcc14e0e76d550ff70d32336d3989b2ed18ca \
+    --hash=sha256:9045e9f786c76cb55da86ada3405c378c32b88f6e3c6296cb288496ab374b068 \
+    --hash=sha256:acb8a6713232ea35c04bc6e40ac4e461dfcc817d395ef2a3c8051c1a33249dd3 \
+    --hash=sha256:bc3cf45967ac1af77a777deb050fb08bbc75256dd97ca6005e4d385358b7af40 \
+    --hash=sha256:ff112353944bf705342741f2fe41674f97154a302b0295eaef7381af92ad2b3a
+pyyaml==6.0.1 ; python_version >= "3.9" \
+    --hash=sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5 \
+    --hash=sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc \
+    --hash=sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df \
+    --hash=sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741 \
+    --hash=sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206 \
+    --hash=sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27 \
+    --hash=sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595 \
+    --hash=sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62 \
+    --hash=sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98 \
+    --hash=sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696 \
+    --hash=sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290 \
+    --hash=sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9 \
+    --hash=sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d \
+    --hash=sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6 \
+    --hash=sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867 \
+    --hash=sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47 \
+    --hash=sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486 \
+    --hash=sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6 \
+    --hash=sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3 \
+    --hash=sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007 \
+    --hash=sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938 \
+    --hash=sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0 \
+    --hash=sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c \
+    --hash=sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735 \
+    --hash=sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d \
+    --hash=sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28 \
+    --hash=sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4 \
+    --hash=sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba \
+    --hash=sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8 \
+    --hash=sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5 \
+    --hash=sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd \
+    --hash=sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3 \
+    --hash=sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0 \
+    --hash=sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515 \
+    --hash=sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c \
+    --hash=sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c \
+    --hash=sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924 \
+    --hash=sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34 \
+    --hash=sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43 \
+    --hash=sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859 \
+    --hash=sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673 \
+    --hash=sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54 \
+    --hash=sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a \
+    --hash=sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b \
+    --hash=sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab \
+    --hash=sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa \
+    --hash=sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c \
+    --hash=sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585 \
+    --hash=sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d \
+    --hash=sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f
+scipy==1.13.1 ; python_version >= "3.9" \
+    --hash=sha256:017367484ce5498445aade74b1d5ab377acdc65e27095155e448c88497755a5d \
+    --hash=sha256:095a87a0312b08dfd6a6155cbbd310a8c51800fc931b8c0b84003014b874ed3c \
+    --hash=sha256:20335853b85e9a49ff7572ab453794298bcf0354d8068c5f6775a0eabf350aca \
+    --hash=sha256:27e52b09c0d3a1d5b63e1105f24177e544a222b43611aaf5bc44d4a0979e32f9 \
+    --hash=sha256:2831f0dc9c5ea9edd6e51e6e769b655f08ec6db6e2e10f86ef39bd32eb11da54 \
+    --hash=sha256:2ac65fb503dad64218c228e2dc2d0a0193f7904747db43014645ae139c8fad16 \
+    --hash=sha256:392e4ec766654852c25ebad4f64e4e584cf19820b980bc04960bca0b0cd6eaa2 \
+    --hash=sha256:436bbb42a94a8aeef855d755ce5a465479c721e9d684de76bf61a62e7c2b81d5 \
+    --hash=sha256:45484bee6d65633752c490404513b9ef02475b4284c4cfab0ef946def50b3f59 \
+    --hash=sha256:54f430b00f0133e2224c3ba42b805bfd0086fe488835effa33fa291561932326 \
+    --hash=sha256:5713f62f781eebd8d597eb3f88b8bf9274e79eeabf63afb4a737abc6c84ad37b \
+    --hash=sha256:5d72782f39716b2b3509cd7c33cdc08c96f2f4d2b06d51e52fb45a19ca0c86a1 \
+    --hash=sha256:637e98dcf185ba7f8e663e122ebf908c4702420477ae52a04f9908707456ba4d \
+    --hash=sha256:8335549ebbca860c52bf3d02f80784e91a004b71b059e3eea9678ba994796a24 \
+    --hash=sha256:949ae67db5fa78a86e8fa644b9a6b07252f449dcf74247108c50e1d20d2b4627 \
+    --hash=sha256:a014c2b3697bde71724244f63de2476925596c24285c7a637364761f8710891c \
+    --hash=sha256:a78b4b3345f1b6f68a763c6e25c0c9a23a9fd0f39f5f3d200efe8feda560a5fa \
+    --hash=sha256:cdd7dacfb95fea358916410ec61bbc20440f7860333aee6d882bb8046264e949 \
+    --hash=sha256:cfa31f1def5c819b19ecc3a8b52d28ffdcc7ed52bb20c9a7589669dd3c250989 \
+    --hash=sha256:d533654b7d221a6a97304ab63c41c96473ff04459e404b83275b60aa8f4b7004 \
+    --hash=sha256:d605e9c23906d1994f55ace80e0125c587f96c020037ea6aa98d01b4bd2e222f \
+    --hash=sha256:de3ade0e53bc1f21358aa74ff4830235d716211d7d077e340c7349bc3542e884 \
+    --hash=sha256:e89369d27f9e7b0884ae559a3a956e77c02114cc60a6058b4e5011572eea9299 \
+    --hash=sha256:eccfa1906eacc02de42d70ef4aecea45415f5be17e72b61bafcfd329bdc52e94 \
+    --hash=sha256:f26264b282b9da0952a024ae34710c2aff7d27480ee91a2e82b7b7073c24722f
diff --git a/zodipy/__init__.py b/zodipy/__init__.py
index 6bfe919..e8d30b9 100644
--- a/zodipy/__init__.py
+++ b/zodipy/__init__.py
@@ -1,12 +1,9 @@
-from zodipy import comps, source_params
-from zodipy._contour import tabulate_density
+from zodipy.model import Model
 from zodipy.model_registry import model_registry
-from zodipy.zodipy import Zodipy
+from zodipy.number_density import grid_number_density
 
 __all__ = (
-    "Zodipy",
+    "Model",
+    "grid_number_density",
     "model_registry",
-    "comps",
-    "source_params",
-    "tabulate_density",
 )
diff --git a/zodipy/_bandpass.py b/zodipy/_bandpass.py
deleted file mode 100644
index 610201a..0000000
--- a/zodipy/_bandpass.py
+++ /dev/null
@@ -1,78 +0,0 @@
-from __future__ import annotations
-
-from dataclasses import dataclass
-from typing import TYPE_CHECKING, Sequence
-
-import astropy.units as u
-import numpy as np
-
-from zodipy._constants import (
-    MAX_INTERPOLATION_GRID_TEMPERATURE,
-    MIN_INTERPOLATION_GRID_TEMPERATURE,
-    N_INTERPOLATION_POINTS,
-)
-from zodipy._source_funcs import get_blackbody_emission
-from zodipy._validators import get_validated_and_normalized_weights, get_validated_freq
-
-if TYPE_CHECKING:
-    import numpy.typing as npt
-
-    from zodipy._ipd_model import InterplanetaryDustModel
-    from zodipy._types import FrequencyOrWavelength
-
-
-@dataclass
-class Bandpass:
-    frequencies: FrequencyOrWavelength
-    weights: npt.NDArray[np.float64]
-
-    def integrate(self, quantity: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
-        """Integrate a quantity over the bandpass."""
-        return np.trapz(self.weights * quantity, self.frequencies.value, axis=-1)
-
-    def switch_convention(self) -> None:
-        """Switch the bandpass from frequency to wavelength or the other way around."""
-        self.frequencies = self.frequencies.to(
-            u.micron if self.frequencies.unit.is_equivalent(u.Hz) else u.Hz,
-            equivalencies=u.spectral(),
-        )
-        if self.frequencies.size > 1:
-            self.frequencies = np.flip(self.frequencies)
-            self.weights = np.flip(self.weights)
-            self.weights /= np.trapz(self.weights, self.frequencies.value)
-
-
-def validate_and_get_bandpass(
-    freq: FrequencyOrWavelength,
-    weights: Sequence[float] | npt.NDArray[np.floating] | None,
-    model: InterplanetaryDustModel,
-    extrapolate: bool,
-) -> Bandpass:
-    """Validate bandpass and return a `Bandpass`."""
-    freq = get_validated_freq(freq, model, extrapolate)
-    normalized_weights = get_validated_and_normalized_weights(weights, freq)
-
-    return Bandpass(freq, normalized_weights)
-
-
-def get_bandpass_interpolation_table(
-    bandpass: Bandpass,
-    n_points: int = N_INTERPOLATION_POINTS,
-    min_temp: float = MIN_INTERPOLATION_GRID_TEMPERATURE,
-    max_temp: float = MAX_INTERPOLATION_GRID_TEMPERATURE,
-) -> npt.NDArray[np.float64]:
-    """Pre-compute the bandpass integrated blackbody emission for a grid of temperatures."""
-    if not bandpass.frequencies.unit.is_equivalent(u.Hz):
-        bandpass.switch_convention()
-
-    bandpass_integrals = np.zeros(n_points)
-    temperature_grid = np.linspace(min_temp, max_temp, n_points)
-    for idx, temp in enumerate(temperature_grid):
-        blackbody_emission = get_blackbody_emission(bandpass.frequencies.value, temp)
-        bandpass_integrals[idx] = (
-            bandpass.integrate(blackbody_emission)
-            if bandpass.frequencies.size > 1
-            else blackbody_emission
-        )
-
-    return np.asarray([temperature_grid, bandpass_integrals])
diff --git a/zodipy/_constants.py b/zodipy/_constants.py
deleted file mode 100644
index d630969..0000000
--- a/zodipy/_constants.py
+++ /dev/null
@@ -1,25 +0,0 @@
-import astropy.constants as const
-import astropy.units as u
-import numpy as np
-
-h: float = const.h.value
-c: float = const.c.value
-k_B: float = const.k_B.value
-
-SPECIFIC_INTENSITY_UNITS = u.W / u.Hz / u.m**2 / u.sr
-
-R_0 = np.finfo(np.float64).eps
-R_MARS = 1.5237
-R_VERITAS = 2.16
-R_EOS = 3.02
-R_THEMIS = 3.14
-R_EARTH = 1
-R_ASTEROID_BELT = 3.137
-R_NARROW_BANDS = 3.015
-R_JUPITER = 5.2
-R_KUIPER_BELT = 30
-DISTANCE_FROM_EARTH_TO_L2 = u.Quantity(0.009896235034000056, u.AU)
-
-N_INTERPOLATION_POINTS = 100
-MIN_INTERPOLATION_GRID_TEMPERATURE = 40
-MAX_INTERPOLATION_GRID_TEMPERATURE = 550
diff --git a/zodipy/_contour.py b/zodipy/_contour.py
deleted file mode 100644
index 6b40514..0000000
--- a/zodipy/_contour.py
+++ /dev/null
@@ -1,55 +0,0 @@
-from __future__ import annotations
-
-from typing import TYPE_CHECKING, Sequence
-
-import astropy.units as u
-import numpy as np
-
-from zodipy._ipd_dens_funcs import construct_density_partials
-from zodipy.model_registry import model_registry
-
-if TYPE_CHECKING:
-    import numpy.typing as npt
-
-DEFAULT_EARTH_POS = u.Quantity([1, 0, 0], u.AU)
-
-
-def tabulate_density(
-    grid: npt.NDArray[np.floating] | Sequence[npt.NDArray[np.floating]],
-    model: str = "DIRBE",
-    earth_pos: u.Quantity[u.AU] = DEFAULT_EARTH_POS,
-) -> npt.NDArray[np.float64]:
-    """Return the tabulated densities of the zodiacal components for a given grid.
-
-    Args:
-        grid: A cartesian mesh grid (x, y, z).
-        model: Name of interplanetary dust model supported by ZodiPy.
-        earth_pos: Position of the Earth in AU.
-
-    Returns:
-        density_grid: The tabulated zodiacal component densities.
-
-    """
-    ipd_model = model_registry.get_model(model)
-
-    if not isinstance(grid, np.ndarray):
-        grid = np.asarray(grid)
-
-    # Prepare attributes and variables for broadcasting with the grid
-    earth_position = np.reshape(earth_pos.to(u.AU).value, (3, 1, 1, 1))
-    for comp in ipd_model.comps.values():
-        comp.X_0 = np.reshape(comp.X_0, (3, 1, 1, 1))
-
-    partials = construct_density_partials(
-        list(ipd_model.comps.values()), {"X_earth": earth_position}
-    )
-
-    density_grid = np.zeros((len(ipd_model.comps), *grid.shape[1:]))
-    for idx, partial in enumerate(partials):
-        density_grid[idx] = partial(grid)
-
-    # Revert broadcasting reshapes
-    for comp in ipd_model.comps.values():
-        comp.X_0 = np.reshape(comp.X_0, (3, 1))
-
-    return density_grid
diff --git a/zodipy/_interpolate_source.py b/zodipy/_interpolate_source.py
deleted file mode 100644
index 6b981fa..0000000
--- a/zodipy/_interpolate_source.py
+++ /dev/null
@@ -1,116 +0,0 @@
-from __future__ import annotations
-
-from functools import partial
-from typing import Any, Callable, Dict, TypeVar, Union
-
-import astropy.units as u
-import numpy as np
-
-from zodipy._bandpass import Bandpass
-from zodipy._constants import SPECIFIC_INTENSITY_UNITS
-from zodipy._ipd_comps import ComponentLabel
-from zodipy._ipd_model import RRM, InterplanetaryDustModel, Kelsall
-
-InterplanetaryDustModelT = TypeVar("InterplanetaryDustModelT", bound=InterplanetaryDustModel)
-
-"""Return the source parameters for a given bandpass and model.
-Must match arguments in the emission fns."""
-GetSourceParametersFn = Callable[
-    [Bandpass, InterplanetaryDustModelT, Callable],
-    Dict[Union[ComponentLabel, str], Any],
-]
-
-
-def get_source_parameters_kelsall_comp(
-    bandpass: Bandpass, model: Kelsall, interpolator: Callable
-) -> dict[ComponentLabel | str, dict[str, Any]]:
-    if not bandpass.frequencies.unit.is_equivalent(model.spectrum.unit):
-        bandpass.switch_convention()
-
-    spectrum = (
-        model.spectrum.to_value(u.Hz)
-        if model.spectrum.unit.is_equivalent(u.Hz)
-        else model.spectrum.to_value(u.micron)
-    )
-
-    interpolator = partial(interpolator, x=spectrum)
-
-    source_parameters: dict[ComponentLabel | str, dict[str, Any]] = {}
-    for comp_label in model.comps:
-        source_parameters[comp_label] = {}
-        emissivity = interpolator(y=model.emissivities[comp_label])(bandpass.frequencies.value)
-        if model.albedos is not None:
-            albedo = interpolator(y=model.albedos[comp_label])(bandpass.frequencies.value)
-        else:
-            albedo = 0
-
-        if bandpass.frequencies.size > 1:
-            emissivity = bandpass.integrate(emissivity)
-            albedo = bandpass.integrate(albedo)
-
-        source_parameters[comp_label]["emissivity"] = emissivity
-        source_parameters[comp_label]["albedo"] = albedo
-
-    if model.phase_coefficients is not None:
-        phase_coefficients = interpolator(y=np.asarray(model.phase_coefficients))(
-            bandpass.frequencies.value
-        )
-        phase_coefficients = interpolator(y=np.asarray(model.phase_coefficients))(
-            bandpass.frequencies.value
-        )
-    else:
-        phase_coefficients = np.repeat(np.zeros((3, 1)), repeats=bandpass.frequencies.size, axis=-1)
-
-    if model.solar_irradiance is not None:
-        solar_irradiance = interpolator(y=model.solar_irradiance)(bandpass.frequencies.value)
-        solar_irradiance = u.Quantity(solar_irradiance, "MJy /sr").to_value(
-            SPECIFIC_INTENSITY_UNITS, equivalencies=u.spectral()
-        )
-    else:
-        solar_irradiance = 0
-
-    if bandpass.frequencies.size > 1:
-        phase_coefficients = bandpass.integrate(phase_coefficients)
-        solar_irradiance = bandpass.integrate(solar_irradiance)
-    source_parameters["common"] = {}
-    source_parameters["common"]["phase_coefficients"] = tuple(phase_coefficients)
-    source_parameters["common"]["solar_irradiance"] = solar_irradiance
-    source_parameters["common"]["T_0"] = model.T_0
-    source_parameters["common"]["delta"] = model.delta
-
-    return source_parameters
-
-
-def get_source_parameters_rmm(
-    bandpass: Bandpass, model: RRM, interpolator: Callable
-) -> dict[ComponentLabel | str, dict[str, Any]]:
-    if not bandpass.frequencies.unit.is_equivalent(model.spectrum.unit):
-        bandpass.switch_convention()
-
-    spectrum = (
-        model.spectrum.to_value(u.Hz)
-        if model.spectrum.unit.is_equivalent(u.Hz)
-        else model.spectrum.to_value(u.micron)
-    )
-
-    source_parameters: dict[ComponentLabel | str, dict[str, Any]] = {}
-    calibration = interpolator(x=spectrum, y=model.calibration)(bandpass.frequencies.value)
-    calibration = u.Quantity(calibration, u.MJy / u.AU).to_value(u.Jy / u.cm)
-
-    if bandpass.frequencies.size > 1:
-        calibration = bandpass.integrate(calibration)
-
-    for comp_label in model.comps:
-        source_parameters[comp_label] = {}
-        source_parameters[comp_label]["T_0"] = model.T_0[comp_label]
-        source_parameters[comp_label]["delta"] = model.delta[comp_label]
-
-    source_parameters["common"] = {"calibration": calibration}
-
-    return source_parameters
-
-
-SOURCE_PARAMS_MAPPING: dict[type[InterplanetaryDustModel], GetSourceParametersFn] = {
-    Kelsall: get_source_parameters_kelsall_comp,
-    RRM: get_source_parameters_rmm,
-}
diff --git a/zodipy/_ipd_model.py b/zodipy/_ipd_model.py
deleted file mode 100644
index 4589106..0000000
--- a/zodipy/_ipd_model.py
+++ /dev/null
@@ -1,98 +0,0 @@
-from __future__ import annotations
-
-from abc import ABC
-from dataclasses import dataclass, field
-from typing import TYPE_CHECKING, Mapping, Sequence
-
-if TYPE_CHECKING:
-    from zodipy._ipd_comps import Component, ComponentLabel
-    from zodipy._types import FrequencyOrWavelength
-
-
-@dataclass
-class InterplanetaryDustModel(ABC):
-    """Base class for interplanetary dust models.
-
-    Args:
-        comps: Mapping of component labels to component classes.
-        spectrum: Frequency or wavelength over which the model is valid.
-
-    """
-
-    comps: Mapping[ComponentLabel, Component]
-    spectrum: FrequencyOrWavelength
-
-    def to_dict(self) -> dict:
-        """Return a dictionary representation of the model."""
-        _dict: dict = {}
-        for key, value in vars(self).items():
-            if key == "comps":
-                _dict[key] = {}
-                for comp_key, comp_value in value.items():
-                    _dict[key][comp_key.value] = {
-                        k: v
-                        for k, v in vars(comp_value).items()
-                        if comp_value.__dataclass_fields__[k].init
-                    }
-            elif isinstance(value, dict):
-                _dict[key] = {k.value: v for k, v in value.items()}
-            else:
-                _dict[key] = value
-
-        return _dict
-
-
-@dataclass
-class Kelsall(InterplanetaryDustModel):
-    """Kelsall et al. (1998) model."""
-
-    T_0: float
-    delta: float
-    emissivities: Mapping[ComponentLabel, Sequence[float]]
-    albedos: Mapping[ComponentLabel, Sequence[float]] | None = None
-    solar_irradiance: Sequence[float] | None = None  # In units of MJy/sr
-    phase_coefficients: Sequence[Sequence[float]] | None = None
-
-
-@dataclass
-class RRM(InterplanetaryDustModel):
-    """Rowan-Robinson and May (2013) model."""
-
-    T_0: Mapping[ComponentLabel, float]
-    delta: Mapping[ComponentLabel, float]
-    calibration: Sequence[float]
-
-
-@dataclass
-class InterplanetaryDustModelRegistry:
-    """Container for registered models."""
-
-    _registry: dict[str, InterplanetaryDustModel] = field(init=False, default_factory=dict)
-
-    @property
-    def models(self) -> list[str]:
-        return list(self._registry.keys())
-
-    def register_model(
-        self,
-        name: str,
-        model: InterplanetaryDustModel,
-    ) -> None:
-        if (name := name.lower()) in self._registry:
-            msg = f"a model by the name {name!s} is already registered."
-            raise ValueError(msg)
-
-        self._registry[name] = model
-
-    def get_model(self, name: str) -> InterplanetaryDustModel:
-        if (name := name.lower()) not in self._registry:
-            msg = (
-                f"{name!r} is not a registered Interplanetary Dust model. "
-                f"Avaliable models are: {', '.join(self._registry)}."
-            )
-            raise ValueError(msg)
-
-        return self._registry[name]
-
-
-model_registry = InterplanetaryDustModelRegistry()
diff --git a/zodipy/_sky_coords.py b/zodipy/_sky_coords.py
deleted file mode 100644
index 44d7dde..0000000
--- a/zodipy/_sky_coords.py
+++ /dev/null
@@ -1,60 +0,0 @@
-from typing import Tuple, Union
-
-import astropy.units as u
-import numpy as np
-import numpy.typing as npt
-from astropy.coordinates import HeliocentricMeanEcliptic, get_body
-from astropy.time import Time
-
-from zodipy._constants import DISTANCE_FROM_EARTH_TO_L2
-
-
-@u.quantity_input
-def get_obs_and_earth_positions(
-    obs: str, obs_time: Time, obs_pos: Union[u.Quantity[u.AU], None]
-) -> Tuple[npt.NDArray[np.float64], npt.NDArray[np.float64]]:
-    """Return the position of the observer and the Earth (3, `n_pointing`, `n_gauss_quad_degree`).
-
-    The lagrange point SEMB-L2 is not included in any of the current available
-    ephemerides. We implement an approximation to its position, assuming that
-    SEMB-L2 is at all times located at a fixed distance from Earth along the vector
-    pointing to Earth from the Sun.
-    """
-    earth_position = _get_earth_position(obs_time)
-    if obs_pos is None:
-        obs_position = _get_observer_position(obs, obs_time, earth_position)
-    else:
-        obs_position = obs_pos.to(u.AU)
-
-    return obs_position.reshape(3, 1, 1).value, earth_position.reshape(3, 1, 1).value
-
-
-def _get_earth_position(obs_time: Time) -> u.Quantity[u.AU]:
-    """Return the position of the Earth given an ephemeris and observation time."""
-    earth_skycoordinate = get_body("earth", obs_time)
-    earth_skycoordinate = earth_skycoordinate.transform_to(HeliocentricMeanEcliptic)
-    return earth_skycoordinate.cartesian.xyz.to(u.AU)
-
-
-def _get_observer_position(
-    obs: str, obs_time: Time, earth_pos: u.Quantity[u.AU]
-) -> u.Quantity[u.AU]:
-    """Return the position of the Earth and the observer."""
-    if obs.lower() == "semb-l2":
-        return _get_sun_earth_moon_barycenter(earth_pos)
-
-    observer_skycoordinate = get_body(obs, obs_time)
-    observer_skycoordinate = observer_skycoordinate.transform_to(HeliocentricMeanEcliptic)
-
-    return observer_skycoordinate.cartesian.xyz.to(u.AU)
-
-
-def _get_sun_earth_moon_barycenter(
-    earth_position: u.Quantity[u.AU],
-) -> u.Quantity[u.AU]:
-    """Return the approximated position of SEMB-L2 given Earth's position."""
-    r_earth = np.linalg.norm(earth_position)
-    earth_unit_vector = earth_position / r_earth
-    semb_l2_length = r_earth + DISTANCE_FROM_EARTH_TO_L2
-
-    return earth_unit_vector * semb_l2_length
diff --git a/zodipy/_source_funcs.py b/zodipy/_source_funcs.py
deleted file mode 100644
index e6ebb5d..0000000
--- a/zodipy/_source_funcs.py
+++ /dev/null
@@ -1,96 +0,0 @@
-from __future__ import annotations
-
-from functools import lru_cache
-from typing import TYPE_CHECKING
-
-import numpy as np
-
-from ._constants import c, h, k_B
-
-if TYPE_CHECKING:
-    import numpy.typing as npt
-
-
-def get_blackbody_emission(
-    freq: float | npt.NDArray[np.float64], T: npt.NDArray[np.float64]
-) -> npt.NDArray[np.float64]:
-    """Return the blackbody emission given a sequence of frequencies and temperatures.
-
-    Args:
-        freq: Frequency [Hz].
-        T: Temperature of the blackbody [K].
-
-    Returns:
-        Blackbody emission [W / m^2 Hz sr].
-
-    """
-    term1 = (2 * h * freq**3) / c**2
-    term2 = np.expm1((h * freq) / (k_B * T))
-    return term1 / term2
-
-
-def get_dust_grain_temperature(
-    R: npt.NDArray[np.float64], T_0: float, delta: float
-) -> npt.NDArray[np.float64]:
-    """Return the dust grain temperature given a radial distance from the Sun.
-
-    Args:
-        R: Radial distance from the sun in ecliptic heliocentric coordinates [AU / 1AU].
-        T_0: Temperature of dust grains located 1 AU from the Sun [K].
-        delta: Powerlaw index.
-
-    Returns:
-        Dust grain temperature [K].
-
-    """
-    return T_0 * R**-delta
-
-
-def get_scattering_angle(
-    R_los: float | npt.NDArray[np.float64],
-    R_helio: npt.NDArray[np.float64],
-    X_los: npt.NDArray[np.float64],
-    X_helio: npt.NDArray[np.float64],
-) -> npt.NDArray[np.float64]:
-    """Return the scattering angle between the Sun and a point along the line of sight.
-
-    Args:
-        R_los: Distance from observer to a point along the line of sight.
-        R_helio: Distance from the Sun to a point along the line of sight.
-        X_los: Vector pointing from the observer to a point along the line of sight.
-        X_helio: Heliocentric position of point along the line of sight.
-
-    Returns:
-        Scattering angle.
-
-    """
-    cos_theta = (X_los * X_helio).sum(axis=0) / (R_los * R_helio)
-    cos_theta = np.clip(cos_theta, -1, 1)
-    return np.arccos(-cos_theta)
-
-
-def get_phase_function(
-    Theta: npt.NDArray[np.float64], C: tuple[float, ...]
-) -> npt.NDArray[np.float64]:
-    """Return the phase function.
-
-    Args:
-        Theta: Scattering angle.
-        C: Phase function parameters.
-
-    Returns:
-        The Phase funciton.
-
-    """
-    phase_normalization = _get_phase_normalization(C)
-    return phase_normalization * (C[0] + C[1] * Theta + np.exp(C[2] * Theta))
-
-
-@lru_cache
-def _get_phase_normalization(C: tuple[float, ...]) -> float:
-    """Return the analyitcal integral for the phase normalization factor N."""
-    int_term1 = 2 * np.pi
-    int_term2 = 2 * C[0]
-    int_term3 = np.pi * C[1]
-    int_term4 = (np.exp(C[2] * np.pi) + 1) / (C[2] ** 2 + 1)
-    return 1 / (int_term1 * (int_term2 + int_term3 + int_term4))
diff --git a/zodipy/_types.py b/zodipy/_types.py
deleted file mode 100644
index 61014ff..0000000
--- a/zodipy/_types.py
+++ /dev/null
@@ -1,10 +0,0 @@
-from typing import Sequence, Union
-
-import astropy.units as u
-import numpy as np
-import numpy.typing as npt
-
-Pixels = Union[int, Sequence[int], npt.NDArray[np.integer]]
-SkyAngles = Union[u.Quantity[u.deg], u.Quantity[u.rad]]
-FrequencyOrWavelength = Union[u.Quantity[u.Hz], u.Quantity[u.m]]
-ParameterDict = dict
diff --git a/zodipy/_unit_vectors.py b/zodipy/_unit_vectors.py
deleted file mode 100644
index 8c0cca3..0000000
--- a/zodipy/_unit_vectors.py
+++ /dev/null
@@ -1,29 +0,0 @@
-from __future__ import annotations
-
-from typing import TYPE_CHECKING, Sequence
-
-import healpy as hp
-import numpy as np
-
-if TYPE_CHECKING:
-    import astropy.units as u
-    import numpy.typing as npt
-
-
-def get_unit_vectors_from_pixels(
-    coord_in: str, pixels: Sequence[int] | npt.NDArray[np.int64], nside: int
-) -> npt.NDArray[np.float64]:
-    """Return ecliptic unit vectors from HEALPix pixels representing some pointing."""
-    unit_vectors = np.asarray(hp.pix2vec(nside, pixels))
-    return np.asarray(hp.Rotator(coord=[coord_in, "E"])(unit_vectors))
-
-
-def get_unit_vectors_from_ang(
-    coord_in: str,
-    phi: u.Quantity[u.rad] | u.Quantity[u.deg],
-    theta: u.Quantity[u.rad] | u.Quantity[u.deg],
-    lonlat: bool = False,
-) -> npt.NDArray[np.float64]:
-    """Return ecliptic unit vectors from sky angles representing some pointing."""
-    unit_vectors = np.asarray(hp.ang2vec(theta, phi, lonlat=lonlat)).transpose()
-    return np.asarray(hp.Rotator(coord=[coord_in, "E"])(unit_vectors))
diff --git a/zodipy/_validators.py b/zodipy/_validators.py
deleted file mode 100644
index 51697a1..0000000
--- a/zodipy/_validators.py
+++ /dev/null
@@ -1,102 +0,0 @@
-from typing import Sequence, Tuple, Union
-
-import astropy.units as u
-import healpy as hp
-import numpy as np
-import numpy.typing as npt
-
-from ._ipd_model import InterplanetaryDustModel
-from ._types import FrequencyOrWavelength, Pixels, SkyAngles
-
-
-def get_validated_freq(
-    freq: FrequencyOrWavelength, model: InterplanetaryDustModel, extrapolate: bool
-) -> FrequencyOrWavelength:
-    """Validate user inputted frequency."""
-    if not isinstance(freq, u.Quantity):
-        msg = "Frequency must be an astropy Quantity."
-        raise TypeError(msg)
-
-    if freq.unit.is_equivalent(u.Hz):
-        freq = freq.to(u.Hz)
-    elif freq.unit.is_equivalent(u.micron):
-        freq = freq.to(u.micron)
-    else:
-        msg = "Frequency must be in units compatible with Hz or micron."
-        raise u.UnitsError(msg)
-
-    if extrapolate:
-        return freq
-
-    freq_in_spectrum_units = freq.to(model.spectrum.unit, equivalencies=u.spectral())
-    lower_freq_range = model.spectrum.min()
-    upper_freq_range = model.spectrum.max()
-
-    if freq_in_spectrum_units.isscalar:
-        freq_is_in_range = lower_freq_range <= freq_in_spectrum_units <= upper_freq_range
-    else:
-        freq_is_in_range = all(
-            lower_freq_range.value <= nu <= upper_freq_range.value and nu
-            for nu in freq_in_spectrum_units.value
-        )
-
-    if not freq_is_in_range:
-        msg = f"Model is only valid in the [{lower_freq_range}," f" {upper_freq_range}] range."
-        raise ValueError(msg)
-
-    return freq
-
-
-def get_validated_and_normalized_weights(
-    weights: Union[Sequence[float], npt.NDArray[np.floating], None],
-    freq: FrequencyOrWavelength,
-) -> npt.NDArray[np.float64]:
-    """Validate user inputted weights."""
-    if weights is None and freq.size > 1:
-        msg = "Bandpass weights must be specified if more than one frequency is given."
-        raise ValueError(msg)
-
-    if weights is not None:
-        if freq.size != len(weights):
-            msg = "Number of frequencies and weights must be the same."
-            raise ValueError(msg)
-        if np.any(np.diff(freq) < 0):
-            msg = "Bandpass frequencies must be strictly increasing."
-            raise ValueError(msg)
-
-        normalized_weights = np.asarray(weights, dtype=np.float64)
-    else:
-        normalized_weights = np.array([1], dtype=np.float64)
-
-    if normalized_weights.size > 1:
-        return normalized_weights / np.trapz(normalized_weights, freq.value)
-
-    return normalized_weights
-
-
-@u.quantity_input(theta=[u.deg, u.rad], phi=[u.deg, u.rad])
-def get_validated_ang(
-    theta: SkyAngles, phi: SkyAngles, lonlat: bool
-) -> Tuple[SkyAngles, SkyAngles]:
-    """Validate user inputted sky angles."""
-    theta = theta.to(u.deg) if lonlat else theta.to(u.rad)
-    phi = phi.to(u.deg) if lonlat else phi.to(u.rad)
-
-    if theta.isscalar:
-        theta = u.Quantity([theta])
-    if phi.isscalar:
-        phi = u.Quantity([phi])
-
-    return theta, phi
-
-
-def get_validated_pix(pixels: Pixels, nside: int) -> Pixels:
-    """Validate user inputted pixels."""
-    if (np.max(pixels) > hp.nside2npix(nside)) or (np.min(pixels) < 0):
-        msg = "invalid pixel number given nside"
-        raise ValueError(msg)
-
-    if np.ndim(pixels) == 0:
-        return np.expand_dims(pixels, axis=0)
-
-    return pixels
diff --git a/zodipy/blackbody.py b/zodipy/blackbody.py
new file mode 100644
index 0000000..e0a9e68
--- /dev/null
+++ b/zodipy/blackbody.py
@@ -0,0 +1,49 @@
+from __future__ import annotations
+
+import numpy as np
+import numpy.typing as npt
+from astropy import units
+from astropy.modeling.physical_models import BlackBody
+from scipy import integrate
+
+MIN_TEMP = 40 * units.K
+MAX_TEMP = 550 * units.K
+N_TEMPS = 100
+TEMPERATURES = np.linspace(MIN_TEMP, MAX_TEMP, N_TEMPS)
+blackbody = BlackBody(TEMPERATURES)
+
+
+def get_dust_grain_temperature(
+    R: npt.NDArray[np.float64], T_0: float, delta: float
+) -> npt.NDArray[np.float64]:
+    """Return the dust grain temperature given at a radial distance from the Sun.
+
+    Args:
+        R: Radial distance from the sun in ecliptic heliocentric coordinates [AU / 1AU].
+        T_0: Temperature of dust grains located 1 AU from the Sun [K].
+        delta: Powerlaw index.
+
+    Returns:
+        Dust grain temperature [K].
+
+    """
+    return T_0 * R**-delta
+
+
+def tabulate_blackbody_emission(
+    wavelengths: units.Quantity, weights: units.Quantity | None
+) -> npt.NDArray[np.float64]:
+    """Tabulate bandpass integrated blackbody specific intensity for a range of temperatures."""
+    if weights is None:
+        tabulated_blackbody_emission = blackbody(wavelengths)
+    else:
+        tabulated_blackbody_emission = integrate.trapezoid(
+            weights * blackbody(wavelengths[:, np.newaxis]).transpose(), wavelengths
+        )
+
+    return np.asarray(
+        [
+            TEMPERATURES.to_value(units.K),
+            tabulated_blackbody_emission.to_value(units.MJy / units.sr),
+        ]
+    )
diff --git a/zodipy/bodies.py b/zodipy/bodies.py
new file mode 100644
index 0000000..d2ea1a3
--- /dev/null
+++ b/zodipy/bodies.py
@@ -0,0 +1,99 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import astropy.coordinates as coords
+import numpy as np
+from astropy import time, units
+from scipy import interpolate
+
+if TYPE_CHECKING:
+    import numpy.typing as npt
+
+MEAN_DIST_TO_L2 = 0.009896235034000056
+
+
+def get_interp_obstimes(t0: float, t1: float) -> time.Time:
+    """Return a subset of the obstimes used to interpolate in body positions."""
+    dt = (1 * units.hour).to_value(units.day)
+    return time.Time(np.arange(t0, t1 + dt, dt), format="mjd")
+
+
+def get_interpolated_bodypos(
+    body: str,
+    obstimes: npt.NDArray[np.float64],
+    interp_obstimes: time.Time,
+    ephemeris: str,
+) -> np.ndarray:
+    """Return the interpolated heliocentric positions of body."""
+    pos = (
+        coords.get_body(body, interp_obstimes, ephemeris=ephemeris)
+        .transform_to(coords.HeliocentricMeanEcliptic)
+        .cartesian.xyz.to_value(units.AU)
+    )
+    interpolator = interpolate.CubicSpline(interp_obstimes.mjd, pos, axis=-1)
+    return interpolator(obstimes)
+
+
+def get_semb_l2_pos(
+    earthpos: npt.NDArray[np.float64],
+) -> npt.NDArray[np.float64]:
+    """Return a SkyCoord of the heliocentric position of the SEMB-L2 point.
+
+    Note that this is an approximate position, as the SEMB-L2 point is not included in
+    any of the current available ephemerides. We assume that SEMB-L2 is at all times
+    located at a fixed distance from Earth along the vector pointing to Earth from the Sun.
+    """
+    earth_distance = np.linalg.norm(earthpos)
+    SEMB_L2_distance = earth_distance + MEAN_DIST_TO_L2
+    earth_unit_vector = earthpos / earth_distance
+    return earth_unit_vector * SEMB_L2_distance
+
+
+def get_earthpos_inst(
+    obstime: time.Time,
+    ephemeris: str,
+) -> npt.NDArray[np.float64]:
+    """Return the sky coordinates of the Earth in the heliocentric frame."""
+    return (
+        coords.get_body("earth", obstime, ephemeris=ephemeris)
+        .transform_to(coords.HeliocentricMeanEcliptic)
+        .cartesian.xyz.to_value(units.AU)
+    ).flatten()
+
+
+def get_obspos_from_str(
+    body: str,
+    obstime: time.Time,
+    interp_obstimes: time.Time | None,
+    earthpos: npt.NDArray[np.float64],
+    ephemeris: str,
+) -> npt.NDArray[np.float64]:
+    """Return the sky coordinates of the observer in the heliocentric frame."""
+    if body == "semb-l2":
+        return get_semb_l2_pos(earthpos)
+    if body == "earth":
+        return earthpos
+
+    if obstime.size == 1:
+        try:
+            return (
+                coords.get_body(body, obstime, ephemeris=ephemeris)
+                .transform_to(coords.HeliocentricMeanEcliptic)
+                .cartesian.xyz.to_value(units.AU)
+            ).flatten()
+        except KeyError as error:
+            valid_obs = [*coords.solar_system_ephemeris.bodies, "semb-l2"]
+            msg = f"Invalid observer string: '{body}'. Valid observers are: {valid_obs}"
+            raise ValueError(msg) from error
+
+    if interp_obstimes is None:  # pragma: no cover
+        msg = "interp_obstimes must be provided when obstime is an array."
+        raise ValueError(msg)
+
+    return get_interpolated_bodypos(
+        body=body,
+        obstimes=obstime.mjd,
+        interp_obstimes=interp_obstimes,
+        ephemeris=ephemeris,
+    )
diff --git a/zodipy/_emission.py b/zodipy/brightness.py
similarity index 68%
rename from zodipy/_emission.py
rename to zodipy/brightness.py
index e7465d2..e587eb9 100644
--- a/zodipy/_emission.py
+++ b/zodipy/brightness.py
@@ -5,41 +5,41 @@
 import numpy as np
 import numpy.typing as npt
 
-from zodipy._ipd_model import RRM, InterplanetaryDustModel, Kelsall
-from zodipy._source_funcs import (
-    get_dust_grain_temperature,
-    get_phase_function,
-    get_scattering_angle,
-)
+from zodipy.blackbody import get_dust_grain_temperature
+from zodipy.scattering import get_phase_function, get_scattering_angle
 
 if TYPE_CHECKING:
-    from zodipy._ipd_dens_funcs import ComponentDensityFn
+    from zodipy.number_density import NumberDensityFunc
 
 """
 Function that return the zodiacal emission at a step along all lines of sight given
 a zodiacal model.
 """
-GetCompEmissionAtStepFn = Callable[..., npt.NDArray[np.float64]]
+BrightnessAtStepCallable = Callable[..., npt.NDArray[np.float64]]
 
 
-def kelsall(
+def kelsall_brightness_at_step(
     r: npt.NDArray[np.float64],
-    start: np.float64,
+    start: npt.NDArray[np.float64],
     stop: npt.NDArray[np.float64],
     X_obs: npt.NDArray[np.float64],
     u_los: npt.NDArray[np.float64],
-    get_density_function: ComponentDensityFn,
+    bp_interpolation_table: npt.NDArray[np.float64],
+    number_density_func: NumberDensityFunc,
     T_0: float,
     delta: float,
     emissivity: np.float64,
     albedo: np.float64,
-    phase_coefficients: tuple[float, ...],
+    C1: np.float64,
+    C2: np.float64,
+    C3: np.float64,
     solar_irradiance: np.float64,
-    bp_interpolation_table: npt.NDArray[np.float64],
 ) -> npt.NDArray[np.float64]:
     """Kelsall uses common line of sight grid from obs to 5.2 AU."""
     # Convert the quadrature range from [-1, 1] to the true ecliptic positions
-    R_los = ((stop - start) / 2) * r + (stop + start) / 2
+    # and back again at the end
+    R_los = 0.5 * (stop - start) * r + 0.5 * (stop + start)
+
     X_los = R_los * u_los
     X_helio = X_los + X_obs
     R_helio = np.sqrt(X_helio[0] ** 2 + X_helio[1] ** 2 + X_helio[2] ** 2)
@@ -47,32 +47,32 @@ def kelsall(
     temperature = get_dust_grain_temperature(R_helio, T_0, delta)
     blackbody_emission = np.interp(temperature, *bp_interpolation_table)
     emission = (1 - albedo) * (emissivity * blackbody_emission)
-
     if albedo != 0:
         solar_flux = solar_irradiance / R_helio**2
         scattering_angle = get_scattering_angle(R_los, R_helio, X_los, X_helio)
-        phase_function = get_phase_function(scattering_angle, phase_coefficients)
-
+        phase_function = get_phase_function(scattering_angle, C1, C2, C3)
         emission += albedo * solar_flux * phase_function
 
-    return emission * get_density_function(X_helio)
+    return emission * number_density_func(X_helio) * 0.5 * (stop - start)
 
 
-def rrm(
+def rrm_brightness_at_step(
     r: npt.NDArray[np.float64],
     start: npt.NDArray[np.float64],
     stop: npt.NDArray[np.float64],
     X_obs: npt.NDArray[np.float64],
     u_los: npt.NDArray[np.float64],
-    get_density_function: ComponentDensityFn,
+    bp_interpolation_table: npt.NDArray[np.float64],
+    number_density_func: NumberDensityFunc,
     T_0: float,
     delta: float,
     calibration: np.float64,
-    bp_interpolation_table: npt.NDArray[np.float64],
 ) -> npt.NDArray[np.float64]:
     """RRM is implented with component specific line-of-sight grids."""
     # Convert the quadrature range from [-1, 1] to the true ecliptic positions
-    R_los = ((stop - start) / 2) * r + (stop + start) / 2
+    # and back again at the end
+    R_los = 0.5 * (stop - start) * r + 0.5 * (stop + start)
+
     X_los = R_los * u_los
     X_helio = X_los + X_obs
     R_helio = np.sqrt(X_helio[0] ** 2 + X_helio[1] ** 2 + X_helio[2] ** 2)
@@ -80,10 +80,4 @@ def rrm(
     temperature = get_dust_grain_temperature(R_helio, T_0, delta)
     blackbody_emission = np.interp(temperature, *bp_interpolation_table)
 
-    return blackbody_emission * get_density_function(X_helio) * calibration
-
-
-EMISSION_MAPPING: dict[type[InterplanetaryDustModel], GetCompEmissionAtStepFn] = {
-    Kelsall: kelsall,
-    RRM: rrm,
-}
+    return blackbody_emission * number_density_func(X_helio) * calibration * 0.5 * (stop - start)
diff --git a/zodipy/_ipd_comps.py b/zodipy/component.py
similarity index 86%
rename from zodipy/_ipd_comps.py
rename to zodipy/component.py
index 2efc914..6e7d163 100644
--- a/zodipy/_ipd_comps.py
+++ b/zodipy/component.py
@@ -1,6 +1,6 @@
 from __future__ import annotations
 
-from abc import ABC
+import abc
 from dataclasses import dataclass, field
 from enum import Enum
 from typing import TYPE_CHECKING
@@ -12,7 +12,7 @@
 
 
 @dataclass
-class Component(ABC):
+class ZodiacalComponent(abc.ABC):
     """Base class for storing common model parameters for zodiacal components.
 
     Args:
@@ -37,7 +37,7 @@ class Component(ABC):
     cos_Omega_rad: float = field(init=False)
 
     def __post_init__(self) -> None:
-        self.X_0 = np.array([self.x_0, self.y_0, self.z_0]).reshape(3, 1, 1)
+        self.X_0 = np.expand_dims([self.x_0, self.y_0, self.z_0], axis=-1)
         self.sin_i_rad = np.sin(np.radians(self.i))
         self.cos_i_rad = np.cos(np.radians(self.i))
         self.sin_Omega_rad = np.sin(np.radians(self.Omega))
@@ -45,7 +45,7 @@ def __post_init__(self) -> None:
 
 
 @dataclass
-class Cloud(Component):
+class Cloud(ZodiacalComponent):
     """DIRBE diffuse cloud.
 
     Args:
@@ -65,7 +65,7 @@ class Cloud(Component):
 
 
 @dataclass
-class Band(Component):
+class Band(ZodiacalComponent):
     """DIRBE asteroidal dust band.
 
     Args:
@@ -90,7 +90,7 @@ def __post_init__(self) -> None:
 
 
 @dataclass
-class Ring(Component):
+class Ring(ZodiacalComponent):
     """DIRBE circum-solar ring (excluding the Earth-trailing Feature).
 
     Args:
@@ -108,7 +108,7 @@ class Ring(Component):
 
 
 @dataclass
-class Feature(Component):
+class Feature(ZodiacalComponent):
     """DIRBE Earth-trailing Feature.
 
     Args:
@@ -137,7 +137,9 @@ def __post_init__(self) -> None:
 
 
 @dataclass
-class Fan(Component):
+class Fan(ZodiacalComponent):
+    """RRM fan."""
+
     gamma: float
     Z_0: float
     Q: float
@@ -146,7 +148,9 @@ class Fan(Component):
 
 
 @dataclass
-class Comet(Component):
+class Comet(ZodiacalComponent):
+    """RRM comet."""
+
     gamma: float
     Z_0: float
     P: float
@@ -156,12 +160,16 @@ class Comet(Component):
 
 
 @dataclass
-class Interstellar(Component):
+class Interstellar(ZodiacalComponent):
+    """RRM interstellar dust."""
+
     amp: float
 
 
 @dataclass
-class NarrowBand(Component):
+class NarrowBand(ZodiacalComponent):
+    """RRM narrow band."""
+
     gamma: float
     A: float
     G: float
@@ -171,7 +179,9 @@ class NarrowBand(Component):
 
 
 @dataclass
-class BroadBand(Component):
+class BroadBand(ZodiacalComponent):
+    """RRM broad band."""
+
     gamma: float
     A: float
     R_inner: float
@@ -182,11 +192,15 @@ class BroadBand(Component):
 
 @dataclass
 class RingRRM(Ring):
+    """RRM circum-solar ring."""
+
     A: float
 
 
 @dataclass
 class FeatureRRM(Feature):
+    """RRM Earth-trailing Feature."""
+
     A: float
 
 
diff --git a/zodipy/comps.py b/zodipy/component_params.py
similarity index 95%
rename from zodipy/comps.py
rename to zodipy/component_params.py
index b44226b..99fdfea 100644
--- a/zodipy/comps.py
+++ b/zodipy/component_params.py
@@ -1,12 +1,10 @@
 from __future__ import annotations
 
-from zodipy._constants import R_ASTEROID_BELT, R_KUIPER_BELT, R_MARS
-from zodipy._ipd_comps import (
+from zodipy.component import (
     Band,
     BroadBand,
     Cloud,
     Comet,
-    Component,
     ComponentLabel,
     Fan,
     Feature,
@@ -15,9 +13,10 @@
     NarrowBand,
     Ring,
     RingRRM,
+    ZodiacalComponent,
 )
 
-DIRBE: dict[ComponentLabel, Component] = {
+DIRBE: dict[ComponentLabel, ZodiacalComponent] = {
     ComponentLabel.CLOUD: Cloud(
         x_0=0.011887800744346281,
         y_0=0.0054765064662263777,
@@ -96,8 +95,11 @@
 PLANCK.pop(ComponentLabel.RING)
 PLANCK.pop(ComponentLabel.FEATURE)
 
+R_MARS = 1.5237
+R_ASTEROID_BELT = 3.137
+R_KUIPER_BELT = 30
 
-RRM: dict[ComponentLabel, Component] = {
+RRM: dict[ComponentLabel, ZodiacalComponent] = {
     ComponentLabel.FAN: Fan(
         x_0=0.0,
         y_0=0.0,
diff --git a/zodipy/_line_of_sight.py b/zodipy/line_of_sight.py
similarity index 69%
rename from zodipy/_line_of_sight.py
rename to zodipy/line_of_sight.py
index 7f55a4a..868455f 100644
--- a/zodipy/_line_of_sight.py
+++ b/zodipy/line_of_sight.py
@@ -1,27 +1,31 @@
 from __future__ import annotations
 
-from typing import TYPE_CHECKING, Iterable
+from typing import TYPE_CHECKING, Callable, Iterable
 
 import numpy as np
 
-from zodipy._constants import R_0, R_EARTH, R_JUPITER, R_KUIPER_BELT
-from zodipy._ipd_comps import ComponentLabel
-from zodipy.comps import RRM
+from zodipy.component import ComponentLabel
+from zodipy.component_params import RRM
 
 if TYPE_CHECKING:
     import numpy.typing as npt
 
 
-DIRBE_CUTOFFS: dict[ComponentLabel, tuple[float | np.float64, float]] = {
+R_0 = np.finfo(np.float64).eps
+R_KUIPER_BELT = 30
+R_EARTH = 1
+R_JUPITER = 5.2
+
+DIRBE_CUTOFFS: dict[ComponentLabel, tuple[float | np.float64, float | np.float64]] = {
     ComponentLabel.CLOUD: (R_0, R_JUPITER),
     ComponentLabel.BAND1: (R_0, R_JUPITER),
     ComponentLabel.BAND2: (R_0, R_JUPITER),
     ComponentLabel.BAND3: (R_0, R_JUPITER),
     ComponentLabel.RING: (R_EARTH - 0.2, R_EARTH + 0.2),
-    ComponentLabel.FEATURE: (R_EARTH - 0.2, R_EARTH + 0.2),
+    ComponentLabel.FEATURE: (R_EARTH - 0.3, R_EARTH + 0.3),
 }
 
-RRM_CUTOFFS: dict[ComponentLabel, tuple[float | np.float64, float]] = {
+RRM_CUTOFFS: dict[ComponentLabel, tuple[float | np.float64, float | np.float64]] = {
     ComponentLabel.FAN: (R_0, RRM[ComponentLabel.FAN].R_outer),  # type: ignore
     ComponentLabel.INNER_NARROW_BAND: (
         RRM[ComponentLabel.INNER_NARROW_BAND].R_inner,  # type: ignore
@@ -48,29 +52,32 @@
 COMPONENT_CUTOFFS = {**DIRBE_CUTOFFS, **RRM_CUTOFFS}
 
 
+def integrate_leggauss(
+    func: Callable[[float], npt.NDArray[np.float64]],
+    points: npt.NDArray[np.float64],
+    weights: npt.NDArray[np.float64],
+) -> npt.NDArray[np.float64]:
+    """Integrate a function using Gauss-Laguerre quadrature."""
+    return np.squeeze(sum(func(x) * w for x, w in zip(points, weights)))
+
+
 def get_sphere_intersection(
     obs_pos: npt.NDArray[np.float64],
     unit_vectors: npt.NDArray[np.float64],
     cutoff: float | np.float64,
 ) -> npt.NDArray[np.float64]:
-    """Get RMAX per pixel.
-
-    Given the observer position, return distance from the observer to the
-    intersection between the line-of-sights and a heliocentric sphere with radius `cutoff`.
-
-    """
-    x, y, z = obs_pos.flatten()
-    r_obs = np.sqrt(x**2 + y**2 + z**2)
-
-    if r_obs > cutoff:
-        return np.asarray([np.finfo(float).eps])
+    """Returns the distance from the observer to a heliocentric sphere with radius `cutoff`."""
+    x_0, y_0, z_0 = obs_pos
+    r_obs = np.sqrt(x_0**2 + y_0**2 + z_0**2)
+    if (r_obs > cutoff).any():
+        return np.full(obs_pos.shape[-1], np.finfo(float).eps)
 
     u_x, u_y, u_z = unit_vectors
     lon = np.arctan2(u_y, u_x)
     lat = np.arcsin(u_z)
 
     cos_lat = np.cos(lat)
-    b = 2 * (x * cos_lat * np.cos(lon) + y * cos_lat * np.sin(lon))
+    b = 2 * (x_0 * cos_lat * np.cos(lon) + y_0 * cos_lat * np.sin(lon))
     c = r_obs**2 - cutoff**2
 
     q = -0.5 * b * (1 + np.sqrt(b**2 - 4 * c) / np.abs(b))
@@ -78,7 +85,7 @@ def get_sphere_intersection(
     return np.maximum(q, c / q)
 
 
-def get_line_of_sight_start_and_stop_distances(
+def get_line_of_sight_range_dicts(
     components: Iterable[ComponentLabel],
     unit_vectors: npt.NDArray[np.float64],
     obs_pos: npt.NDArray[np.float64],
@@ -86,7 +93,7 @@ def get_line_of_sight_start_and_stop_distances(
     dict[ComponentLabel, npt.NDArray[np.float64]],
     dict[ComponentLabel, npt.NDArray[np.float64]],
 ]:
-    """Get the start and stop distances for each component."""
+    """Return the component-wise start- and end-point of each line of sight."""
     start = {
         comp: get_sphere_intersection(obs_pos, unit_vectors, COMPONENT_CUTOFFS[comp][0])
         for comp in components
diff --git a/zodipy/model.py b/zodipy/model.py
new file mode 100644
index 0000000..33a3d46
--- /dev/null
+++ b/zodipy/model.py
@@ -0,0 +1,331 @@
+from __future__ import annotations
+
+import functools
+import itertools
+import multiprocessing
+import platform
+
+import numpy as np
+import numpy.typing as npt
+from astropy import coordinates as coords
+from astropy import time, units
+from scipy import integrate
+
+from zodipy.blackbody import tabulate_blackbody_emission
+from zodipy.bodies import (
+    get_earthpos_inst,
+    get_interp_obstimes,
+    get_interpolated_bodypos,
+    get_obspos_from_str,
+)
+from zodipy.component import ComponentLabel
+from zodipy.line_of_sight import (
+    get_line_of_sight_range_dicts,
+    integrate_leggauss,
+)
+from zodipy.model_registry import model_registry
+from zodipy.number_density import get_partial_number_density_func, update_partial_earth_pos
+from zodipy.unpack_model import get_model_interp_func
+
+PLATFORM_METHOD = "fork" if "windows" not in platform.system().lower() else None
+
+
+class Model:
+    """Main interface to ZodiPy."""
+
+    def __init__(
+        self,
+        x: units.Quantity,
+        *,
+        weights: npt.ArrayLike | None = None,
+        name: str = "dirbe",
+        gauss_quad_degree: int = 50,
+        extrapolate: bool = False,
+        ephemeris: str = "builtin",
+    ) -> None:
+        """Initialize the ZodiPy model interface.
+
+        Args:
+            x: Wavelength or frequency. If `x` is a sequence, it is assumed to be a the points
+                corresponding to a bandpass and the corresponding `weights` must be provided.
+            weights: Bandpass weights corresponding the the frequencies or wavelengths in `x`. The
+                weights are assumed to represent a normalized instrument response in units of
+                spectral radiance (Jy/sr).
+            name: Zodiacal light model to use for the simulations. For a list of available models,
+                see https://cosmoglobe.github.io/zodipy/introduction/. Defaults to 'dirbe'.
+            gauss_quad_degree: Order of the Gaussian-legendre quadrature used to evaluate the
+                line-of-sight integrals in the simulations. Default is 50 points.
+            extrapolate: If `True` all spectral quantities in the selected model are extrapolated to
+                the requested frequencies or wavelengths. If `False`, an exception is raised on
+                requested values of `x` outside of the valid model range. Default is `False`.
+            ephemeris: Ephemeris used in `astropy.coordinates.solar_system_ephemeris` to compute the
+                positions of the observer and Earth. Defaults to 'builtin'. See the
+                [Astropy documentation](https://docs.astropy.org/en/stable/coordinates/solarsystem.html)
+                for available ephemerides.
+
+        """
+        try:
+            if not x.isscalar and weights is None:
+                msg = "Bandpass weights must be provided for non-scalar `x`."
+                raise ValueError(msg)
+        except AttributeError as error:
+            msg = "The input 'x' must be an astropy Quantity."
+            raise TypeError(msg) from error
+        if x.isscalar and weights is not None:
+            msg = "Bandpass weights should not be provided for scalar `x`."
+            raise ValueError(msg)
+
+        self._ipd_model = model_registry.get_model(name)
+
+        if not extrapolate and not self._ipd_model.is_valid_at(x):
+            msg = (
+                "The requested frequencies are outside the valid range of the model. "
+                "If this was intended, set the extrapolate argument to True."
+            )
+            raise ValueError(msg)
+
+        # Bandpass is provided rather than a delta wavelength or frequency.
+        if weights is not None:
+            weights = np.asarray(weights)
+            if x.size != weights.size:
+                msg = "Number of wavelengths and weights must be the same in the bandpass."
+                raise ValueError(msg)
+            normalized_weights = weights / integrate.trapezoid(weights, x)
+        else:
+            normalized_weights = None
+
+        self._x = x
+        self._normalized_weights = normalized_weights
+        self._b_nu_table = tabulate_blackbody_emission(self._x, self._normalized_weights)
+
+        quad_points, quad_weights = np.polynomial.legendre.leggauss(gauss_quad_degree)
+        self._integrate_leggauss = functools.partial(
+            integrate_leggauss,
+            points=quad_points,
+            weights=quad_weights,
+        )
+
+        self._ephemeris = ephemeris
+
+        # Make mypy happy by declaring types of to-be initialized attributes.
+        self._number_density_partials: dict[ComponentLabel, functools.partial]
+        self._interped_comp_params: dict[ComponentLabel, dict]
+        self._interped_shared_params: dict
+
+        self._init_ipd_model_partials()
+
+    def evaluate(
+        self,
+        skycoord: coords.SkyCoord,
+        *,
+        obspos: units.Quantity | str = "earth",
+        return_comps: bool = False,
+        nprocesses: int = 1,
+    ) -> units.Quantity[units.MJy / units.sr]:
+        """Return the simulated zodiacal light.
+
+        The zodiacal light is simulated for a single, or a sequence of observations. If a single
+        `obspos` and `obstime` is provided for multiple coordinates, all coordinates are assumed to
+        be observed from that position at that time. Otherwise, each coordinate is simulated from
+        the corresponding observer position and time.
+
+        Args:
+            skycoord: `astropy.coordinates.SkyCoord` object representing the coordinates or
+                observations for which to simulate the zodiacal light. The `frame` and `obstime`
+                attributes of the `SkyCoord` object must be set. The `obstime` attribute must be
+                specified, and correspond to a single, or a sequence of observational times with
+                length matching the number of coordinates. The frame must be convertible to the
+                `astropy.coordinates.BarycentricMeanEcliptic` frame.
+            obspos: The heliocentric ecliptic position of the observer, or a string representing
+                an observer in the `astropy.coordinates.solar_system_ephemeris`. If an explicit
+                position is given, it must either be a single, or a sequence of positions with
+                shape matching the number of coordinates Defaults to 'earth'.
+            return_comps: If True, the emission is returned component-wise. Defaults to False.
+            nprocesses: Number of cores to use. If `nprocesses >= 1`, the line-of-sight integrals
+                are parallelized using the `multiprocessing` module. Defaults to 1.
+
+        Returns:
+            emission: Simulated zodiacal light in units of 'MJy/sr'.
+
+        """
+        try:
+            if skycoord.obstime is None:
+                msg = "The `obstime` attribute of the `SkyCoord` object is not set."
+                raise ValueError(msg)
+        except AttributeError as error:
+            msg = "The input coordinates must be an astropy SkyCoord object."
+            raise TypeError(msg) from error
+
+        try:
+            if not (obspos_isstr := isinstance(obspos, str)) and (
+                (obspos.ndim > 1 and skycoord.obstime.size != obspos.shape[-1])
+                or (obspos.ndim == 1 and skycoord.obstime.size != 1)
+            ):
+                msg = "The number of obstime (ncoords) and obspos (3, ncoords) does not match."
+                raise ValueError(msg)
+        except AttributeError as error:
+            msg = "The observer position is not a string or an astropy Quantity."
+            raise TypeError(msg) from error
+
+        if skycoord.obstime.size > skycoord.size:
+            msg = "The size of obstime must be either 1 or ncoords."
+            raise ValueError(msg)
+
+        if skycoord.obstime.size == 1:
+            interp_obstimes = None
+        else:
+            interp_obstimes = get_interp_obstimes(skycoord.obstime[0].mjd, skycoord.obstime[-1].mjd)
+
+        dist_coords_to_cores = skycoord.size > nprocesses and nprocesses > 1
+        if dist_coords_to_cores:
+            skycoord_splits = np.array_split(skycoord, nprocesses)
+            obspos_splits = (
+                itertools.repeat(obspos, nprocesses)
+                if obspos_isstr
+                else np.array_split(obspos, nprocesses, axis=-1)
+            )
+            interp_obstime_splits = itertools.repeat(interp_obstimes, nprocesses)
+            with multiprocessing.get_context(PLATFORM_METHOD).Pool(nprocesses) as pool:
+                emission_splits = [
+                    pool.apply_async(self._evaluate, args=(skycoord, obspos, obstime_lims))
+                    for skycoord, obspos, obstime_lims in zip(
+                        skycoord_splits, obspos_splits, interp_obstime_splits
+                    )
+                ]
+                emission = np.concatenate([split.get() for split in emission_splits], axis=-1)
+        else:
+            emission = self._evaluate(skycoord, obspos, interp_obstimes)
+
+        emission <<= units.MJy / units.sr
+        return emission if return_comps else emission.sum(axis=0)
+
+    def _evaluate(
+        self,
+        skycoord: coords.SkyCoord,
+        obspos: units.Quantity | str,
+        interp_obstimes: time.Time | None,
+    ) -> npt.NDArray[np.float64]:
+        """Evaluate the zodiacal light for a single or a sequence of sky coordinates."""
+        if interp_obstimes is None:
+            earth_xyz = get_earthpos_inst(skycoord.obstime, self._ephemeris)
+        else:
+            earth_xyz = get_interpolated_bodypos(
+                body="earth",
+                obstimes=skycoord.obstime.mjd,
+                interp_obstimes=interp_obstimes,
+                ephemeris=self._ephemeris,
+            )
+
+        if isinstance(obspos, str):
+            obs_xyz = get_obspos_from_str(
+                body=obspos,
+                obstime=skycoord.obstime,
+                interp_obstimes=interp_obstimes,
+                earthpos=earth_xyz,
+                ephemeris=self._ephemeris,
+            )
+        else:
+            try:
+                obs_xyz = obspos.to_value(units.AU)
+            except units.UnitConversionError as error:
+                msg = "The observer position must be in length units."
+                raise units.UnitConversionError(msg) from error
+
+        instantaneous = skycoord.obstime.size == 1
+        if instantaneous:
+            obs_xyz = obs_xyz[:, np.newaxis]
+        if earth_xyz.ndim == 1:
+            earth_xyz = earth_xyz[:, np.newaxis]
+
+        # Model evaluation is performed in heliocentric ecliptic coordinates. We transform
+        # to the barycentric frame, which is compatiable with the Galactic and Celestial,
+        # and pretend that that this is the heliocentric frame as we only need the correction
+        # rotation.
+        skycoord = skycoord.transform_to(coords.BarycentricMeanEcliptic)
+
+        skycoord_xyz: npt.NDArray[np.float64] = skycoord.cartesian.xyz.value
+        if skycoord.isscalar:
+            skycoord_xyz = skycoord.cartesian.xyz.value[:, np.newaxis]
+
+        start, stop = get_line_of_sight_range_dicts(
+            components=self._ipd_model.comps.keys(),
+            unit_vectors=skycoord_xyz,
+            obs_pos=obs_xyz,
+        )
+
+        number_density_partials = self._number_density_partials
+        shared_brightness_partial = self._shared_brightness_partial
+
+        number_density_partials = update_partial_earth_pos(
+            number_density_partials, earth_pos=earth_xyz
+        )
+        shared_brightness_partial = functools.partial(shared_brightness_partial, X_obs=obs_xyz)
+
+        emission = np.zeros((self._ipd_model.ncomps, skycoord.size))
+        for idx, comp_label in enumerate(self._ipd_model.comps.keys()):
+            comp_func = functools.partial(
+                shared_brightness_partial,
+                u_los=skycoord_xyz,
+                start=start[comp_label],
+                stop=stop[comp_label],
+                number_density_func=number_density_partials[comp_label],
+                **self._interped_comp_params[comp_label],
+            )
+            emission[idx] = self._integrate_leggauss(comp_func)
+
+        return emission
+
+    def _init_ipd_model_partials(self) -> None:
+        """Initialize the partial functions for the interplanetary dust model.
+
+        The spectrally dependant model parameters are interpolated over the provided bandpass or
+        delta frequency/wavelength. The partial functions are pre-populated functions that contains
+        all non line-of-sight related parameters.
+        """
+        interp_and_unpack_func = get_model_interp_func(self._ipd_model)
+        dicts = interp_and_unpack_func(self._x, self._normalized_weights, self._ipd_model)
+        self._interped_comp_params = dicts[0]
+        self._interped_shared_params = dicts[1]
+
+        self._shared_brightness_partial = functools.partial(
+            self._ipd_model.brightness_at_step_callable,
+            bp_interpolation_table=self._b_nu_table,
+            **self._interped_shared_params,
+        )
+
+        self._number_density_partials = get_partial_number_density_func(comps=self._ipd_model.comps)
+
+    def get_parameters(self) -> dict:
+        """Return a dictionary containing the interplanetary dust model parameters.
+
+        This method is mainly meant to be used to fit or sample zodiacal light models.
+
+        Returns:
+            parameters: Dictionary of parameters of the interplanetary dust model.
+        """
+        return self._ipd_model.to_dict()
+
+    def update_parameters(self, parameters: dict) -> None:
+        """Update the interplanetary dust model parameters.
+
+        This method is mainly meant to be used to fit or sample zodiacal light models.
+
+        Args:
+            parameters: Dictionary of parameters to update. The keys must be the names
+                of the parameters as defined in the model. To get the parameters dict
+                of an existing model, use the`get_parameters` method of an initialized
+                `zodipy.Model`.
+        """
+        _dict = parameters.copy()
+        _dict["comps"] = {}
+        for key, value in parameters.items():
+            if key == "comps":
+                for comp_key, comp_value in value.items():
+                    _dict["comps"][ComponentLabel(comp_key)] = type(
+                        self._ipd_model.comps[ComponentLabel(comp_key)]
+                    )(**comp_value)
+            elif isinstance(value, dict):
+                _dict[key] = {ComponentLabel(k): v for k, v in value.items()}
+
+        self._ipd_model = self._ipd_model.__class__(**_dict)
+        self._init_ipd_model_partials()
diff --git a/zodipy/model_registry.py b/zodipy/model_registry.py
index 34ad223..0cea1aa 100644
--- a/zodipy/model_registry.py
+++ b/zodipy/model_registry.py
@@ -1,15 +1,17 @@
-from zodipy import comps, source_params
-from zodipy._ipd_model import RRM, Kelsall, model_registry
+from zodipy import component_params, source_params
+from zodipy.zodiacal_light_model import RRM, Kelsall, model_registry
 
 model_registry.register_model(
     name="dirbe",
     model=Kelsall(
-        comps=comps.DIRBE,
+        comps=component_params.DIRBE,
         spectrum=source_params.SPECTRUM_DIRBE,
         emissivities=source_params.EMISSIVITY_DIRBE,
         albedos=source_params.ALBEDO_DIRBE,
         solar_irradiance=source_params.SOLAR_IRRADIANCE_DIRBE,
-        phase_coefficients=source_params.PHASE_FUNCTION_DIRBE,
+        C1=source_params.C1_DIRBE,
+        C2=source_params.C2_DIRBE,
+        C3=source_params.C3_DIRBE,
         T_0=source_params.T_0_DIRBE,
         delta=source_params.DELTA_DIRBE,
     ),
@@ -18,7 +20,7 @@
 model_registry.register_model(
     name="planck13",
     model=Kelsall(
-        comps=comps.DIRBE,
+        comps=component_params.DIRBE,
         spectrum=source_params.SPECTRUM_PLANCK,
         emissivities=source_params.EMISSIVITY_PLANCK_13,
         T_0=source_params.T_0_DIRBE,
@@ -29,7 +31,7 @@
 model_registry.register_model(
     name="planck15",
     model=Kelsall(
-        comps=comps.PLANCK,
+        comps=component_params.PLANCK,
         spectrum=source_params.SPECTRUM_PLANCK,
         emissivities=source_params.EMISSIVITY_PLANCK_15,
         T_0=source_params.T_0_DIRBE,
@@ -40,7 +42,7 @@
 model_registry.register_model(
     name="planck18",
     model=Kelsall(
-        comps=comps.PLANCK,
+        comps=component_params.PLANCK,
         spectrum=source_params.SPECTRUM_PLANCK,
         emissivities=source_params.EMISSIVITY_PLANCK_18,
         T_0=source_params.T_0_DIRBE,
@@ -51,7 +53,7 @@
 model_registry.register_model(
     name="odegard",
     model=Kelsall(
-        comps=comps.PLANCK,
+        comps=component_params.PLANCK,
         spectrum=source_params.SPECTRUM_PLANCK,
         emissivities=source_params.EMISSIVITY_ODEGARD,
         T_0=source_params.T_0_DIRBE,
@@ -62,7 +64,7 @@
 model_registry.register_model(
     name="rrm-experimental",
     model=RRM(
-        comps=comps.RRM,
+        comps=component_params.RRM,
         spectrum=source_params.SPECTRUM_IRAS,
         calibration=source_params.CALIBRATION_RRM,
         T_0=source_params.T_0_RRM,
diff --git a/zodipy/_ipd_dens_funcs.py b/zodipy/number_density.py
similarity index 71%
rename from zodipy/_ipd_dens_funcs.py
rename to zodipy/number_density.py
index 7109a99..e3d9ae9 100644
--- a/zodipy/_ipd_dens_funcs.py
+++ b/zodipy/number_density.py
@@ -1,19 +1,20 @@
 from __future__ import annotations
 
+import copy
 import inspect
 from dataclasses import asdict
 from functools import partial
-from typing import Any, Callable, Mapping, Protocol, Sequence
+from typing import TYPE_CHECKING, Callable, Mapping, Protocol
 
 import numpy as np
-import numpy.typing as npt  # type: ignore
+import numpy.typing as npt
 
-from zodipy._ipd_comps import (
+from zodipy.bodies import get_earthpos_inst
+from zodipy.component import (
     Band,
     BroadBand,
     Cloud,
     Comet,
-    Component,
     ComponentLabel,
     Fan,
     Feature,
@@ -22,7 +23,13 @@
     NarrowBand,
     Ring,
     RingRRM,
+    ZodiacalComponent,
 )
+from zodipy.model_registry import model_registry
+
+if TYPE_CHECKING:
+    from astropy import time, units
+
 
 """The density functions for the different types of components.
 
@@ -36,7 +43,7 @@ class must have all the parameters as instance variables.
 ComputeDensityFunc = Callable[..., npt.NDArray[np.float64]]
 
 
-def compute_cloud_density(
+def cloud_number_density(
     X_helio: npt.NDArray[np.float64],
     X_0: npt.NDArray[np.float64],
     sin_Omega_rad: float,
@@ -52,7 +59,6 @@ def compute_cloud_density(
     """Density of the diffuse cloud (see Eq (6). in K98)."""
     X_cloud = X_helio - X_0
     R_cloud = np.sqrt(X_cloud[0] ** 2 + X_cloud[1] ** 2 + X_cloud[2] ** 2)
-
     Z_cloud = (
         X_cloud[0] * sin_Omega_rad * sin_i_rad
         - X_cloud[1] * cos_Omega_rad * sin_i_rad
@@ -66,7 +72,7 @@ def compute_cloud_density(
     return n_0 * R_cloud**-alpha * np.exp(-beta * g**gamma)
 
 
-def compute_band_density(
+def band_number_density(
     X_helio: npt.NDArray[np.float64],
     X_0: npt.NDArray[np.float64],
     sin_Omega_rad: float,
@@ -103,7 +109,7 @@ def compute_band_density(
     return term1 * term2 * term3 * term4
 
 
-def compute_ring_density(
+def ring_number_density(
     X_helio: npt.NDArray[np.float64],
     X_0: npt.NDArray[np.float64],
     sin_Omega_rad: float,
@@ -132,7 +138,7 @@ def compute_ring_density(
     return n_0 * np.exp(term1 - term2)
 
 
-def compute_feature_density(
+def feature_number_density(
     X_helio: npt.NDArray[np.float64],
     X_0: npt.NDArray[np.float64],
     X_earth: npt.NDArray[np.float64],
@@ -156,8 +162,8 @@ def compute_feature_density(
         - X_feature[1] * cos_Omega_rad * sin_i_rad
         + X_feature[2] * cos_i_rad
     )
-    X_earth_comp = X_earth - X_0
 
+    X_earth_comp = X_earth - X_0
     theta_comp = np.arctan2(X_feature[1], X_feature[0]) - np.arctan2(
         X_earth_comp[1], X_earth_comp[0]
     )
@@ -174,7 +180,7 @@ def compute_feature_density(
     return n_0 * np.exp(-exp_term)
 
 
-def compute_fan_density(  # *
+def fan_number_density(
     X_helio: npt.NDArray[np.float64],
     X_0: npt.NDArray[np.float64],
     sin_Omega_rad: float,
@@ -211,7 +217,7 @@ def compute_fan_density(  # *
     return density
 
 
-def compute_comet_density(
+def comet_number_density(
     X_helio: npt.NDArray[np.float64],
     X_0: npt.NDArray[np.float64],
     sin_Omega_rad: float,
@@ -249,14 +255,15 @@ def compute_comet_density(
     return density
 
 
-def compute_interstellar_density(
+def interstellar_number_density(
     X_helio: npt.NDArray[np.float64],
     amp: float,
 ) -> npt.NDArray[np.float64]:
+    """Interstellar constant number density."""
     return np.array([amp])
 
 
-def compute_narrow_band_density(
+def narrow_band_number_density(
     X_helio: npt.NDArray[np.float64],
     X_0: npt.NDArray[np.float64],
     sin_Omega_rad: float,
@@ -296,7 +303,7 @@ def compute_narrow_band_density(
     return density
 
 
-def compute_broad_band_density(
+def broad_band_number_density(
     X_helio: npt.NDArray[np.float64],
     X_0: npt.NDArray[np.float64],
     sin_Omega_rad: float,
@@ -334,7 +341,7 @@ def compute_broad_band_density(
     return density
 
 
-def compute_ring_density_rmm(
+def rrm_ring_number_density(
     X_helio: npt.NDArray[np.float64],
     X_0: npt.NDArray[np.float64],
     sin_Omega_rad: float,
@@ -347,7 +354,8 @@ def compute_ring_density_rmm(
     sigma_z: float,
     A: float,
 ) -> npt.NDArray[np.float64]:
-    return A * compute_ring_density(
+    """RRM ring is just K98 ring with a scaling factor."""
+    return A * ring_number_density(
         X_helio=X_helio,
         X_0=X_0,
         sin_Omega_rad=sin_Omega_rad,
@@ -361,7 +369,7 @@ def compute_ring_density_rmm(
     )
 
 
-def compute_feature_density_rmm(
+def rrm_feature_number_density(
     X_helio: npt.NDArray[np.float64],
     X_0: npt.NDArray[np.float64],
     X_earth: npt.NDArray[np.float64],
@@ -377,7 +385,8 @@ def compute_feature_density_rmm(
     sigma_theta_rad: float,
     A: float,
 ) -> npt.NDArray[np.float64]:
-    return A * compute_feature_density(
+    """RRM feature is just K98 feature with a scaling factor."""
+    return A * feature_number_density(
         X_helio=X_helio,
         X_0=X_0,
         sin_Omega_rad=sin_Omega_rad,
@@ -395,99 +404,126 @@ def compute_feature_density_rmm(
 
 
 # Mapping of implemented zodiacal component data classes and their density functions.
-DENSITY_FUNCS: dict[type[Component], ComputeDensityFunc] = {
-    Cloud: compute_cloud_density,
-    Band: compute_band_density,
-    Ring: compute_ring_density,
-    Feature: compute_feature_density,
-    Fan: compute_fan_density,
-    Comet: compute_comet_density,
-    Interstellar: compute_interstellar_density,
-    NarrowBand: compute_narrow_band_density,
-    BroadBand: compute_broad_band_density,
-    RingRRM: compute_ring_density_rmm,
-    FeatureRRM: compute_feature_density_rmm,
+DENSITY_FUNCS: dict[type[ZodiacalComponent], ComputeDensityFunc] = {
+    Cloud: cloud_number_density,
+    Band: band_number_density,
+    Ring: ring_number_density,
+    Feature: feature_number_density,
+    Fan: fan_number_density,
+    Comet: comet_number_density,
+    Interstellar: interstellar_number_density,
+    NarrowBand: narrow_band_number_density,
+    BroadBand: broad_band_number_density,
+    RingRRM: rrm_ring_number_density,
+    FeatureRRM: rrm_feature_number_density,
 }
 
 
-class ComponentDensityFn(Protocol):
-    def __call__(self, X_helio: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]: ...
+class NumberDensityFunc(Protocol):
+    """Protocol for a zodiacal components number density function."""
 
+    def __call__(self, X_helio: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
+        """Return the number density of the component at the heliocentric position."""
 
-def construct_density_partials(
-    comps: Sequence[Component],
-    dynamic_params: dict[str, Any],
-) -> tuple[ComponentDensityFn, ...]:
-    """Return density partials for the components.
+
+def get_partial_number_density_func(
+    comps: Mapping[ComponentLabel, ZodiacalComponent],
+) -> dict[ComponentLabel, partial[npt.NDArray[np.float64]]]:
+    """Construct density partials for components.
 
     Return a tuple of the density expressions above which has been prepopulated with
     model and configuration parameters, leaving only the `X_helio` argument to be supplied.
     Raises exception for incorrectly defined components or component density functions.
     """
-    partial_density_funcs: list[ComponentDensityFn] = []
-    for comp in comps:
+    partial_density_funcs: dict[ComponentLabel, partial[npt.NDArray[np.float64]]] = {}
+    for comp_label, comp in comps.items():
         comp_dict = asdict(comp)
         func_params = inspect.signature(DENSITY_FUNCS[type(comp)]).parameters.keys()
         residual_params = [key for key in func_params if key not in comp_dict]
         try:
             residual_params.remove("X_helio")
-        except ValueError as err:
+        except ValueError as err:  # pragma: no cover
             msg = "X_helio must be be the first argument to the density function of a component."
             raise ValueError(msg) from err
 
-        if residual_params:
-            if residual_params - dynamic_params.keys():
-                msg = (
-                    f"Argument(s) {residual_params} required by the density function "
-                    f"{DENSITY_FUNCS[type(comp)]} are not provided by instance variables in "
-                    f"{type(comp)} or by the `computed_parameters` dict."
-                )
-                raise ValueError(msg)
-            comp_dict.update(dynamic_params)
+        if "X_earth" in residual_params:
+            residual_params.remove("X_earth")
+
+        if residual_params:  # pragma: no cover
+            msg = (
+                f"Argument(s) {residual_params} required by the density function "
+                f"{DENSITY_FUNCS[type(comp)]} are not provided by instance variables in "
+                f"{type(comp)} or by the `computed_parameters` dict."
+            )
+            raise ValueError(msg)
 
         # Remove excess intermediate parameters from the component dict.
         comp_params = {key: value for key, value in comp_dict.items() if key in func_params}
         partial_func = partial(DENSITY_FUNCS[type(comp)], **comp_params)
-        partial_density_funcs.append(partial_func)
+        partial_density_funcs[comp_label] = partial_func
 
-    return tuple(partial_density_funcs)
+    return partial_density_funcs
 
 
-def construct_density_partials_comps(
-    comps: Mapping[ComponentLabel, Component],
-    dynamic_params: dict[str, Any],
-) -> dict[ComponentLabel, ComponentDensityFn]:
-    """Construct density partials for components.
+def update_partial_earth_pos(
+    partials: dict[ComponentLabel, partial[npt.NDArray[np.float64]]],
+    earth_pos: npt.NDArray[np.float64],
+) -> dict[ComponentLabel, partial[npt.NDArray[np.float64]]]:
+    """Inplace populate the `X_earth` parameter in the partial density functions."""
+    updated_partials = copy.deepcopy(partials)
+    for partial_func in updated_partials.values():
+        remaining = inspect.signature(partial_func).parameters.keys() - partial_func.keywords.keys()
+        if "X_earth" in remaining:
+            partial_func.keywords["X_earth"] = earth_pos
+    return updated_partials
+
+
+def grid_number_density(
+    x: units.Quantity,
+    y: units.Quantity,
+    z: units.Quantity,
+    obstime: time.Time,
+    name: str = "DIRBE",
+    ephemeris: str = "builtin",
+) -> npt.NDArray[np.float64]:
+    """Return the tabulated densities of the zodiacal components for a given grid.
 
-    Return a tuple of the density expressions above which has been prepopulated with
-    model and configuration parameters, leaving only the `X_helio` argument to be supplied.
-    Raises exception for incorrectly defined components or component density functions.
+    Args:
+        x: Cartesian mesh grid x-coordinates.
+        y: Cartesian mesh grid y-coordinates.
+        z: Cartesian mesh grid z-coordinates.
+        obstime: Time of observation.
+        name: Interplanetary dust model to select. Default is 'DIRBE'.
+        ephemeris: Solar system ephemeris to use. Default is 'builtin'.
+
+    Returns:
+        number_density_grid: The tabulated zodiacal component densities.
 
     """
-    partial_density_funcs: dict[ComponentLabel, ComponentDensityFn] = {}
-    for comp_label, comp in comps.items():
-        comp_dict = asdict(comp)
-        func_params = inspect.signature(DENSITY_FUNCS[type(comp)]).parameters.keys()
-        residual_params = [key for key in func_params if key not in comp_dict]
-        try:
-            residual_params.remove("X_helio")
-        except ValueError as err:
-            msg = "X_helio must be be the first argument to the density function of a component."
-            raise ValueError(msg) from err
+    ipd_model = model_registry.get_model(name)
 
-        if residual_params:
-            if residual_params - dynamic_params.keys():
-                msg = (
-                    f"Argument(s) {residual_params} required by the density function "
-                    f"{DENSITY_FUNCS[type(comp)]} are not provided by instance variables in "
-                    f"{type(comp)} or by the `computed_parameters` dict."
-                )
-                raise ValueError(msg)
-            comp_dict.update(dynamic_params)
+    grid = np.asarray(np.meshgrid(x, y, z))
 
-        # Remove excess intermediate parameters from the component dict.
-        comp_params = {key: value for key, value in comp_dict.items() if key in func_params}
-        partial_func = partial(DENSITY_FUNCS[type(comp)], **comp_params)
-        partial_density_funcs[comp_label] = partial_func
+    earthpos_xyz = get_earthpos_inst(obstime, ephemeris=ephemeris)
+    # Prepare attributes and variables for broadcasting with the grid
 
-    return partial_density_funcs
+    # broadcasting reshapes
+    for comp in ipd_model.comps.values():
+        comp.X_0 = comp.X_0.reshape(3, 1, 1, 1)
+
+    density_partials = get_partial_number_density_func(
+        comps=ipd_model.comps,
+    )
+    density_partials = update_partial_earth_pos(
+        density_partials, earthpos_xyz[:, np.newaxis, np.newaxis, np.newaxis]
+    )
+
+    number_density_grid = np.zeros((len(ipd_model.comps), *grid.shape[1:]))
+    for idx, number_density_callable in enumerate(density_partials.values()):
+        number_density_grid[idx] = number_density_callable(grid)
+
+    # Revert broadcasting reshapes
+    for comp in ipd_model.comps.values():
+        comp.X_0 = comp.X_0.reshape(3, 1)
+
+    return number_density_grid
diff --git a/zodipy/scattering.py b/zodipy/scattering.py
new file mode 100644
index 0000000..21c4311
--- /dev/null
+++ b/zodipy/scattering.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+from typing import TYPE_CHECKING
+
+import numpy as np
+
+if TYPE_CHECKING:
+    import numpy.typing as npt
+
+
+def get_scattering_angle(
+    R_los: float | npt.NDArray[np.float64],
+    R_helio: npt.NDArray[np.float64],
+    X_los: npt.NDArray[np.float64],
+    X_helio: npt.NDArray[np.float64],
+) -> npt.NDArray[np.float64]:
+    """Return the scattering angle between the Sun and a point along the line of sight.
+
+    Args:
+        R_los: Distance from observer to a point along the line of sight.
+        R_helio: Distance from the Sun to a point along the line of sight.
+        X_los: Vector pointing from the observer to a point along the line of sight.
+        X_helio: Heliocentric position of point along the line of sight.
+
+    Returns:
+        Scattering angle.
+
+    """
+    cos_theta = (X_los * X_helio).sum(axis=0) / (R_los * R_helio)
+    cos_theta = np.clip(cos_theta, -1, 1)
+    return np.arccos(-cos_theta)
+
+
+def get_phase_function(
+    Theta: npt.NDArray[np.float64], C1: np.float64, C2: np.float64, C3: np.float64
+) -> npt.NDArray[np.float64]:
+    """Return the phase function.
+
+    Args:
+        Theta: Scattering angle.
+        C1: Phase_function coefficient.
+        C2: Phase_function coefficient.
+        C3: Phase_function coefficient.
+
+    Returns:
+        The Phase funciton.
+
+    """
+    phase_normalization = _get_phase_normalization(C1, C2, C3)
+    return phase_normalization * (C1 + C2 * Theta + np.exp(C3 * Theta))
+
+
+def _get_phase_normalization(C1: np.float64, C2: np.float64, C3: np.float64) -> np.float64:
+    """Return the analyitcal integral for the phase normalization factor N."""
+    int_term1 = 2 * np.pi
+    int_term2 = 2 * C1
+    int_term3 = np.pi * C2
+    int_term4 = (np.exp(C3 * np.pi) + 1) / (C3**2 + 1)
+    return 1 / (int_term1 * (int_term2 + int_term3 + int_term4))
diff --git a/zodipy/source_params.py b/zodipy/source_params.py
index b3a2792..d17cafd 100644
--- a/zodipy/source_params.py
+++ b/zodipy/source_params.py
@@ -1,6 +1,6 @@
 import astropy.units as u
 
-from zodipy._ipd_comps import ComponentLabel
+from zodipy.component import ComponentLabel
 
 T_0_DIRBE = 286
 DELTA_DIRBE = 0.46686259861486573
@@ -220,10 +220,41 @@
     ),
 }
 
-PHASE_FUNCTION_DIRBE = [
-    (-0.94209999, -0.52670002, -0.4312, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
-    (0.1214, 0.18719999, 0.1715, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
-    (-0.1648, -0.59829998, -0.63330001, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
+C1_DIRBE = [
+    -0.94209999,
+    -0.52670002,
+    -0.4312,
+    0.0,
+    0.0,
+    0.0,
+    0.0,
+    0.0,
+    0.0,
+    0.0,
+]
+C2_DIRBE = [
+    0.1214,
+    0.18719999,
+    0.1715,
+    0.0,
+    0.0,
+    0.0,
+    0.0,
+    0.0,
+    0.0,
+    0.0,
+]
+C3_DIRBE = [
+    -0.1648,
+    -0.59829998,
+    -0.63330001,
+    0.0,
+    0.0,
+    0.0,
+    0.0,
+    0.0,
+    0.0,
+    0.0,
 ]
 
 SOLAR_IRRADIANCE_DIRBE = [
diff --git a/zodipy/unpack_model.py b/zodipy/unpack_model.py
new file mode 100644
index 0000000..48c99e8
--- /dev/null
+++ b/zodipy/unpack_model.py
@@ -0,0 +1,155 @@
+from __future__ import annotations
+
+from typing import Any, Callable, TypeVar, Union
+
+import numpy as np
+import numpy.typing as npt
+from astropy import units
+from scipy import integrate, interpolate
+
+from zodipy.component import ComponentLabel
+from zodipy.zodiacal_light_model import RRM, Kelsall, ZodiacalLightModel
+
+CompParamDict = dict[ComponentLabel, dict[str, Any]]
+CommonParamDict = dict[str, Any]
+UnpackedModelDicts = tuple[CompParamDict, CommonParamDict]
+T = TypeVar("T", bound=ZodiacalLightModel)
+UnpackModelCallable = Callable[[units.Quantity, Union[units.Quantity, None], T], UnpackedModelDicts]
+
+
+def interp_and_unpack_kelsall(
+    wavelengths: units.Quantity,
+    weights: units.Quantity | None,
+    model: Kelsall,
+) -> UnpackedModelDicts:
+    """InterplantaryDustModelToDicts implementation for Kelsall model."""
+    model_spectrum = model.spectrum.to(wavelengths.unit, equivalencies=units.spectral())
+
+    comp_params: dict[ComponentLabel, dict[str, Any]] = {}
+    common_params: dict[str, Any] = {
+        "T_0": model.T_0,
+        "delta": model.delta,
+    }
+
+    for comp_label in model.comps:
+        comp_params[comp_label] = {}
+        comp_params[comp_label]["emissivity"] = interp_spectral_param(
+            wavelengths,
+            weights,
+            model_spectrum,
+            spectral_parameter=model.emissivities[comp_label],
+        )
+        if model.albedos is not None:
+            comp_params[comp_label]["albedo"] = interp_spectral_param(
+                wavelengths,
+                weights,
+                model_spectrum,
+                spectral_parameter=model.albedos[comp_label],
+            )
+        else:
+            comp_params[comp_label]["albedo"] = 0
+
+    common_params["C1"] = (
+        interp_spectral_param(
+            wavelengths,
+            weights,
+            model_spectrum,
+            spectral_parameter=model.C1,
+            use_nearest=True,
+        )
+        if model.C1 is not None
+        else 0
+    )
+    common_params["C2"] = (
+        interp_spectral_param(
+            wavelengths,
+            weights,
+            model_spectrum,
+            spectral_parameter=model.C2,
+            use_nearest=True,
+        )
+        if model.C2 is not None
+        else 0
+    )
+
+    common_params["C3"] = (
+        interp_spectral_param(
+            wavelengths,
+            weights,
+            model_spectrum,
+            spectral_parameter=model.C3,
+            use_nearest=True,
+        )
+        if model.C3 is not None
+        else 0
+    )
+    if model.solar_irradiance is None:
+        common_params["solar_irradiance"] = 0
+    else:
+        common_params["solar_irradiance"] = interp_spectral_param(
+            wavelengths,
+            weights,
+            model_spectrum,
+            spectral_parameter=model.solar_irradiance,
+        )
+
+    return comp_params, common_params
+
+
+def interp_and_unpack_rrm(
+    wavelengths: units.Quantity,
+    weights: units.Quantity | None,
+    model: RRM,
+) -> UnpackedModelDicts:
+    """InterplantaryDustModelToDicts implementation for Kelsall model."""
+    model_spectrum = model.spectrum.to(wavelengths.unit, equivalencies=units.spectral())
+
+    comp_params: dict[ComponentLabel, dict[str, Any]] = {}
+    common_params: dict[str, Any] = {}
+
+    for comp_label in model.comps:
+        comp_params[comp_label] = {}
+        comp_params[comp_label]["T_0"] = model.T_0[comp_label]
+        comp_params[comp_label]["delta"] = model.delta[comp_label]
+
+    calibration = interp_spectral_param(
+        wavelengths, weights, model_spectrum, spectral_parameter=model.calibration
+    )
+    calibration_quantity = units.Quantity(calibration, unit=units.MJy / units.AU)
+    common_params["calibration"] = calibration_quantity.to_value(units.Jy / units.cm)
+    return comp_params, common_params
+
+
+def interp_spectral_param(
+    wavelengths: units.Quantity,
+    weights: units.Quantity | None,
+    model_spectrum: units.Quantity,
+    spectral_parameter: npt.ArrayLike,
+    use_nearest: bool = False,
+) -> npt.NDArray:
+    """Interpolate a spectral parameters."""
+    paramameter = np.asarray(spectral_parameter)
+
+    if use_nearest:
+        interped_param = interpolate.interp1d(model_spectrum.value, paramameter, kind="nearest")(
+            wavelengths.value
+        )
+    else:
+        interped_param = np.interp(wavelengths.value, model_spectrum.value, paramameter)
+
+    if weights is not None:
+        return integrate.trapezoid(weights.value * interped_param, x=wavelengths.value)
+    return interped_param
+
+
+interp_and_unpack_func_mapping: dict[type[ZodiacalLightModel], UnpackModelCallable] = {
+    Kelsall: interp_and_unpack_kelsall,
+    RRM: interp_and_unpack_rrm,
+}
+
+
+def get_model_interp_func(
+    model: ZodiacalLightModel,
+) -> UnpackModelCallable:
+    """Get the appropriate parameter unpacker for the model."""
+    return interp_and_unpack_func_mapping[type(model)]
diff --git a/zodipy/zodiacal_light_model.py b/zodipy/zodiacal_light_model.py
new file mode 100644
index 0000000..31f9123
--- /dev/null
+++ b/zodipy/zodiacal_light_model.py
@@ -0,0 +1,142 @@
+from __future__ import annotations
+
+import abc
+from dataclasses import dataclass, field
+from typing import TYPE_CHECKING, Mapping, Sequence
+
+import numpy as np
+from astropy import units
+
+from zodipy.brightness import (
+    BrightnessAtStepCallable,
+    kelsall_brightness_at_step,
+    rrm_brightness_at_step,
+)
+
+if TYPE_CHECKING:
+    from zodipy.component import ComponentLabel, ZodiacalComponent
+
+
+@dataclass(repr=False)
+class ZodiacalLightModel(abc.ABC):
+    """Base class for interplanetary dust models.
+
+    Args:
+        comps: Mapping of component labels to component classes.
+        spectrum: Frequency or wavelength over which the model is valid.
+
+    """
+
+    comps: Mapping[ComponentLabel, ZodiacalComponent]
+    spectrum: units.Quantity
+
+    @property
+    @abc.abstractmethod
+    def brightness_at_step_callable(cls) -> BrightnessAtStepCallable:
+        """Return the callable that computes the brightness at a step."""
+
+    def to_dict(self) -> dict:
+        """Return a dictionary representation of the model."""
+        _dict: dict = {}
+        for key, value in vars(self).items():
+            if key == "comps":
+                _dict[key] = {}
+                for comp_key, comp_value in value.items():
+                    _dict[key][comp_key.value] = {
+                        k: v
+                        for k, v in vars(comp_value).items()
+                        if comp_value.__dataclass_fields__[k].init
+                    }
+            elif isinstance(value, dict):
+                _dict[key] = {k.value: v for k, v in value.items()}
+            else:
+                _dict[key] = value
+
+        return _dict
+
+    @property
+    def ncomps(self) -> int:
+        """Return the number of components in the model."""
+        return len(self.comps)
+
+    def is_valid_at(self, wavelengths: units.Quantity) -> np.bool_:
+        """Check if the model is valid at a given wavelength."""
+        try:
+            wavelengths = wavelengths.to(self.spectrum.unit, equivalencies=units.spectral())
+        except units.UnitConversionError as error:
+            msg = "Input 'x' must have units convertible to Hz or m."
+            raise units.UnitConversionError(msg) from error
+        return np.all((self.spectrum.min() <= wavelengths) & (wavelengths <= self.spectrum.max()))
+
+
+@dataclass(repr=False)
+class Kelsall(ZodiacalLightModel):
+    """Kelsall et al. (1998) model."""
+
+    T_0: float
+    delta: float
+    emissivities: Mapping[ComponentLabel, Sequence[float]]
+    albedos: Mapping[ComponentLabel, Sequence[float]] | None = None
+    solar_irradiance: Sequence[float] | None = None  # In units of MJy/sr
+    C1: Sequence[float] | None = None
+    C2: Sequence[float] | None = None
+    C3: Sequence[float] | None = None
+
+    @property
+    def brightness_at_step_callable(cls) -> BrightnessAtStepCallable:
+        """Kellsall brightness at a step fuction."""
+        return kelsall_brightness_at_step
+
+
+@dataclass(repr=False)
+class RRM(ZodiacalLightModel):
+    """Rowan-Robinson and May (2013) model."""
+
+    T_0: Mapping[ComponentLabel, float]
+    delta: Mapping[ComponentLabel, float]
+    calibration: Sequence[float]
+
+    @property
+    def brightness_at_step_callable(cls) -> BrightnessAtStepCallable:
+        """RRM brightness at a step fuction."""
+        return rrm_brightness_at_step
+
+
+@dataclass
+class ModelRegistry:
+    """Container for registered models."""
+
+    _registry: dict[str, ZodiacalLightModel] = field(init=False, default_factory=dict)
+
+    @property
+    def models(self) -> list[str]:
+        """Return a list of registered models."""
+        return list(self._registry.keys())
+
+    def register_model(
+        self,
+        name: str,
+        model: ZodiacalLightModel,
+    ) -> None:
+        """Register a model with the registry."""
+        if (name := name.lower()) in self._registry:
+            msg = f"a model by the name {name!s} is already registered."
+            raise ValueError(msg)
+        if not isinstance(model, ZodiacalLightModel):  # pragma: no cover
+            msg = "model must be an instance of ZodiacalLightModel."
+            raise TypeError(msg)
+        self._registry[name] = model
+
+    def get_model(self, name: str) -> ZodiacalLightModel:
+        """Return a model from the registry."""
+        if (name := name.lower()) not in self._registry:
+            msg = (
+                f"{name!r} is not a registered Interplanetary Dust model. "
+                f"Avaliable models are: {', '.join(self._registry)}."
+            )
+            raise ValueError(msg)
+
+        return self._registry[name]
+
+
+model_registry = ModelRegistry()
diff --git a/zodipy/zodipy.py b/zodipy/zodipy.py
deleted file mode 100644
index f7ed6b5..0000000
--- a/zodipy/zodipy.py
+++ /dev/null
@@ -1,548 +0,0 @@
-from __future__ import annotations
-
-import multiprocessing
-import platform
-from functools import partial
-from typing import TYPE_CHECKING, Callable, Literal, Sequence
-
-import astropy.units as u
-import healpy as hp
-import numpy as np
-from astropy.coordinates import solar_system_ephemeris
-from scipy.interpolate import interp1d
-
-from zodipy._bandpass import get_bandpass_interpolation_table, validate_and_get_bandpass
-from zodipy._constants import SPECIFIC_INTENSITY_UNITS
-from zodipy._emission import EMISSION_MAPPING
-from zodipy._interpolate_source import SOURCE_PARAMS_MAPPING
-from zodipy._ipd_comps import ComponentLabel
-from zodipy._ipd_dens_funcs import construct_density_partials_comps
-from zodipy._line_of_sight import get_line_of_sight_start_and_stop_distances
-from zodipy._sky_coords import get_obs_and_earth_positions
-from zodipy._unit_vectors import get_unit_vectors_from_ang, get_unit_vectors_from_pixels
-from zodipy._validators import get_validated_ang, get_validated_pix
-from zodipy.model_registry import model_registry
-
-if TYPE_CHECKING:
-    import numpy.typing as npt
-    from astropy.time import Time
-
-    from zodipy._types import FrequencyOrWavelength, ParameterDict, Pixels, SkyAngles
-
-
-PLATFORM = platform.system().lower()
-SYS_PROC_START_METHOD = "fork" if "windows" not in PLATFORM else None
-
-
-class Zodipy:
-    """Interface for simulating zodiacal emission.
-
-    This class provides methods for simulating zodiacal emission given observer pointing
-    either in sky angles or through HEALPix pixels.
-
-    Args:
-        model (str): Name of the interplanetary dust model to use in the simulations.
-            Defaults to DIRBE.
-        gauss_quad_degree (int): Order of the Gaussian-Legendre quadrature used to evaluate
-            the line-of-sight integral in the simulations. Default is 50 points.
-        interp_kind (str): Interpolation kind used to interpolate relevant model parameters.
-            Defaults to 'linear'. For more information on available interpolation methods,
-            please visit the [Scipy documentation](
-            https://docs.scipy.org/doc/scipy/reference/generated/scipy.interpolate.interp1d.html).
-        extrapolate (bool): If `True` all spectral quantities in the selected model are
-            extrapolated to the requested frequencies or wavelengths. If `False`, an
-            exception is raised on requested frequencies/wavelengths outside of the
-            valid model range. Default is `False`.
-        ephemeris (str): Ephemeris used to compute the positions of the observer and the
-            Earth. Defaults to 'de432s', which requires downloading (and caching) a ~10MB
-            file. For more information on available ephemeridis, please visit the [Astropy
-            documentation](https://docs.astropy.org/en/stable/coordinates/solarsystem.html)
-        solar_cut (u.Quantity[u.deg]): Cutoff angle from the sun in degrees. The emission
-            for all the pointing with angular distance between the sun smaller than
-            `solar_cutoff` are masked. Defaults to `None`.
-        solar_cut_fill_value (float): Fill value for pixels masked with `solar_cut`.
-            Defaults to `np.nan`.
-        n_proc (int): Number of cores to use. If `n_proc` is greater than 1, the line-of-sight
-            integrals are parallelized using the `multiprocessing` module. Defaults to 1.
-
-    """
-
-    def __init__(
-        self,
-        model: str = "dirbe",
-        gauss_quad_degree: int = 50,
-        extrapolate: bool = False,
-        interp_kind: str = "linear",
-        ephemeris: str = "de432s",
-        solar_cut: u.Quantity[u.deg] | None = None,
-        solar_cut_fill_value: float = np.nan,
-        n_proc: int = 1,
-    ) -> None:
-        self.model = model
-        self.gauss_quad_degree = gauss_quad_degree
-        self.extrapolate = extrapolate
-        self.interp_kind = interp_kind
-        self.ephemeris = ephemeris
-        self.solar_cut = solar_cut.to(u.rad) if solar_cut is not None else solar_cut
-        self.solar_cut_fill_value = solar_cut_fill_value
-        self.n_proc = n_proc
-
-        self._interpolator = partial(
-            interp1d,
-            kind=self.interp_kind,
-            fill_value="extrapolate" if self.extrapolate else np.nan,
-        )
-        self._ipd_model = model_registry.get_model(model)
-        self._gauss_points_and_weights = np.polynomial.legendre.leggauss(gauss_quad_degree)
-
-    @property
-    def supported_observers(self) -> list[str]:
-        """Return a list of available observers given an ephemeris."""
-        return [*list(solar_system_ephemeris.bodies), "semb-l2"]
-
-    def get_parameters(self) -> ParameterDict:
-        """Return a dictionary containing the interplanetary dust model parameters."""
-        return self._ipd_model.to_dict()
-
-    def update_parameters(self, parameters: ParameterDict) -> None:
-        """Update the interplanetary dust model parameters.
-
-        Args:
-            parameters: Dictionary of parameters to update. The keys must be the names
-                of the parameters as defined in the model. To get the parameters dict
-                of an existing model, use `Zodipy("dirbe").get_parameters()`.
-
-        """
-        _dict = parameters.copy()
-        _dict["comps"] = {}
-        for key, value in parameters.items():
-            if key == "comps":
-                for comp_key, comp_value in value.items():
-                    _dict["comps"][ComponentLabel(comp_key)] = type(
-                        self._ipd_model.comps[ComponentLabel(comp_key)]
-                    )(**comp_value)
-            elif isinstance(value, dict):
-                _dict[key] = {ComponentLabel(k): v for k, v in value.items()}
-
-        self._ipd_model = self._ipd_model.__class__(**_dict)
-
-    def get_emission_ang(
-        self,
-        freq: FrequencyOrWavelength,
-        theta: SkyAngles,
-        phi: SkyAngles,
-        obs_time: Time,
-        obs: str = "earth",
-        obs_pos: u.Quantity[u.AU] | None = None,
-        weights: Sequence[float] | npt.NDArray[np.floating] | None = None,
-        lonlat: bool = False,
-        return_comps: bool = False,
-        coord_in: Literal["E", "G", "C"] = "E",
-    ) -> u.Quantity[u.MJy / u.sr]:
-        """Return the simulated zodiacal emission given angles on the sky.
-
-        The pointing, for which to compute the emission, is specified in form of angles on
-        the sky given by `theta` and `phi`.
-
-        Args:
-            freq: Delta frequency/wavelength or a sequence of frequencies corresponding to
-                a bandpass over which to evaluate the zodiacal emission. The frequencies
-                must be strictly increasing.
-            theta: Angular co-latitude coordinate of a point, or a sequence of points, on
-                the celestial sphere. Must be in the range [0, π] rad. Units must be either
-                radians or degrees.
-            phi: Angular longitude coordinate of a point, or a sequence of points, on the
-                celestial sphere. Must be in the range [0, 2π] rad. Units must be either
-                radians or degrees.
-            obs_time: Time of observation.
-            obs: Name of the Solar System observer. A list of all support observers (for a
-                given ephemeridis) is specified in `supported_observers` attribute of the
-                `Zodipy` instance. Defaults to 'earth'.
-            obs_pos: The heliocentric ecliptic cartesian position of the observer in AU.
-                Overrides the `obs` argument. Default is None.
-            weights: Bandpass weights corresponding the the frequencies in `freq`. The weights
-                are assumed to be given in spectral radiance units (Jy/sr).
-            lonlat: If True, input angles (`theta`, `phi`) are assumed to be longitude and
-                latitude, otherwise, they are co-latitude and longitude.
-            return_comps: If True, the emission is returned component-wise. Defaults to False.
-            coord_in: Coordinate frame of the input pointing. Assumes 'E' (ecliptic
-                coordinates) by default.
-
-        Returns:
-            emission: Simulated zodiacal emission in units of 'MJy/sr'.
-
-        """
-        theta, phi = get_validated_ang(theta=theta, phi=phi, lonlat=lonlat)
-
-        unique_angles, indicies = np.unique(np.asarray([theta, phi]), return_inverse=True, axis=1)
-        unit_vectors = get_unit_vectors_from_ang(
-            coord_in=coord_in,
-            theta=unique_angles[0],
-            phi=unique_angles[1],
-            lonlat=lonlat,
-        )
-
-        return self._compute_emission(
-            freq=freq,
-            weights=weights,
-            obs=obs,
-            obs_time=obs_time,
-            obs_pos=obs_pos,
-            unit_vectors=unit_vectors,
-            indicies=indicies,
-            return_comps=return_comps,
-        )
-
-    def get_emission_pix(
-        self,
-        freq: FrequencyOrWavelength,
-        pixels: Pixels,
-        nside: int,
-        obs_time: Time,
-        obs: str = "earth",
-        obs_pos: u.Quantity[u.AU] | None = None,
-        weights: Sequence[float] | npt.NDArray[np.floating] | None = None,
-        return_comps: bool = False,
-        coord_in: Literal["E", "G", "C"] = "E",
-    ) -> u.Quantity[u.MJy / u.sr]:
-        """Return the simulated zodiacal emission given pixel numbers.
-
-        The pixel numbers represent the pixel indicies on a HEALPix grid with resolution
-        given by `nside`.
-
-        Args:
-            freq: Delta frequency/wavelength or a sequence of frequencies corresponding to
-                a bandpass over which to evaluate the zodiacal emission. The frequencies
-                must be strictly increasing.
-            pixels: HEALPix pixel indicies representing points on the celestial sphere.
-            nside: HEALPix resolution parameter of the pixels and the binned map.
-            obs_time: Time of observation.
-            obs: Name of the Solar System observer. A list of all support observers (for a
-                given ephemeridis) is specified in `supported_observers` attribute of the
-                `Zodipy` instance. Defaults to 'earth'.
-            obs_pos: The heliocentric ecliptic cartesian position of the observer in AU.
-                Overrides the `obs` argument. Default is None.
-            weights: Bandpass weights corresponding the the frequencies in `freq`. The weights
-                are assumed to be given in spectral radiance units (Jy/sr).
-            return_comps: If True, the emission is returned component-wise. Defaults to False.
-            coord_in: Coordinate frame of the input pointing. Assumes 'E' (ecliptic
-                coordinates) by default.
-
-        Returns:
-            emission: Simulated zodiacal emission in units of 'MJy/sr'.
-
-        """
-        pixels = get_validated_pix(pixels=pixels, nside=nside)
-
-        unique_pixels, indicies = np.unique(pixels, return_inverse=True)
-        unit_vectors = get_unit_vectors_from_pixels(
-            coord_in=coord_in,
-            pixels=unique_pixels,
-            nside=nside,
-        )
-
-        return self._compute_emission(
-            freq=freq,
-            weights=weights,
-            obs=obs,
-            obs_time=obs_time,
-            obs_pos=obs_pos,
-            unit_vectors=unit_vectors,
-            indicies=indicies,
-            pixels=unique_pixels,
-            nside=nside,
-            return_comps=return_comps,
-        )
-
-    def get_binned_emission_ang(
-        self,
-        freq: FrequencyOrWavelength,
-        theta: SkyAngles,
-        phi: SkyAngles,
-        nside: int,
-        obs_time: Time,
-        obs: str = "earth",
-        obs_pos: u.Quantity[u.AU] | None = None,
-        weights: Sequence[float] | npt.NDArray[np.floating] | None = None,
-        lonlat: bool = False,
-        return_comps: bool = False,
-        coord_in: Literal["E", "G", "C"] = "E",
-    ) -> u.Quantity[u.MJy / u.sr]:
-        """Return the simulated binned zodiacal emission given angles on the sky.
-
-        The pointing, for which to compute the emission, is specified in form of angles on
-        the sky given by `theta` and `phi`. The emission is binned to a HEALPix map with
-        resolution given by `nside`.
-
-        Args:
-            freq: Delta frequency/wavelength or a sequence of frequencies corresponding to
-                a bandpass over which to evaluate the zodiacal emission. The frequencies
-                must be strictly increasing.
-            theta: Angular co-latitude coordinate of a point, or a sequence of points, on
-                the celestial sphere. Must be in the range [0, π] rad. Units must be either
-                radians or degrees.
-            phi: Angular longitude coordinate of a point, or a sequence of points, on the
-                celestial sphere. Must be in the range [0, 2π] rad. Units must be either
-                radians or degrees.
-            nside: HEALPix resolution parameter of the binned map.
-            obs_time: Time of observation.
-            obs: Name of the Solar System observer. A list of all support observers (for a
-                given ephemeridis) is specified in `supported_observers` attribute of the
-                `Zodipy` instance. Defaults to 'earth'.
-            obs_pos: The heliocentric ecliptic cartesian position of the observer in AU.
-                Overrides the `obs` argument. Default is None.
-            weights: Bandpass weights corresponding the the frequencies in `freq`. The weights
-                are assumed to be given in spectral radiance units (Jy/sr).
-            lonlat: If True, input angles `theta`, `phi` are assumed to be longitude and
-                latitude, otherwise, they are co-latitude and longitude.
-            return_comps: If True, the emission is returned component-wise. Defaults to False.
-            coord_in: Coordinate frame of the input pointing. Assumes 'E' (ecliptic
-                coordinates) by default.
-
-        Returns:
-            emission: Simulated zodiacal emission in units of 'MJy/sr'.
-
-        """
-        theta, phi = get_validated_ang(theta=theta, phi=phi, lonlat=lonlat)
-
-        unique_angles, counts = np.unique(np.asarray([theta, phi]), return_counts=True, axis=1)
-        unique_pixels = hp.ang2pix(nside, *unique_angles, lonlat=lonlat)
-        unit_vectors = get_unit_vectors_from_ang(
-            coord_in=coord_in,
-            theta=unique_angles[0],
-            phi=unique_angles[1],
-            lonlat=lonlat,
-        )
-
-        return self._compute_emission(
-            freq=freq,
-            weights=weights,
-            obs=obs,
-            obs_time=obs_time,
-            obs_pos=obs_pos,
-            unit_vectors=unit_vectors,
-            indicies=counts,
-            binned=True,
-            pixels=unique_pixels,
-            nside=nside,
-            return_comps=return_comps,
-        )
-
-    def get_binned_emission_pix(
-        self,
-        freq: FrequencyOrWavelength,
-        pixels: Pixels,
-        nside: int,
-        obs_time: Time,
-        obs: str = "earth",
-        obs_pos: u.Quantity[u.AU] | None = None,
-        weights: Sequence[float] | npt.NDArray[np.floating] | None = None,
-        return_comps: bool = False,
-        coord_in: Literal["E", "G", "C"] = "E",
-    ) -> u.Quantity[u.MJy / u.sr]:
-        """Return the simulated binned zodiacal Emission given pixel numbers.
-
-        The pixel numbers represent the pixel indicies on a HEALPix grid with resolution
-        given by `nside`. The emission is binned to a HEALPix map with resolution given by
-        `nside`.
-
-        Args:
-            freq: Delta frequency/wavelength or a sequence of frequencies corresponding to
-                a bandpass over which to evaluate the zodiacal emission. The frequencies
-                must be strictly increasing.
-            pixels: HEALPix pixel indicies representing points on the celestial sphere.
-            nside: HEALPix resolution parameter of the pixels and the binned map.
-            obs_time: Time of observation.
-            obs: Name of the Solar System observer. A list of all support observers (for a
-                given ephemeridis) is specified in `supported_observers` attribute of the
-                `Zodipy` instance. Defaults to 'earth'.
-            obs_pos: The heliocentric ecliptic cartesian position of the observer in AU.
-                Overrides the `obs` argument. Default is None.
-            weights: Bandpass weights corresponding the the frequencies in `freq`. The weights
-                are assumed to be given in spectral radiance units (Jy/sr).
-            return_comps: If True, the emission is returned component-wise. Defaults to False.
-            coord_in: Coordinate frame of the input pointing. Assumes 'E' (ecliptic
-                coordinates) by default.
-
-        Returns:
-            emission: Simulated zodiacal emission in units of 'MJy/sr'.
-
-        """
-        pixels = get_validated_pix(pixels=pixels, nside=nside)
-
-        unique_pixels, counts = np.unique(pixels, return_counts=True)
-        unit_vectors = get_unit_vectors_from_pixels(
-            coord_in=coord_in,
-            pixels=unique_pixels,
-            nside=nside,
-        )
-
-        return self._compute_emission(
-            freq=freq,
-            weights=weights,
-            obs=obs,
-            obs_time=obs_time,
-            obs_pos=obs_pos,
-            unit_vectors=unit_vectors,
-            indicies=counts,
-            binned=True,
-            pixels=unique_pixels,
-            nside=nside,
-            return_comps=return_comps,
-        )
-
-    def _compute_emission(
-        self,
-        freq: FrequencyOrWavelength,
-        weights: Sequence[float] | npt.NDArray[np.floating] | None,
-        obs: str,
-        obs_time: Time,
-        unit_vectors: npt.NDArray[np.float64],
-        indicies: npt.NDArray[np.int64],
-        binned: bool = False,
-        obs_pos: u.Quantity[u.AU] | None = None,
-        pixels: npt.NDArray[np.int64] | None = None,
-        nside: int | None = None,
-        return_comps: bool = False,
-    ) -> u.Quantity[u.MJy / u.sr]:
-        """Compute the component-wise zodiacal emission."""
-        bandpass = validate_and_get_bandpass(
-            freq=freq,
-            weights=weights,
-            model=self._ipd_model,
-            extrapolate=self.extrapolate,
-        )
-
-        # Get model parameters, some of which have been interpolated to the given
-        # frequency or bandpass.
-        source_parameters = SOURCE_PARAMS_MAPPING[type(self._ipd_model)](
-            bandpass, self._ipd_model, self._interpolator
-        )
-
-        observer_position, earth_position = get_obs_and_earth_positions(
-            obs=obs, obs_time=obs_time, obs_pos=obs_pos
-        )
-
-        # Get the integration limits for each zodiacal component (which may be
-        # different or the same depending on the model) along all line of sights.
-        start, stop = get_line_of_sight_start_and_stop_distances(
-            components=self._ipd_model.comps.keys(),
-            unit_vectors=unit_vectors,
-            obs_pos=observer_position,
-        )
-
-        density_partials = construct_density_partials_comps(
-            comps=self._ipd_model.comps,
-            dynamic_params={"X_earth": earth_position},
-        )
-
-        # Make table of pre-computed bandpass integrated blackbody emission.
-        bandpass_interpolatation_table = get_bandpass_interpolation_table(bandpass)
-
-        common_integrand = partial(
-            EMISSION_MAPPING[type(self._ipd_model)],
-            X_obs=observer_position,
-            bp_interpolation_table=bandpass_interpolatation_table,
-            **source_parameters["common"],
-        )
-
-        if self.n_proc > 1:
-            unit_vector_chunks = np.array_split(unit_vectors, self.n_proc, axis=-1)
-            integrated_comp_emission = np.zeros((len(self._ipd_model.comps), unit_vectors.shape[1]))
-            with multiprocessing.get_context(SYS_PROC_START_METHOD).Pool(
-                processes=self.n_proc
-            ) as pool:
-                for idx, comp_label in enumerate(self._ipd_model.comps.keys()):
-                    stop_chunks = np.array_split(stop[comp_label], self.n_proc, axis=-1)
-                    if start[comp_label].size == 1:
-                        start_chunks = [start[comp_label]] * self.n_proc
-                    else:
-                        start_chunks = np.array_split(start[comp_label], self.n_proc, axis=-1)
-                    comp_integrands = [
-                        partial(
-                            common_integrand,
-                            u_los=np.expand_dims(unit_vectors, axis=-1),
-                            start=np.expand_dims(start, axis=-1),
-                            stop=np.expand_dims(stop, axis=-1),
-                            get_density_function=density_partials[comp_label],
-                            **source_parameters[comp_label],
-                        )
-                        for unit_vectors, start, stop in zip(
-                            unit_vector_chunks, start_chunks, stop_chunks
-                        )
-                    ]
-
-                    proc_chunks = [
-                        pool.apply_async(
-                            _integrate_gauss_quad,
-                            args=(comp_integrand, *self._gauss_points_and_weights),
-                        )
-                        for comp_integrand in comp_integrands
-                    ]
-
-                    integrated_comp_emission[idx] += (
-                        np.concatenate([result.get() for result in proc_chunks])
-                        * 0.5
-                        * (stop[comp_label] - start[comp_label])
-                    )
-
-        else:
-            integrated_comp_emission = np.zeros((len(self._ipd_model.comps), unit_vectors.shape[1]))
-            unit_vectors_expanded = np.expand_dims(unit_vectors, axis=-1)
-
-            for idx, comp_label in enumerate(self._ipd_model.comps.keys()):
-                comp_integrand = partial(
-                    common_integrand,
-                    u_los=unit_vectors_expanded,
-                    start=np.expand_dims(start[comp_label], axis=-1),
-                    stop=np.expand_dims(stop[comp_label], axis=-1),
-                    get_density_function=density_partials[comp_label],
-                    **source_parameters[comp_label],
-                )
-
-                integrated_comp_emission[idx] = (
-                    _integrate_gauss_quad(comp_integrand, *self._gauss_points_and_weights)
-                    * 0.5
-                    * (stop[comp_label] - start[comp_label])
-                )
-
-        emission = np.zeros(
-            (
-                len(self._ipd_model.comps),
-                hp.nside2npix(nside) if binned else indicies.size,
-            )
-        )
-        if binned:
-            emission[:, pixels] = integrated_comp_emission
-        else:
-            emission = integrated_comp_emission[:, indicies]
-
-        if self.solar_cut is not None:
-            ang_dist = hp.rotator.angdist(-observer_position.flatten(), unit_vectors)
-            solar_mask = ang_dist < self.solar_cut.value
-            if binned and pixels is not None:
-                emission[:, pixels[solar_mask]] = self.solar_cut_fill_value
-            else:
-                emission[:, solar_mask[indicies]] = self.solar_cut_fill_value
-
-        emission = (emission << SPECIFIC_INTENSITY_UNITS).to(u.MJy / u.sr)
-
-        return emission if return_comps else emission.sum(axis=0)
-
-    def __repr__(self) -> str:
-        repr_str = f"{self.__class__.__name__}("
-        for attribute_name, attribute in self.__dict__.items():
-            if attribute_name.startswith("_"):
-                continue
-            repr_str += f"{attribute_name}={attribute!r}, "
-
-        return repr_str[:-2] + ")"
-
-
-def _integrate_gauss_quad(
-    fn: Callable[[float], npt.NDArray[np.float64]],
-    points: npt.NDArray[np.float64],
-    weights: npt.NDArray[np.float64],
-) -> npt.NDArray[np.float64]:
-    """Integrate a function using Gauss-Legendre quadrature."""
-    return np.squeeze(sum(fn(x) * w for x, w in zip(points, weights)))


Test patch:
diff --git a/tests/_strategies.py b/tests/_strategies.py
deleted file mode 100644
index 52a9c14..0000000
--- a/tests/_strategies.py
+++ /dev/null
@@ -1,212 +0,0 @@
-from __future__ import annotations
-
-import datetime
-from functools import partial
-from math import log2
-from typing import Any, Callable, Sequence
-
-import astropy.units as u
-import healpy as hp
-import numpy as np
-import numpy.typing as npt
-from astropy.coordinates import HeliocentricMeanEcliptic, get_body
-from astropy.time import Time
-from hypothesis.extra.numpy import arrays
-from hypothesis.strategies import (
-    DrawFn,
-    SearchStrategy,
-    booleans,
-    builds,
-    composite,
-    datetimes,
-    floats,
-    integers,
-    lists,
-    one_of,
-    sampled_from,
-)
-
-import zodipy
-from zodipy._line_of_sight import COMPONENT_CUTOFFS
-
-MIN_FREQ = u.Quantity(10, u.GHz)
-MAX_FREQ = u.Quantity(0.1, u.micron).to(u.GHz, equivalencies=u.spectral())
-N_FREQS = 1000
-FREQ_LOG_RANGE = np.geomspace(np.log(MIN_FREQ.value), np.log(MAX_FREQ.value), N_FREQS).tolist()
-
-MIN_DATE = datetime.datetime(year=1900, month=1, day=1)
-MAX_DATE = datetime.datetime(year=2100, month=1, day=1)
-
-MIN_NSIDE = 8
-MAX_NSIDE = 1024
-MIN_NSIDE_EXP = int(log2(MIN_NSIDE))
-MAX_NSIDE_EXP = int(log2(MAX_NSIDE))
-
-MAX_PIXELS_LEN = 10000
-MAX_ANGELS_LEN = 10000
-
-AVAILABLE_MODELS = zodipy.model_registry.models
-
-
-@composite
-def quantities(
-    draw: Callable[[SearchStrategy[float]], u.Quantity],
-    min_value: float,
-    max_value: float,
-    unit: u.Unit,
-) -> u.Quantity:
-    return draw(
-        floats(
-            min_value=min_value,
-            max_value=max_value,
-            allow_infinity=False,
-            allow_nan=False,
-        ).map(partial(u.Quantity, unit=unit))
-    )
-
-
-@composite
-def time(draw: DrawFn) -> Time:
-    return draw(datetimes(min_value=MIN_DATE, max_value=MAX_DATE).map(Time))
-
-
-@composite
-def nside(draw: Callable[[SearchStrategy[int]], int]) -> int:
-    return draw(integers(min_value=MIN_NSIDE_EXP, max_value=MAX_NSIDE_EXP).map(partial(pow, 2)))
-
-
-@composite
-def pixels(draw: DrawFn, nside: int) -> int | list[int] | npt.NDArray[np.integer]:
-    npix = hp.nside2npix(nside)
-    pixel_strategy = integers(min_value=0, max_value=npix - 1)
-
-    shape = draw(integers(min_value=1, max_value=npix - 1))
-
-    list_stategy = lists(pixel_strategy, min_size=1)
-    array_strategy = arrays(dtype=int, shape=shape, elements=pixel_strategy)
-
-    return draw(one_of(pixel_strategy, list_stategy, array_strategy))
-
-
-@composite
-def angles(draw: DrawFn, lonlat: bool = False) -> tuple[u.Quantity[u.deg], u.Quantity[u.deg]]:
-    if lonlat:
-        theta_strategy = floats(min_value=0, max_value=360)
-        phi_strategy = floats(min_value=-90, max_value=90)
-    else:
-        theta_strategy = floats(min_value=0, max_value=180)
-        phi_strategy = floats(min_value=0, max_value=360)
-
-    shape = draw(integers(min_value=1, max_value=MAX_ANGELS_LEN))
-
-    theta_array_strategy = arrays(dtype=float, shape=shape, elements=theta_strategy).map(
-        partial(u.Quantity, unit=u.deg)
-    )
-    phi_array_strategy = arrays(dtype=float, shape=shape, elements=phi_strategy).map(
-        partial(u.Quantity, unit=u.deg)
-    )
-
-    return draw(theta_array_strategy), draw(phi_array_strategy)
-
-
-@composite
-def freq(draw: DrawFn, model: zodipy.Zodipy) -> u.Quantity[u.GHz] | u.Quantity[u.micron]:
-    if model.extrapolate:
-        return draw(sampled_from(FREQ_LOG_RANGE).map(np.exp).map(partial(u.Quantity, unit=u.GHz)))
-
-    min_freq = model._ipd_model.spectrum[0]
-    max_freq = model._ipd_model.spectrum[-1]
-    freq_range = np.geomspace(np.log(min_freq.value), np.log(max_freq.value), N_FREQS)
-    freq_strategy = (
-        sampled_from(freq_range.tolist())
-        .map(np.exp)
-        .map(partial(u.Quantity, unit=model._ipd_model.spectrum.unit))
-    )
-
-    return np.clip(draw(freq_strategy), min_freq, max_freq)
-
-
-@composite
-def random_freq(draw: DrawFn, unit: u.Unit | None = None, bandpass: bool = False) -> u.Quantity:
-    random_freq = draw(
-        sampled_from(FREQ_LOG_RANGE).map(np.exp).map(partial(u.Quantity, unit=u.GHz))
-    )
-    if unit is not None:
-        random_freq = random_freq.to(unit, u.spectral())
-    else:
-        micron = draw(booleans())
-        if micron:
-            random_freq = random_freq.to(u.micron, u.spectral())
-
-    if bandpass:
-        shape = draw(integers(min_value=2, max_value=100))
-        sigma = draw(floats(min_value=0.1, max_value=0.3))
-        return np.linspace(
-            random_freq - random_freq * sigma, random_freq + random_freq * sigma, shape
-        )
-
-    return random_freq
-
-
-@composite
-def weights(
-    draw: DrawFn, freqs: u.Quantity[u.GHz] | u.Quantity[u.micron]
-) -> list[float] | npt.NDArray[np.float64]:
-    def normalize_array(
-        array: Sequence[float] | npt.NDArray[np.float64],
-        freqs: u.Quantity[u.GHz] | u.Quantity[u.micron],
-    ) -> u.Quantity[u.MJy / u.sr]:
-        return (array / np.trapz(array, freqs)).value
-
-    weights_strategy = floats(min_value=1, max_value=100)
-    list_stategy = lists(weights_strategy, min_size=freqs.size, max_size=freqs.size)
-    array_strategy = arrays(dtype=float, shape=freqs.size, elements=weights_strategy)
-
-    return draw(one_of(list_stategy, array_strategy).map(partial(normalize_array, freqs=freqs)))
-
-
-@composite
-def obs(draw: DrawFn, model: zodipy.Zodipy, obs_time: Time) -> str:
-    def get_obs_dist(obs: str, obs_time: Time) -> u.Quantity[u.AU]:
-        if obs == "semb-l2":
-            obs_pos = (
-                get_body("earth", obs_time).transform_to(HeliocentricMeanEcliptic).cartesian.xyz
-            )
-            obs_pos += 0.01 * u.AU
-        else:
-            obs_pos = get_body(obs, obs_time).transform_to(HeliocentricMeanEcliptic).cartesian.xyz
-        return u.Quantity(np.linalg.norm(obs_pos.value), u.AU)
-
-    los_dist_cut = min(
-        [COMPONENT_CUTOFFS[comp][1] for comp in model._ipd_model.comps],
-    )
-    if isinstance(los_dist_cut, dict):
-        los_dist_cut = min(list(los_dist_cut.values()))
-
-    obs_list = model.supported_observers
-    return draw(
-        sampled_from(obs_list).filter(lambda obs: los_dist_cut > get_obs_dist(obs, obs_time).value)
-    )
-
-
-@composite
-def any_obs(draw: DrawFn, model: zodipy.Zodipy) -> str:
-    return draw(sampled_from(model.supported_observers))
-
-
-MODEL_STRATEGY_MAPPINGS: dict[str, SearchStrategy[Any]] = {
-    "model": sampled_from(AVAILABLE_MODELS),
-    "gauss_quad_degree": integers(min_value=1, max_value=200),
-    "extrapolate": booleans(),
-    "solar_cut": quantities(min_value=0, max_value=360, unit=u.deg),
-}
-
-
-@composite
-def model(draw: DrawFn, **static_params: dict[str, Any]) -> zodipy.Zodipy:
-    strategies = MODEL_STRATEGY_MAPPINGS.copy()
-    for key in static_params:
-        if key in strategies:
-            strategies.pop(key)
-
-    return draw(builds(partial(zodipy.Zodipy, **static_params), **strategies))
diff --git a/tests/_tabulated_dirbe.py b/tests/dirbe_tabulated.py
similarity index 58%
rename from tests/_tabulated_dirbe.py
rename to tests/dirbe_tabulated.py
index 6a0a5b7..e177a6a 100644
--- a/tests/_tabulated_dirbe.py
+++ b/tests/dirbe_tabulated.py
@@ -1,3 +1,6 @@
+from astropy import time, units
+
+DIRBE_START_DAY = time.Time("1990-01-01")
 # Days from 1990-01-01 where 1 = 1 Jan, 1990
 DAYS = [162, 61, 244, 163, 225, 354, 144, 360, 289, 204]
 
@@ -151,3 +154,175 @@
         0.24349573391915608,
     ],
 }
+
+DIRBE_25um_BANDPASS: tuple[units.Quantity, list[float]] = (
+    [
+        15.543,
+        15.664,
+        15.787,
+        15.910,
+        16.034,
+        16.160,
+        16.286,
+        16.413,
+        16.541,
+        16.670,
+        16.801,
+        16.932,
+        17.064,
+        17.198,
+        17.332,
+        17.467,
+        17.604,
+        17.741,
+        17.880,
+        18.019,
+        18.160,
+        18.302,
+        18.445,
+        18.589,
+        18.734,
+        18.881,
+        19.028,
+        19.177,
+        19.327,
+        19.477,
+        19.630,
+        19.783,
+        19.938,
+        20.093,
+        20.250,
+        20.408,
+        20.568,
+        20.728,
+        20.890,
+        21.054,
+        21.218,
+        21.384,
+        21.551,
+        21.719,
+        21.889,
+        22.060,
+        22.232,
+        22.406,
+        22.581,
+        22.757,
+        22.935,
+        23.114,
+        23.295,
+        23.477,
+        23.660,
+        23.845,
+        24.031,
+        24.219,
+        24.408,
+        24.599,
+        24.791,
+        24.984,
+        25.180,
+        25.376,
+        25.574,
+        25.774,
+        25.976,
+        26.178,
+        26.383,
+        26.589,
+        26.797,
+        27.006,
+        27.217,
+        27.430,
+        27.644,
+        27.860,
+        28.077,
+        28.297,
+        28.518,
+        28.741,
+        28.965,
+        29.191,
+    ]
+    * units.micron,
+    [
+        0.01,
+        0.02,
+        0.04,
+        0.05,
+        0.08,
+        0.16,
+        0.21,
+        0.34,
+        0.45,
+        0.55,
+        0.67,
+        0.79,
+        0.85,
+        0.89,
+        0.92,
+        0.93,
+        0.91,
+        0.88,
+        0.84,
+        0.82,
+        0.88,
+        0.94,
+        1.00,
+        0.99,
+        0.93,
+        0.87,
+        0.81,
+        0.68,
+        0.56,
+        0.46,
+        0.51,
+        0.60,
+        0.69,
+        0.70,
+        0.64,
+        0.60,
+        0.58,
+        0.63,
+        0.66,
+        0.70,
+        0.75,
+        0.80,
+        0.80,
+        0.69,
+        0.59,
+        0.53,
+        0.57,
+        0.61,
+        0.66,
+        0.74,
+        0.83,
+        0.83,
+        0.78,
+        0.73,
+        0.70,
+        0.68,
+        0.67,
+        0.79,
+        0.90,
+        0.95,
+        0.94,
+        0.93,
+        0.77,
+        0.62,
+        0.48,
+        0.35,
+        0.24,
+        0.18,
+        0.14,
+        0.09,
+        0.08,
+        0.07,
+        0.08,
+        0.08,
+        0.07,
+        0.05,
+        0.04,
+        0.03,
+        0.02,
+        0.02,
+        0.01,
+        0.01,
+    ],
+)
diff --git a/tests/strategies.py b/tests/strategies.py
new file mode 100644
index 0000000..2f131e2
--- /dev/null
+++ b/tests/strategies.py
@@ -0,0 +1,148 @@
+from __future__ import annotations
+
+import datetime
+
+import numpy as np
+import numpy.typing as npt
+from astropy import time, units
+from astropy.coordinates import SkyCoord
+from hypothesis import strategies as st
+from hypothesis.extra.numpy import arrays
+
+from zodipy import Model
+from zodipy.model_registry import model_registry
+from zodipy.zodiacal_light_model import ZodiacalLightModel
+
+MIN_DATE = time.Time(datetime.datetime(year=1960, month=1, day=1))
+MAX_DATE = time.Time(datetime.datetime(year=2080, month=1, day=1))
+TEST_SAMPRATE = 1 / 86400  # 1 sec
+
+
+@st.composite
+def bandpass(draw: st.DrawFn, model: ZodiacalLightModel) -> tuple[units.Quantity, np.ndarray]:
+    """Generate randomized bandpass."""
+    min_freq = model.spectrum.min()
+    max_freq = model.spectrum.max()
+    scale = draw(st.floats(min_value=0.1, max_value=0.5))
+    full_width = max_freq - min_freq
+    width = full_width * scale
+    size = draw(st.integers(min_value=10, max_value=100))
+    x = (
+        np.linspace(0, width, size)
+        + min_freq
+        + (full_width - width) * draw(st.floats(min_value=0, max_value=1))
+    )
+    weights = draw(arrays(dtype=float, shape=size, elements=st.floats(min_value=0.1, max_value=1)))
+    return x, weights
+
+
+@st.composite
+def models(draw: st.DrawFn) -> Model:
+    """Generate randomized model."""
+    model_name = draw(st.sampled_from(model_registry.models))
+    zodi_model = model_registry.get_model(model_name)
+    has_bandpass = draw(st.booleans())
+    if has_bandpass:
+        x, weights = draw(bandpass(zodi_model))
+
+    else:
+        x = (
+            draw(
+                st.floats(
+                    min_value=zodi_model.spectrum.min().value,
+                    max_value=zodi_model.spectrum.max().value,
+                )
+            )
+        ) * zodi_model.spectrum.unit
+        weights = None
+    return Model(x, weights=weights, name=model_name)
+
+
+@st.composite
+def obstime_inst(draw: st.DrawFn) -> time.Time:
+    """Return a strategy for generating astropy Time objects."""
+    t0 = draw(st.integers(min_value=MIN_DATE.mjd, max_value=MAX_DATE.mjd))
+    return time.Time(t0, format="mjd")
+
+
+@st.composite
+def obstime_tod(draw: st.DrawFn, size: int) -> time.Time:
+    """Return a strategy for generating astropy Time objects."""
+    t0 = draw(st.integers(min_value=MIN_DATE.mjd, max_value=MAX_DATE.mjd))
+    return time.Time(np.linspace(t0, t0 + TEST_SAMPRATE * size, size), format="mjd")
+
+
+@st.composite
+def get_obspos_vec(draw: st.DrawFn, size: int) -> units.Quantity:
+    """Return a strategy for generating a heliocentric ecliptic position."""
+    shape = (3, size) if size != 1 else 3
+    positive_elements = st.floats(min_value=0.1, max_value=1)
+    sign = st.sampled_from([-1, 1])
+    elements = st.builds(lambda x, y: x * y, positive_elements, sign)
+    vector = draw(arrays(dtype=float, shape=shape, elements=elements))
+    normalized_vector = vector / np.linalg.norm(vector, axis=0)
+    magnitude = draw(st.floats(min_value=0.8, max_value=2))
+    return units.Quantity(normalized_vector * magnitude, unit=units.AU)
+
+
+@st.composite
+def obspos_xyz_inst(draw: st.DrawFn) -> units.Quantity:
+    """Return a strategy for generating a heliocentric ecliptic position."""
+    return draw(get_obspos_vec(1))
+
+
+@st.composite
+def obspos_xyz_tod(draw: st.DrawFn, size: int) -> units.Quantity:
+    """Return a strategy for generating a heliocentric ecliptic position."""
+    return draw(get_obspos_vec(size))
+
+
+@st.composite
+def obspos_str(draw: st.DrawFn) -> str:
+    """Return a strategy for generating a heliocentric ecliptic position."""
+    return draw(st.sampled_from(["earth", "mars", "moon", "semb-l2"]))
+
+
+@st.composite
+def obs_inst(draw: st.DrawFn) -> str | units.Quantity:
+    """Return a strategy for generating a heliocentric ecliptic position."""
+    return draw(st.one_of(obspos_str(), obspos_xyz_inst()))
+
+
+@st.composite
+def obs_tod(draw: st.DrawFn, size: int) -> str | units.Quantity:
+    """Return a strategy for generating a heliocentric ecliptic position."""
+    return draw(st.one_of(obspos_str(), obspos_xyz_tod(size)))
+
+
+@st.composite
+def frames(draw: st.DrawFn) -> units.Quantity:
+    """Return a strategy for generating astropy coordinate frames."""
+    return draw(st.sampled_from(["galactic", "barycentricmeanecliptic", "icrs"]))
+
+
+@st.composite
+def get_lonlat(draw: st.DrawFn) -> tuple[npt.NDArray, npt.NDArray]:
+    """Return a strategy for generating longitude and latitude arrays."""
+    theta_strategy = st.floats(min_value=0, max_value=360)
+    phi_strategy = st.floats(min_value=-90, max_value=90)
+    ncoords = draw(st.integers(min_value=1, max_value=1000))
+
+    theta_array_strategy = arrays(dtype=float, shape=ncoords, elements=theta_strategy)
+    phi_array_strategy = arrays(dtype=float, shape=ncoords, elements=phi_strategy)
+    return draw(theta_array_strategy), draw(phi_array_strategy)
+
+
+@st.composite
+def sky_coord_inst(draw: st.DrawFn) -> SkyCoord:
+    """Return a strategy for generating astropy SkyCoord objects."""
+    lon, lat = draw(get_lonlat())
+    return SkyCoord(lon, lat, unit=(units.deg), frame=draw(frames()), obstime=draw(obstime_inst()))
+
+
+@st.composite
+def sky_coord_tod(draw: st.DrawFn) -> SkyCoord:
+    """Return a strategy for generating astropy SkyCoord objects."""
+    lon, lat = draw(get_lonlat())
+    obstime = draw(obstime_tod(lon.size))
+    return SkyCoord(lon, lat, unit=(units.deg), frame=draw(frames()), obstime=obstime)
diff --git a/tests/test_evaluate.py b/tests/test_evaluate.py
new file mode 100644
index 0000000..c0d61ff
--- /dev/null
+++ b/tests/test_evaluate.py
@@ -0,0 +1,251 @@
+from __future__ import annotations
+
+import numpy as np
+import pytest
+from astropy import coordinates as coords
+from astropy import time, units
+from astropy.coordinates import SkyCoord
+from hypothesis import HealthCheck, given, settings
+from hypothesis.strategies import DataObject, data
+from numpy.testing import assert_array_equal
+
+from zodipy import Model
+
+from .dirbe_tabulated import DAYS, DIRBE_START_DAY, LAT, LON, TABULATED_DIRBE_EMISSION
+from .strategies import (
+    models,
+    obs_inst,
+    obs_tod,
+    sky_coord_inst,
+    sky_coord_tod,
+)
+
+np.random.seed(42)
+
+TEST_TIME = time.Time("2021-01-01T00:00:00", scale="utc")
+TEST_SKY_COORD = SkyCoord(20, 30, unit=units.deg, obstime=TEST_TIME)
+TEST_SKY_COORD_GALACTIC = SkyCoord(20, 30, unit=units.deg, obstime=TEST_TIME, frame="galactic")
+test_model = Model(x=20 * units.micron)
+
+
+def test_compare_to_dirbe_idl() -> None:
+    """Tests that ZodiPy reproduces the DIRBE software.
+
+    Zodipy should be able to reproduce the tabulated emission from the DIRBE Zoidacal Light
+    Prediction Software with a maximum difference of 0.1%.
+    """
+    for frequency, tabulated_emission in TABULATED_DIRBE_EMISSION.items():
+        model = Model(x=frequency * units.micron, name="dirbe")
+        for idx, (day, lon, lat) in enumerate(zip(DAYS, LON, LAT)):
+            obstime = DIRBE_START_DAY + time.TimeDelta(day - 1, format="jd")
+            coord = coords.SkyCoord(
+                lon,
+                lat,
+                unit=units.deg,
+                frame=coords.BarycentricMeanEcliptic,
+                obstime=obstime,
+            )
+            emission = model.evaluate(coord)
+            assert emission.value == pytest.approx(tabulated_emission[idx], rel=0.01)
+
+
+@settings(deadline=None)
+@given(models(), sky_coord_inst(), obs_inst())
+def test_evaluate_inst(
+    model: Model,
+    sky_coord: SkyCoord,
+    obspos: units.Quantity,
+) -> None:
+    """Test that the evaluate function works for valid user input."""
+    emission = model.evaluate(sky_coord, obspos=obspos)
+    assert emission.size == sky_coord.size
+    assert isinstance(emission, units.Quantity)
+    assert emission.unit == units.MJy / units.sr
+
+
+@settings(deadline=None, suppress_health_check=[HealthCheck.data_too_large])
+@given(data(), models(), sky_coord_tod())
+def test_evaluate_tod(
+    data: DataObject,
+    model: Model,
+    sky_coord: SkyCoord,
+) -> None:
+    """Test that the evaluate function works for valid user input."""
+    obspos = data.draw(obs_tod(sky_coord.size))
+    emission = model.evaluate(sky_coord, obspos=obspos)
+    assert emission.size == sky_coord.size
+    assert isinstance(emission, units.Quantity)
+    assert emission.unit == units.MJy / units.sr
+
+
+def test_invalid_sky_coord() -> None:
+    """Test that the evaluate function raises an error for invalid sky_coord."""
+    with pytest.raises(TypeError):
+        test_model.evaluate(20)
+
+    # test that obstime is set
+    with pytest.raises(ValueError):
+        skycoord = SkyCoord(20, 30, unit=units.deg)
+        test_model.evaluate(skycoord)
+
+    # test that it crashed for multiple obstime
+    with pytest.raises(ValueError):
+        skycoord = SkyCoord(
+            20,
+            30,
+            unit=units.deg,
+            obstime=time.Time(["2021-01-01T00:00:00", "2021-01-01T00:02:00"]),
+        )
+        test_model.evaluate(skycoord)
+
+
+def test_evaluate_invalid_obspos() -> None:
+    """Test that the evaluate function raises an error for invalid obspos."""
+    test_model = Model(x=20 * units.micron)
+    with pytest.raises(ValueError):
+        test_model.evaluate(TEST_SKY_COORD, obspos="invalid")
+
+    with pytest.raises(TypeError):
+        test_model.evaluate(TEST_SKY_COORD, obspos=[30, 40, 50])
+
+    with pytest.raises(units.UnitConversionError):
+        test_model.evaluate(TEST_SKY_COORD, obspos=[30, 40, 50] * units.s)
+
+    with pytest.raises(ValueError):
+        test_model.evaluate(TEST_SKY_COORD, obspos=[[30, 40, 50], [30, 40, 50]] * units.AU)
+
+
+def test_output_shape() -> None:
+    """Test that the return_comps function works for valid user input."""
+    n_comps = test_model._ipd_model.ncomps
+    assert test_model.evaluate(TEST_SKY_COORD, return_comps=True).shape == (n_comps, 1)
+    assert test_model.evaluate(TEST_SKY_COORD, return_comps=False).shape == (1,)
+
+    skycoord = SkyCoord(
+        [20, 22, 24, 26, 28, 30],
+        [30, 28, 26, 24, 22, 20],
+        unit=units.deg,
+        obstime=TEST_TIME,
+    )
+    assert test_model.evaluate(skycoord, return_comps=True).shape == (n_comps, 6)
+    assert test_model.evaluate(skycoord, return_comps=False).shape == (6,)
+
+
+def test_input_shape() -> None:
+    """Test that shape of the input sky_coord is correct and obspos and obstime are correct."""
+    obstimes = time.Time([TEST_TIME.mjd + i for i in range(4)], format="mjd")
+    obsposes = (
+        coords.get_body("earth", obstimes)
+        .transform_to(coords.HeliocentricMeanEcliptic)
+        .cartesian.xyz.to(units.AU)
+    )
+    test_model.evaluate(SkyCoord([20, 30], [30, 40], unit=units.deg, obstime=TEST_TIME))
+    test_model.evaluate(
+        SkyCoord([20, 30, 40, 50], [30, 40, 30, 20], unit=units.deg, obstime=obstimes)
+    )
+    test_model.evaluate(
+        SkyCoord([20, 30, 40, 50], [30, 40, 30, 20], unit=units.deg, obstime=obstimes),
+        obspos=obsposes,
+    )
+
+    # obstime > sky_coord
+    with pytest.raises(ValueError):
+        test_model.evaluate(SkyCoord([20, 30], [30, 40], unit=units.deg, obstime=obstimes))
+
+    # obspos > sky_coord
+    with pytest.raises(ValueError):
+        test_model.evaluate(SkyCoord(20, 30, unit=units.deg, obstime=TEST_TIME), obspos=obsposes)
+
+    # obspos > obstime
+    with pytest.raises(ValueError):
+        test_model.evaluate(SkyCoord(20, 30, unit=units.deg, obstime=TEST_TIME), obspos=obsposes)
+
+    # obstimes > obspos
+    with pytest.raises(ValueError):
+        test_model.evaluate(
+            SkyCoord([20, 30, 40, 50], [30, 40, 30, 20], unit=units.deg, obstime=obstimes),
+            obspos=[1, 2, 3] * units.AU,
+        )
+    with pytest.raises(ValueError):
+        test_model.evaluate(
+            SkyCoord([20, 30, 40, 50], [30, 40, 30, 20], unit=units.deg, obstime=obstimes),
+            obspos=[[1, -0.4, 0.2]] * units.AU,
+        )
+    with pytest.raises(ValueError):
+        test_model.evaluate(
+            SkyCoord([20, 30, 40, 50], [30, 40, 30, 20], unit=units.deg, obstime=obstimes),
+            obspos=obsposes[:2],
+        )
+
+
+def test_return_comps() -> None:
+    """Test that the return_comps function works for valid user input."""
+    emission_comps = test_model.evaluate(TEST_SKY_COORD, return_comps=True)
+    emission = test_model.evaluate(TEST_SKY_COORD, return_comps=False)
+    assert_array_equal(emission_comps.sum(axis=0), emission)
+
+    skycoord = SkyCoord(
+        [20, 22, 24, 26, 28, 30],
+        [30, 28, 26, 24, 22, 20],
+        unit=units.deg,
+        obstime=TEST_TIME,
+    )
+    emission_comps = test_model.evaluate(skycoord, return_comps=True)
+    emission = test_model.evaluate(skycoord, return_comps=False)
+    assert_array_equal(emission_comps.sum(axis=0), emission)
+
+
+def test_multiprocessing_nproc_inst() -> None:
+    """Test that the multiprocessing works with n_proc > 1."""
+    model = Model(x=20 * units.micron)
+
+    lon = np.random.randint(low=0, high=360, size=10000)
+    lat = np.random.randint(low=-90, high=90, size=10000)
+    skycoord = SkyCoord(
+        lon,
+        lat,
+        unit=units.deg,
+        obstime=TEST_TIME,
+    )
+    emission_multi = model.evaluate(skycoord, nprocesses=4)
+    emission = model.evaluate(skycoord, nprocesses=1)
+    assert_array_equal(emission_multi, emission)
+
+    model = Model(x=75 * units.micron, name="rrm-experimental")
+
+    lon = np.random.randint(low=0, high=360, size=10000)
+    lat = np.random.randint(low=-90, high=90, size=10000)
+    skycoord = SkyCoord(
+        lon,
+        lat,
+        unit=units.deg,
+        obstime=TEST_TIME,
+    )
+    emission_multi = model.evaluate(skycoord, nprocesses=4)
+    emission = model.evaluate(skycoord, nprocesses=1)
+
+    assert_array_equal(emission_multi, emission)
+
+
+def test_multiprocessing_nproc_time() -> None:
+    """Test that the multiprocessing works with n_proc > 1."""
+    model = Model(x=20 * units.micron)
+
+    lon = np.random.randint(low=0, high=360, size=10000)
+    lat = np.random.randint(low=-90, high=90, size=10000)
+    obstime = np.linspace(0, 300, 10000) + TEST_TIME.mjd
+    skycoord = SkyCoord(
+        lon,
+        lat,
+        unit=units.deg,
+        obstime=time.Time(obstime, format="mjd"),
+    )
+    emission_multi = model.evaluate(skycoord, nprocesses=4)
+    emission = model.evaluate(skycoord, nprocesses=1)
+    assert_array_equal(emission_multi, emission)
+
+    # model = Model(x=75 * units.micron, name="rrm-experimental")
+    # emission_multi = model.evaluate(skycoord, nprocesses=4)
+    # emission = model.evaluate(skycoord, nprocesses=1)
+
+    # assert_array_equal(emission_multi, emission)
diff --git a/tests/test_get_emission.py b/tests/test_get_emission.py
deleted file mode 100644
index a6a8758..0000000
--- a/tests/test_get_emission.py
+++ /dev/null
@@ -1,476 +0,0 @@
-from __future__ import annotations
-
-import astropy.units as u
-import healpy as hp
-import numpy as np
-import pytest
-from astropy.time import Time, TimeDelta
-from hypothesis import given, settings
-from hypothesis.strategies import DataObject, data, integers
-
-from zodipy.zodipy import Zodipy
-
-from ._strategies import (
-    angles,
-    freq,
-    model,
-    nside,
-    obs,
-    pixels,
-    random_freq,
-    time,
-    weights,
-)
-from ._tabulated_dirbe import DAYS, LAT, LON, TABULATED_DIRBE_EMISSION
-
-DIRBE_START_DAY = Time("1990-01-01")
-
-
-@given(model(), time(), nside(), data())
-@settings(deadline=None)
-def test_get_emission_pix(
-    model: Zodipy,
-    time: Time,
-    nside: int,
-    data: DataObject,
-) -> None:
-    """Property test for get_emission_pix."""
-    observer = data.draw(obs(model, time))
-    frequency = data.draw(freq(model))
-    pix = data.draw(pixels(nside))
-    emission = model.get_emission_pix(
-        frequency,
-        pixels=pix,
-        nside=nside,
-        obs_time=time,
-        obs=observer,
-    )
-    assert np.size(emission) == np.size(pix)
-
-
-@given(model(), time(), nside(), data())
-@settings(deadline=None)
-def test_get_binned_emission_pix(
-    model: Zodipy,
-    time: Time,
-    nside: int,
-    data: DataObject,
-) -> None:
-    """Property test for get_binned_emission_pix."""
-    observer = data.draw(obs(model, time))
-    frequency = data.draw(freq(model))
-    pix = data.draw(pixels(nside))
-
-    emission_binned = model.get_binned_emission_pix(
-        frequency,
-        pixels=pix,
-        nside=nside,
-        obs_time=time,
-        obs=observer,
-    )
-    assert emission_binned.shape == (hp.nside2npix(nside),)
-
-
-@given(model(), time(), angles(), data())
-@settings(deadline=None)
-def test_get_emission_ang(
-    model: Zodipy,
-    time: Time,
-    angles: tuple[u.Quantity[u.deg], u.Quantity[u.deg]],
-    data: DataObject,
-) -> None:
-    """Property test for get_emission_ang."""
-    theta, phi = angles
-
-    observer = data.draw(obs(model, time))
-    frequency = data.draw(freq(model))
-
-    emission = model.get_emission_ang(
-        frequency,
-        theta=theta,
-        phi=phi,
-        obs_time=time,
-        obs=observer,
-    )
-    assert emission.size == theta.size == phi.size
-
-
-@given(model(), time(), nside(), angles(), data())
-@settings(deadline=None)
-def test_get_binned_emission_ang(
-    model: Zodipy,
-    time: Time,
-    nside: int,
-    angles: tuple[u.Quantity[u.deg], u.Quantity[u.deg]],
-    data: DataObject,
-) -> None:
-    """Property test for get_binned_emission_pix."""
-    theta, phi = angles
-
-    observer = data.draw(obs(model, time))
-    frequency = data.draw(freq(model))
-
-    emission_binned = model.get_binned_emission_ang(
-        frequency,
-        theta=theta,
-        phi=phi,
-        nside=nside,
-        obs_time=time,
-        obs=observer,
-    )
-    assert emission_binned.shape == (hp.nside2npix(nside),)
-
-
-@given(model(extrapolate=False), time(), angles(lonlat=True), nside(), data())
-@settings(deadline=None)
-def test_invalid_freq(
-    model: Zodipy,
-    time: Time,
-    angles: tuple[u.Quantity[u.deg], u.Quantity[u.deg]],
-    nside: int,
-    data: DataObject,
-) -> None:
-    """Property test checking for unsupported spectral range."""
-    theta, phi = angles
-    observer = data.draw(obs(model, time))
-    pix = data.draw(pixels(nside))
-
-    freq = data.draw(random_freq(unit=model._ipd_model.spectrum.unit))
-    if not (model._ipd_model.spectrum[0] <= freq <= model._ipd_model.spectrum[-1]):
-        with pytest.raises(ValueError):
-            model.get_emission_pix(
-                freq,
-                pixels=pix,
-                nside=nside,
-                obs_time=time,
-                obs=observer,
-            )
-
-        with pytest.raises(ValueError):
-            model.get_emission_ang(
-                freq,
-                theta=theta,
-                phi=phi,
-                obs_time=time,
-                obs=observer,
-                lonlat=True,
-            )
-
-        with pytest.raises(ValueError):
-            model.get_binned_emission_pix(
-                freq,
-                pixels=pix,
-                nside=nside,
-                obs_time=time,
-                obs=observer,
-            )
-
-        with pytest.raises(ValueError):
-            model.get_binned_emission_ang(
-                freq,
-                theta=theta,
-                phi=phi,
-                nside=nside,
-                obs_time=time,
-                obs=observer,
-                lonlat=True,
-            )
-
-
-def test_compare_to_dirbe_idl() -> None:
-    """Tests that ZodiPy reproduces the DIRBE software.
-
-    Zodipy should be able to reproduce the tabulated emission from the DIRBE Zoidacal Light
-    Prediction Software with a maximum difference of 0.1%.
-    """
-    model = Zodipy("dirbe")
-    for frequency, tabulated_emission in TABULATED_DIRBE_EMISSION.items():
-        for idx, (day, lon, lat) in enumerate(zip(DAYS, LON, LAT)):
-            time = DIRBE_START_DAY + TimeDelta(day - 1, format="jd")
-
-            emission = model.get_emission_ang(
-                frequency * u.micron,
-                theta=lon * u.deg,
-                phi=lat * u.deg,
-                lonlat=True,
-                obs="earth",
-                obs_time=time,
-            )
-            assert emission.value == pytest.approx(tabulated_emission[idx], rel=0.01)
-
-
-@given(model(), time(), nside(), integers(min_value=1, max_value=100), data())
-@settings(max_examples=10, deadline=None)
-def test_invalid_pixel(
-    model: Zodipy,
-    time: Time,
-    nside: int,
-    random_integer: int,
-    data: DataObject,
-) -> None:
-    """Tests that an error is raised when an invalid pixel is passed to get_emission_pix."""
-    frequency = data.draw(freq(model))
-    observer = data.draw(obs(model, time))
-    npix = hp.nside2npix(nside)
-
-    with pytest.raises(ValueError):
-        model.get_emission_pix(
-            frequency,
-            pixels=npix + random_integer,
-            nside=nside,
-            obs_time=time,
-            obs=observer,
-        )
-
-    with pytest.raises(ValueError):
-        model.get_emission_pix(
-            frequency,
-            pixels=-random_integer,
-            nside=nside,
-            obs_time=time,
-            obs=observer,
-        )
-
-
-def test_multiprocessing() -> None:
-    """Tests multiprocessing with for zodipy.
-
-    Tests that model with multiprocessing enabled returns the same value as
-    without multiprocessing.
-    """
-    model = Zodipy()
-    model_parallel = Zodipy(n_proc=4)
-
-    observer = "earth"
-    time = Time("2020-01-01")
-    frequency = 78 * u.micron
-    nside = 32
-    pix = np.random.randint(0, hp.nside2npix(nside), size=1000)
-    theta = np.random.uniform(0, 180, size=1000) * u.deg
-    phi = np.random.uniform(0, 360, size=1000) * u.deg
-
-    emission_pix = model.get_emission_pix(
-        frequency,
-        pixels=pix,
-        nside=nside,
-        obs_time=time,
-        obs=observer,
-    )
-    emission_pix_parallel = model_parallel.get_emission_pix(
-        frequency,
-        pixels=pix,
-        nside=nside,
-        obs_time=time,
-        obs=observer,
-    )
-    assert np.allclose(emission_pix, emission_pix_parallel)
-
-    emission_binned_pix = model.get_binned_emission_pix(
-        frequency,
-        pixels=pix,
-        nside=nside,
-        obs_time=time,
-        obs=observer,
-    )
-    emission_binned_pix_parallel = model_parallel.get_binned_emission_pix(
-        frequency,
-        pixels=pix,
-        nside=nside,
-        obs_time=time,
-        obs=observer,
-    )
-    assert np.allclose(emission_binned_pix, emission_binned_pix_parallel)
-
-    emission_ang = model.get_emission_ang(
-        frequency,
-        theta=theta,
-        phi=phi,
-        obs_time=time,
-        obs=observer,
-    )
-    emission_ang_parallel = model_parallel.get_emission_ang(
-        frequency,
-        theta=theta,
-        phi=phi,
-        obs_time=time,
-        obs=observer,
-    )
-    assert np.allclose(emission_ang, emission_ang_parallel)
-
-    emission_binned_ang = model.get_binned_emission_ang(
-        frequency,
-        theta=theta,
-        phi=phi,
-        nside=nside,
-        obs_time=time,
-        obs=observer,
-    )
-
-    emission_binned_ang_parallel = model_parallel.get_binned_emission_ang(
-        frequency,
-        theta=theta,
-        phi=phi,
-        nside=nside,
-        obs_time=time,
-        obs=observer,
-    )
-
-    assert np.allclose(emission_binned_ang.value, emission_binned_ang_parallel.value)
-
-
-def test_inner_radial_cutoff_multiprocessing() -> None:
-    """Testing that model with inner radial cutoffs can be parallelized."""
-    model = Zodipy("RRM-experimental")
-    model_parallel = Zodipy("RRM-experimental", n_proc=4)
-
-    observer = "earth"
-    time = Time("2020-01-01")
-    frequency = 78 * u.micron
-    nside = 32
-    pix = np.random.randint(0, hp.nside2npix(nside), size=1000)
-
-    emission_pix = model.get_emission_pix(
-        frequency,
-        pixels=pix,
-        nside=nside,
-        obs_time=time,
-        obs=observer,
-    )
-    emission_pix_parallel = model_parallel.get_emission_pix(
-        frequency,
-        pixels=pix,
-        nside=nside,
-        obs_time=time,
-        obs=observer,
-    )
-    assert np.allclose(emission_pix, emission_pix_parallel)
-
-
-@given(
-    model(extrapolate=True),
-    time(),
-    nside(),
-    angles(),
-    random_freq(bandpass=True),
-    data(),
-)
-@settings(deadline=None)
-def test_bandpass_integration(
-    model: Zodipy,
-    time: Time,
-    nside: int,
-    angles: tuple[u.Quantity[u.deg], u.Quantity[u.deg]],
-    freqs: u.Quantity[u.Hz] | u.Quantity[u.micron],
-    data: DataObject,
-) -> None:
-    """Property test for bandpass integrations."""
-    theta, phi = angles
-    observer = data.draw(obs(model, time))
-    bp_weights = data.draw(weights(freqs))
-    emission_binned = model.get_binned_emission_ang(
-        freqs,
-        weights=bp_weights,
-        theta=theta,
-        phi=phi,
-        nside=nside,
-        obs_time=time,
-        obs=observer,
-    )
-    assert emission_binned.shape == (hp.nside2npix(nside),)
-
-
-@given(
-    model(extrapolate=True),
-    time(),
-    nside(),
-    angles(),
-    random_freq(bandpass=True),
-    data(),
-)
-@settings(deadline=None)
-def test_weights(
-    model: Zodipy,
-    time: Time,
-    nside: int,
-    angles: tuple[u.Quantity[u.deg], u.Quantity[u.deg]],
-    freqs: u.Quantity[u.Hz] | u.Quantity[u.micron],
-    data: DataObject,
-) -> None:
-    """Property test for bandpass weights."""
-    theta, phi = angles
-    observer = data.draw(obs(model, time))
-    bp_weights = data.draw(weights(freqs))
-
-    model.get_binned_emission_ang(
-        freqs,
-        weights=bp_weights,
-        theta=theta,
-        phi=phi,
-        nside=nside,
-        obs_time=time,
-        obs=observer,
-    )
-
-
-def test_custom_weights() -> None:
-    """Tests bandpass integration with custom weights."""
-    model = Zodipy()
-    time = Time("2020-01-01")
-    nside = 32
-    pix = np.arange(hp.nside2npix(nside))
-    central_freq = 25
-    sigma_freq = 3
-    freqs = np.linspace(central_freq - sigma_freq, central_freq + sigma_freq, 100) * u.micron
-    weights = np.random.randn(len(freqs))
-    weights /= np.trapz(weights, freqs.value)
-
-    model.get_emission_pix(
-        freq=freqs,
-        weights=weights,
-        pixels=pix,
-        nside=nside,
-        obs_time=time,
-        obs="earth",
-    )
-
-
-def test_custom_obs_pos() -> None:
-    """Tests a user specified observer position."""
-    model = Zodipy()
-    time = Time("2020-01-01")
-    nside = 64
-    pix = np.arange(hp.nside2npix(nside))
-
-    model.get_emission_pix(
-        freq=234 * u.micron,
-        pixels=pix,
-        nside=nside,
-        obs_time=time,
-        obs_pos=[0.1, 0.2, 1] * u.AU,
-    )
-
-    model.get_emission_pix(
-        freq=234 * u.micron,
-        pixels=pix,
-        nside=nside,
-        obs_time=time,
-        obs_pos=[2, 0.1, 4] * u.AU,
-    )
-
-    with pytest.raises(TypeError):
-        model.get_emission_pix(
-            freq=234 * u.micron,
-            pixels=pix,
-            nside=nside,
-            obs_time=time,
-            obs_pos=[2, 0.1, 4],
-        )
-
-    with pytest.raises(u.UnitsError):
-        model.get_emission_pix(
-            freq=234 * u.micron,
-            pixels=pix,
-            nside=nside,
-            obs_time=time,
-            obs_pos=[2, 0.1, 4] * u.s,
-        )
diff --git a/tests/test_model.py b/tests/test_model.py
new file mode 100644
index 0000000..53dddeb
--- /dev/null
+++ b/tests/test_model.py
@@ -0,0 +1,110 @@
+import random
+import warnings
+
+import numpy as np
+import pytest
+from astropy import coordinates, time, units
+
+from zodipy import Model, grid_number_density, model_registry
+
+
+def test_x_input() -> None:
+    """Test that the x attribute is set correctly."""
+    with pytest.raises(units.UnitConversionError):
+        Model(x=20 * units.s)
+
+    with pytest.raises(TypeError):
+        Model(x=20)
+
+    x = np.linspace(20, 25, 10) * units.micron
+    with pytest.raises(ValueError):
+        Model(x)
+
+
+def test_weights_input() -> None:
+    """Test weights."""
+    bandpass = np.random.rand(10)
+    x = np.linspace(20, 25, 15) * units.micron
+
+    with pytest.raises(ValueError):
+        Model(20 * units.micron, weights=bandpass)
+
+    with pytest.raises(ValueError):
+        Model(x, weights=bandpass, name="dirbe")
+
+
+def test_extrapolate_input() -> None:
+    """Test extrapolate."""
+    with pytest.raises(ValueError):
+        Model(0.5 * units.micron, name="dirbe", extrapolate=False)
+
+    with pytest.raises(ValueError):
+        Model(400 * units.micron, name="dirbe", extrapolate=False)
+
+    Model(20 * units.micron, extrapolate=True)
+
+    x = np.linspace(40, 125, 10) * units.GHz
+    bandpass = np.random.rand(10)
+    with pytest.raises(ValueError):
+        Model(x, weights=bandpass, name="planck18", extrapolate=False)
+
+    Model(x, weights=bandpass, name="planck18", extrapolate=True)
+
+
+def test_get_parameters() -> None:
+    """Tests that the parameters are returned as a dictionary."""
+    model = Model(20 * units.micron, name="dirbe")
+    assert isinstance(model.get_parameters(), dict)
+
+
+def test_update_model() -> None:
+    """Tests that the model can be updated."""
+    model = Model(20 * units.micron, name="dirbe")
+    obstime = time.Time("2021-01-01T00:00:00")
+    skycoord = coordinates.SkyCoord(20, 30, unit=units.deg, obstime=obstime)
+    emission_before = model.evaluate(skycoord)
+    parameters = model.get_parameters()
+    comp = random.choice(list(parameters["comps"].keys()))
+    parameter = random.choice(list(parameters["comps"][comp]))
+    parameters["comps"][comp][parameter] = random.random()
+    model.update_parameters(parameters)
+    emission_after = model.evaluate(skycoord)
+    assert not np.allclose(emission_before, emission_after)
+
+
+def test_get_model_raises_error() -> None:
+    """Tests that an error is raised when the model is not found."""
+    with pytest.raises(ValueError):
+        model_registry.get_model("invalid")
+
+
+def test_register_model() -> None:
+    """Tests that a model can be registered."""
+    DIRBE_model = model_registry.get_model("DIRBE")
+    DIRBE_model.T_0 += 250
+    model_registry.register_model("updated model", DIRBE_model)
+
+
+def test_register_model_raises_error() -> None:
+    """Tests that an error is raised when the model is already registered."""
+    with pytest.raises(ValueError):
+        model_registry.register_model("DIRBE", model_registry.get_model("DIRBE"))
+
+
+def test_grid_number_density() -> None:
+    """Test the number density gridding function."""
+    x = np.linspace(0, 5, 100) * units.AU
+    y = np.linspace(0, 5, 100) * units.AU
+    z = np.linspace(0, 2, 100) * units.AU
+    obstime = time.Time("2022-01-01")
+
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore")
+        grid = grid_number_density(x, y, z, obstime=obstime)
+    assert grid.sum(axis=0).shape == (100, 100, 100)
+
+    with warnings.catch_warnings():
+        warnings.simplefilter("ignore")
+        grid = grid_number_density(x, y, z, obstime=obstime, name="rrm-experimental")
+
+    assert grid.sum(axis=0).shape == (100, 100, 100)
diff --git a/tests/test_model_registry.py b/tests/test_model_registry.py
deleted file mode 100644
index f8851e0..0000000
--- a/tests/test_model_registry.py
+++ /dev/null
@@ -1,32 +0,0 @@
-import hypothesis.strategies as st
-import pytest
-from hypothesis import given
-
-from zodipy import model_registry
-
-ALL_MODELS = model_registry.models
-
-
-@given(st.sampled_from(ALL_MODELS))
-def test_get_model(model: str) -> None:
-    """Tests that the model can be retrieved."""
-    model_registry.get_model(model)
-
-
-def test_get_model_raises_error() -> None:
-    """Tests that an error is raised when the model is not found."""
-    with pytest.raises(ValueError):
-        model_registry.get_model("metins_model")
-
-
-def test_register_model() -> None:
-    """Tests that a model can be registered."""
-    DIRBE_model = model_registry.get_model("DIRBE")
-    DIRBE_model.T_0 += 250
-    model_registry.register_model("metins_model", DIRBE_model)
-
-
-def test_register_model_raises_error() -> None:
-    """Tests that an error is raised when the model is already registered."""
-    with pytest.raises(ValueError):
-        model_registry.register_model("DIRBE", model_registry.get_model("DIRBE"))
diff --git a/tests/test_params.py b/tests/test_params.py
deleted file mode 100644
index c60ed46..0000000
--- a/tests/test_params.py
+++ /dev/null
@@ -1,23 +0,0 @@
-import random
-
-from hypothesis import given
-
-from zodipy.zodipy import Zodipy
-
-from ._strategies import model
-
-
-@given(model())
-def test_get_parameters(model: Zodipy) -> None:
-    """Tests that the parameters are returned as a dictionary."""
-    assert isinstance(model.get_parameters(), dict)
-
-
-@given(model())
-def test_update_model(model: Zodipy) -> None:
-    """Tests that the model can be updated."""
-    parameters = model.get_parameters()
-    comp = random.choice(list(parameters["comps"].keys()))
-    parameter = random.choice(list(parameters["comps"][comp]))
-    parameters["comps"][comp][parameter] = random.random()
-    model.update_parameters(parameters)
diff --git a/tests/test_reprs.py b/tests/test_reprs.py
deleted file mode 100644
index ef05271..0000000
--- a/tests/test_reprs.py
+++ /dev/null
@@ -1,12 +0,0 @@
-from hypothesis import given
-
-from zodipy.zodipy import Zodipy
-
-from ._strategies import model
-
-
-@given(model())
-def test_ipd_model_repr(model: Zodipy) -> None:
-    """Tests that the IPD model has a userfriendly repr."""
-    repr(model)
-    repr(model._ipd_model)
diff --git a/tests/test_source_functions.py b/tests/test_source_functions.py
deleted file mode 100644
index d0315f2..0000000
--- a/tests/test_source_functions.py
+++ /dev/null
@@ -1,24 +0,0 @@
-import numpy as np
-import pytest
-
-from zodipy._source_funcs import get_blackbody_emission
-
-TEMPERATURE = np.array([30])
-TEMPERATURE_ARRAY = np.array([31, 45, 53])
-R = 3
-R_ARRAY = np.array([4, 5.3, 6])
-DELTA = 0.324
-FREQUENCY = 549e9
-
-
-def test_blackbody_emission_value() -> None:
-    """Tests that return value."""
-    emission = get_blackbody_emission(T=TEMPERATURE, freq=FREQUENCY)
-    assert emission == pytest.approx(1.73442848898e-15, abs=1e-20)
-
-
-def test_blackbody_emission_value_array() -> None:
-    """Tests the return value given a temperature array."""
-    emission = get_blackbody_emission(T=TEMPERATURE_ARRAY, freq=FREQUENCY)
-    true_values = np.array([1.82147825366e-15, 3.06550295038e-15, 3.78860400626e-15])
-    assert emission == pytest.approx(true_values, abs=1e-20)
diff --git a/tests/test_tabulated_density.py b/tests/test_tabulated_density.py
deleted file mode 100644
index 5572cb3..0000000
--- a/tests/test_tabulated_density.py
+++ /dev/null
@@ -1,55 +0,0 @@
-import random
-
-import numpy as np
-from hypothesis import given, settings
-from hypothesis.strategies import floats, integers
-
-from zodipy import model_registry, tabulate_density
-from zodipy.zodipy import Zodipy
-
-from ._strategies import model
-
-
-@given(
-    model(),
-    integers(min_value=10, max_value=200),
-    floats(min_value=2, max_value=10),
-    floats(min_value=2, max_value=10),
-    floats(min_value=2, max_value=10),
-)
-@settings(max_examples=10, deadline=None)
-def test_tabulated_density(
-    model: Zodipy,
-    n_grid_points: int,
-    x_boundary: float,
-    y_boundary: float,
-    z_boundary: float,
-) -> None:
-    """Tests that the tabulated density is a 3D array of the correct shape."""
-    x = np.linspace(-x_boundary, -x_boundary, n_grid_points)
-    y = np.linspace(-y_boundary, y_boundary, n_grid_points)
-    z = np.linspace(-z_boundary, z_boundary, n_grid_points)
-
-    grid_regular = np.meshgrid(x, y, z)
-    grid_array = np.asarray(grid_regular)
-    grid = random.choice([grid_array, grid_regular])
-
-    assert tabulate_density(grid, model=model.model).shape == (
-        len(model._ipd_model.comps),
-        n_grid_points,
-        n_grid_points,
-        n_grid_points,
-    )
-
-
-def test_tabulated_density_str_model() -> None:
-    """Tests that the tabulated density runs."""
-    x = np.linspace(-5, -5, 100)
-    y = np.linspace(-5, 5, 100)
-    z = np.linspace(-2, 2, 100)
-
-    grid = np.meshgrid(x, y, z)
-
-    model = random.choice(model_registry.models)
-
-    tabulate_density(grid, model=model)
diff --git a/tests/test_validators.py b/tests/test_validators.py
deleted file mode 100644
index 53d070e..0000000
--- a/tests/test_validators.py
+++ /dev/null
@@ -1,136 +0,0 @@
-import astropy.units as u
-import numpy as np
-import pytest
-from astropy.time import Time
-from hypothesis import given
-from hypothesis.strategies import DataObject, data
-
-from zodipy._types import FrequencyOrWavelength
-from zodipy._validators import get_validated_and_normalized_weights, get_validated_freq
-from zodipy.zodipy import Zodipy
-
-from ._strategies import model, random_freq, weights
-
-BANDPASS_FREQUENCIES = np.linspace(95, 105, 11) * u.GHz
-BANDPASS_WAVELENGTHS = np.linspace(20, 25, 11) * u.micron
-BANDPASS_WEIGHTS = np.array([2, 3, 5, 9, 11, 12, 11, 9, 5, 3, 2])
-OBS_TIME = Time("2021-01-01T00:00:00")
-
-
-@given(model(extrapolate=False))
-def test_validate_frequencies(model: Zodipy) -> None:
-    """Tests that the frequencies are validated."""
-    with pytest.raises(TypeError):
-        get_validated_freq(
-            freq=BANDPASS_FREQUENCIES.value,
-            model=model._ipd_model,
-            extrapolate=model.extrapolate,
-        )
-    with pytest.raises(TypeError):
-        get_validated_freq(
-            freq=25,
-            model=model._ipd_model,
-            extrapolate=model.extrapolate,
-        )
-    with pytest.raises(u.UnitsError):
-        get_validated_freq(
-            freq=BANDPASS_FREQUENCIES.value * u.g,
-            model=model._ipd_model,
-            extrapolate=model.extrapolate,
-        )
-    with pytest.raises(u.UnitsError):
-        get_validated_freq(
-            freq=25 * u.g,
-            model=model._ipd_model,
-            extrapolate=model.extrapolate,
-        )
-
-
-@given(random_freq(bandpass=True), data())
-def test_validate_weights(freq: FrequencyOrWavelength, data: DataObject) -> None:
-    """Tests that the bandpass weights are validated."""
-    bp_weights = data.draw(weights(freq))
-    bp_weights = get_validated_and_normalized_weights(
-        weights=bp_weights,
-        freq=freq,
-    )
-    assert np.trapz(bp_weights, freq.value) == pytest.approx(1.0)
-
-    with pytest.raises(ValueError):
-        get_validated_and_normalized_weights(
-            weights=None,
-            freq=BANDPASS_FREQUENCIES,
-        )
-
-
-def test_validate_weights_numbers() -> None:
-    """Tests that the bandpass weights are normalized."""
-    get_validated_and_normalized_weights(
-        weights=None,
-        freq=BANDPASS_FREQUENCIES[0],
-    )
-    with pytest.raises(ValueError):
-        get_validated_and_normalized_weights(
-            weights=np.array([1, 2, 3]),
-            freq=BANDPASS_FREQUENCIES,
-        )
-    with pytest.raises(ValueError):
-        get_validated_and_normalized_weights(
-            weights=BANDPASS_WEIGHTS[:10],
-            freq=BANDPASS_FREQUENCIES[0],
-        )
-    with pytest.raises(ValueError):
-        get_validated_and_normalized_weights(
-            weights=None,
-            freq=BANDPASS_FREQUENCIES,
-        )
-
-
-def test_validate_weights_strictly_increasing() -> None:
-    """Tests that an error is raised if a bandpass is not strictly increasing."""
-    with pytest.raises(ValueError):
-        get_validated_and_normalized_weights(
-            weights=BANDPASS_WEIGHTS,
-            freq=np.flip(BANDPASS_FREQUENCIES),
-        )
-
-
-def test_validate_weights_shape() -> None:
-    """Tests that the bandpass weights have the correct shape."""
-    weights = get_validated_and_normalized_weights(
-        weights=BANDPASS_WEIGHTS,
-        freq=BANDPASS_FREQUENCIES,
-    )
-    assert weights.shape == BANDPASS_WEIGHTS.shape
-
-    weights = get_validated_and_normalized_weights(
-        weights=None,
-        freq=BANDPASS_FREQUENCIES[0],
-    )
-    assert weights.size == 1
-    assert weights == np.array([1.0], dtype=np.float64)
-
-
-def test_extrapolate_raises_error() -> None:
-    """Tests that an error is correctly raised when extrapolation is not allowed."""
-    with pytest.raises(ValueError):
-        model = Zodipy("dirbe")
-        model.get_emission_pix(400 * u.micron, pixels=[1, 4, 5], nside=32, obs_time=OBS_TIME)
-
-    model = Zodipy("dirbe", extrapolate=True)
-    model.get_emission_pix(400 * u.micron, pixels=[1, 4, 5], nside=32, obs_time=OBS_TIME)
-
-
-def test_interp_kind() -> None:
-    """Tests that the interpolation kind can be passed in."""
-    model = Zodipy("dirbe", interp_kind="linear")
-    linear = model.get_emission_pix(27 * u.micron, pixels=[1, 4, 5], nside=32, obs_time=OBS_TIME)
-
-    model = Zodipy("dirbe", interp_kind="quadratic")
-    quadratic = model.get_emission_pix(27 * u.micron, pixels=[1, 4, 5], nside=32, obs_time=OBS_TIME)
-
-    assert not np.allclose(linear, quadratic)
-
-    with pytest.raises(NotImplementedError):
-        model = Zodipy("dirbe", interp_kind="sdfs")
-        model.get_emission_pix(27 * u.micron, pixels=[1, 4, 5], nside=32, obs_time=OBS_TIME)