diff --git a/docs/animation.md b/docs/animation.md index c512b4a..49cd723 100644 --- a/docs/animation.md +++ b/docs/animation.md @@ -35,9 +35,10 @@ resolved data has 2 dimensions. | Dimensions | Plotting function | Notes | | ---------- | ----------------------------- | --------------------- | -| `2` | | | -| `3` | | | -| `>3` | | Not fully implemented | +| 2 | | | +| 3 | | | +| 4 | | WIP | +| >4 | | Not fully implemented | ### 1D simulation @@ -103,6 +104,10 @@ anim.show() ### 3D simulation +```{warning} +[Voxel](./plotting.md#voxel-plots) animations are not currently implemented. +``` + Opening a 3D simulation as a multi-file dataset and plotting it will return a . However, this may not be desirable. We can plot a 3D simulation along a certain plane in the diff --git a/docs/figures/voxel_plot_high_res.png b/docs/figures/voxel_plot_high_res.png new file mode 100644 index 0000000..0e2262b Binary files /dev/null and b/docs/figures/voxel_plot_high_res.png differ diff --git a/docs/index.md b/docs/index.md index 336ce52..8cdfb2b 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,6 +21,7 @@ why_sdf_xarray.md loading_data.md understanding_datasets.md unit_conversion.md +plotting.md animation.md ``` diff --git a/docs/plotting.md b/docs/plotting.md new file mode 100644 index 0000000..80f76c5 --- /dev/null +++ b/docs/plotting.md @@ -0,0 +1,95 @@ +--- +file_format: mystnb +kernelspec: + name: python3 +--- + +# Plotting + +You can plot datasets using +[`xarray.DataArray.epoch.plot`](project:#sdf_xarray.dataarray_accessor.EpochAccessor.plot). +This is a custom plotting routine that builds on top of +, so you keep the familiar plotting +behaviour while using conveniences (see +[here](project:#sdf_xarray.dataarray_accessor.EpochAccessor.plot) for details). +Under the hood, plotting is still handled by , which means you +can use the full API to customise your figure. + +| Dimensions | Base plotting function | Notes | +| ---------- | ----------------------------- | --------------------- | +| 1 | | | +| 2 | | | +| 3 | | Custom functionality built for | +| >3 | | Not fully implemented | + +## Line plots + +One dimensional data will be plotted as a line. Multiple lines can be easily +plotted at the same time. + +```{code-cell} ipython3 +import sdf_xarray as sdfxr +import matplotlib.pyplot as plt +ds = sdfxr.open_dataset("tutorial_dataset_1d/0010.sdf") +da_1 = ds["Derived_Number_Density_Electron"] +da_2 = ds["Derived_Number_Density_Ion"] +da_1.epoch.plot(label = "Electron") +da_2.epoch.plot(label = "Ion") +plt.legend() +plt.show() +``` + +## Mesh plots + +Two dimensional data will be plotted as a mesh. + +```{code-cell} ipython3 +ds = sdfxr.open_dataset("tutorial_dataset_2d/0010.sdf") +da = ds["Derived_Number_Density_Electron"] +da.epoch.plot() +plt.show() +``` + +## Voxel plots + +Three dimensional data will be plotted using voxels. This behaviour is not native +to , it has been custom built specifically for this package. + +```python +ds = sdfxr.open_dataset("tutorial_dataset_3d/0000.sdf") +da = ds["Derived_Number_Density"] +da.epoch.plot(vmin = 1e27) +plt.show() +``` + +![voxel_plot_high_res](./figures/voxel_plot_high_res.png) + +```{warning} +Voxel plots can be extremely computationally expensive and may take longer than +expected to plot. +``` + +Because voxel plots are very expensive, even with relatively small data arrays, +plotting can be sped up by using [`xarray.DataArray.epoch.resize`](project:#sdf_xarray.dataarray_accessor.EpochAccessor.resize), +which uses interpolation to reduce the resolution. + +```{code-cell} ipython3 +ds = sdfxr.open_dataset("tutorial_dataset_3d/0005.sdf") +da = ds["Derived_Number_Density"] +da_resized = da.epoch.resize((20, 20, 20)) +da_resized.epoch.plot(vmin = 1e27) +plt.show() +``` + +## Histograms + +When the data array has four or more dimensions (or is specified by the user), it will +be plotted as a histogram. + +```{code-cell} ipython3 +ds = sdfxr.open_dataset("tutorial_dataset_3d/0005.sdf") +da = ds["Derived_Number_Density"] + +da.epoch.plot(hist = True) +plt.show() +``` \ No newline at end of file diff --git a/docs/understanding_datasets.md b/docs/understanding_datasets.md index d2c1914..087731b 100644 --- a/docs/understanding_datasets.md +++ b/docs/understanding_datasets.md @@ -29,23 +29,10 @@ ds = sdfxr.open_mfdataset("tutorial_dataset_1d/*.sdf") ds["Electric_Field_Ex"] ``` -## Plotting +## Visualisation -You can plot datasets using -[`xarray.DataArray.epoch.plot`](project:#sdf_xarray.dataarray_accessor.EpochAccessor.plot). -This is a custom plotting routine that builds on top of -, so you keep the familiar plotting -behaviour while using conveniences (see -[here](project:#sdf_xarray.dataarray_accessor.EpochAccessor.plot) for details). -Under the hood, plotting is still handled by , which means you -can use the full API to customise your figure. - -```{code-cell} ipython3 -# This is discretized in both space and time -ds["Electric_Field_Ex"].epoch.plot() -plt.title("Electric field along the x-axis") -plt.show() -``` +`sdf-xarray` has built-in plotting and animation functionality, more details can +be found on [](./plotting.md) and [](./animation.md). ## Dimension slicing @@ -56,6 +43,7 @@ for easy indexing. To quickly determine the number of time steps available, you can check the size of the time dimension. ```{code-cell} ipython3 +ds = sdfxr.open_mfdataset("tutorial_dataset_1d/*.sdf") # This corresponds to the number of individual SDF files loaded print(f"There are a total of {ds['time'].size} time steps") @@ -66,7 +54,7 @@ print(f"The time at the 20th simulation step is {sim_time:.2e} s") You can select and extract a single simulation snapshot using the integer index of the time step with the function. This can be -done by passsing the index to the `time` parameter (e.g., `time=0` for +done by passing the index to the `time` parameter (e.g., `time=0` for the first snapshot). ```{code-cell} ipython3 @@ -74,7 +62,7 @@ ds["Electric_Field_Ex"].isel(time=20) ``` We can also use the function if you wish to pass a -value intead of an index. +value instead of an index. ```{tip} If you know roughly what time you wish to select but not the exact value @@ -122,6 +110,51 @@ print(f"Total particle energy absorbed: {ds["Total_Particle_Energy_in_Simulation print(f"The laser absorption fraction: {ds["Laser_Absorption_Fraction_in_Simulation"][-1].values:.1f} %") ``` +## Limit + +The `.epoch` accessor has the ability to "limit" data arrays. This is a helper +function which applies to the upper and lower bounds +of each dimension. You can use `None` to preserve the existing limit. + +```{code-cell} ipython3 +ds = sdfxr.open_dataset("tutorial_dataset_2d/0020.sdf") +da = ds["Derived_Number_Density_Electron"] +da.epoch.plot() +print(f"Orignal shape: {da.shape}") +plt.show() +``` + +```{code-cell} ipython3 +da_limit = da.epoch.limit(((-2e-6, 2e-6), (None, 0))) +da_limit.epoch.plot() +print(f"New shape: {da_limit.shape}") +plt.show() +``` + +## Resize + +The data arrays can be interpolated with +to either increase or decrease the resolution while mantaining the general structure. +Decreasing the size may be useful to download large data from HPCs to local machines +or ploting with [](./plotting.md#voxel-plots). Be aware that the original +data will not be preserved. + +```{code-cell} ipython3 +ds = sdfxr.open_dataset("tutorial_dataset_2d/0020.sdf") +da = ds["Derived_Number_Density_Electron"] + +da.epoch.plot() +print(f"Orignal shape: {da.shape}") +plt.show() +``` + +```{code-cell} ipython3 +da_resized = da.epoch.resize((50, 50)) +da_resized.epoch.plot() +print(f"New shape: {da_resized.shape}") +plt.show() +``` + ## Visualisation on HPC Machines In many cases you will be running EPOCH simulations via a HPC cluster and your diff --git a/docs/why_sdf_xarray.md b/docs/why_sdf_xarray.md index 76bf3f9..58cecfb 100644 --- a/docs/why_sdf_xarray.md +++ b/docs/why_sdf_xarray.md @@ -7,7 +7,7 @@ There are several benefits to using this package over the [`sdf_helper`](https:/ - [data to numpy and pandas](https://docs.xarray.dev/en/stable/user-guide/pandas.html) - Data can be easily converted to [numpy](https://www.numpy.org/) or [pandas](https://pandas.pydata.org/) if you prefer to work with raw data. - [data labelling](project:understanding_datasets.md) - All SDF files come with a bunch of information about them such as the `dimensions`, `units` and `name` of the variable. In these are added to each variable making it much easier to read. - [lazy/partial loading](https://docs.xarray.dev/en/stable/internals/internal-design.html#lazy-loading) - This reduces the RAM requirements when loading large SDF files as we only load the actual array values when they are needed. -- [plotting](project:understanding_datasets.md#plotting) - You can easily plot variables across several dimensions including time using built in support. +- [plotting](project:plotting.md) - You can easily plot variables across several dimensions including time using built in support. - [animating](project:animation.md) - Visualise your data across time or slice through another dimension and make beautiful GIFs for your presentations. - [deck loading](loading-input-deck) - automatically loads in the associated `input.deck` utilised when creating the simulation so that you can access setup variables. - [easier unit conversion](project:unit_conversion.md) - Convert dimensions and variable units to other types with the help of [`pint`](https://pint.readthedocs.io/en/stable). diff --git a/pyproject.toml b/pyproject.toml index 33b9c35..158f2c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,7 @@ docs = [ "sphinx-design>=0.7.0", "pooch>=1.8.2", "tqdm", + "scipy", ] build = ["cibuildwheel[uv]"] lint = ["ruff"] @@ -80,6 +81,7 @@ test = [ "tqdm", "pint", "pint-xarray", + "scipy", ] [project.entry-points."xarray.backends"] @@ -133,6 +135,10 @@ ignore = [ "PLR2004", # magic-comparison "B9", # flake8-bugbear opinionated warnings "PLR0913", # remove maximum number of arguments in a function + "PLR0917", # remove maximum number of positional arguments in a function + "BLE001", # allow capturing of base exception ] -exclude = ["**/_version.py"] -# Auto-generated file +exclude = ["**/_version.py"] # Auto-generated file + +[tool.pytest.ini_options] +filterwarnings = ['ignore::UserWarning'] diff --git a/src/sdf_xarray/dataarray_accessor.py b/src/sdf_xarray/dataarray_accessor.py index 2377105..f9bf69c 100644 --- a/src/sdf_xarray/dataarray_accessor.py +++ b/src/sdf_xarray/dataarray_accessor.py @@ -3,21 +3,54 @@ from types import MethodType from typing import TYPE_CHECKING +import numpy as np import xarray as xr from xarray.plot.accessor import DataArrayPlotAccessor -from .plotting import animate, show +from .plotting import _recover_vertex_coord, animate, show, voxel_plot if TYPE_CHECKING: from matplotlib.animation import FuncAnimation +def _resize_ndarray( + arr: np.ndarray, + new_shape: tuple | list | np.ndarray, +) -> np.ndarray: + """ + Resizes a `numpy.ndarray` to another shape. The returned array must have the + same dimensionality as the input array. + Parameters + ---------- + arr + The input array. + new_shape + The shape of the new `xarray.DataArray`, must be the same length as arr.shape. + """ + + from scipy.interpolate import RegularGridInterpolator # noqa: PLC0415 + + if arr.ndim != len(new_shape): + raise ValueError( + f"The number of dimensions must match the input array. (original: {arr.ndim}, new: {len(new_shape)})" + ) + + old_grids = tuple(np.linspace(0, 1, size) for size in arr.shape) + new_grids = tuple(np.linspace(0, 1, size) for size in new_shape) + mesh = np.meshgrid(*new_grids, indexing="ij") + coords = np.stack(mesh, axis=-1) + + return RegularGridInterpolator(old_grids, arr, bounds_error=False, fill_value=0)( + coords + ) + + @xr.register_dataarray_accessor("epoch") class EpochAccessor: def __init__(self, xarray_obj: xr.DataArray): self._obj = xarray_obj - def plot(self, *args, **kwargs) -> DataArrayPlotAccessor: + def plot(self, hist=False, *args, **kwargs) -> DataArrayPlotAccessor: """ Builds upon `xarray.DataArray.plot` while changing some of its default behaviours. @@ -29,24 +62,31 @@ def plot(self, *args, **kwargs) -> DataArrayPlotAccessor: Parameters ---------- + hist + If ``True``, will plot a histogram regardless of dimensionality (default = ``False``). args Positional arguments passed to `xarray.DataArray.plot`. kwargs Keyword arguments passed to `xarray.DataArray.plot`. """ dims = self._obj.dims - is_not_2d_data = len(dims) != 2 - is_time_dim_present = "time" in dims - is_x_or_y_specified_in_kwargs = "x" in kwargs or "y" in kwargs - if is_not_2d_data or is_time_dim_present or is_x_or_y_specified_in_kwargs: + if hist: + return self._obj.plot.hist(*args, **kwargs) + + if len(dims) == 1: return self._obj.plot(*args, **kwargs) - updated_kwargs = dict(kwargs) - updated_kwargs.setdefault("x", dims[0]) - updated_kwargs.setdefault("y", dims[1]) + if len(dims) == 2: + updated_kwargs = dict(kwargs) + updated_kwargs.setdefault("x", dims[0]) + updated_kwargs.setdefault("y", dims[1]) + return self._obj.plot(*args, **updated_kwargs) + + if len(dims) == 3: + return voxel_plot(self._obj, *args, **kwargs) - return self._obj.plot(*args, **updated_kwargs) + return self._obj.plot(*args, **kwargs) def animate(self, *args, **kwargs) -> FuncAnimation: """Generate animations of Epoch data. @@ -72,3 +112,115 @@ def animate(self, *args, **kwargs) -> FuncAnimation: anim.show = MethodType(show, anim) return anim + + def resize( + self, + new_shape, + ) -> xr.DataArray: + """ + Resizes a `xarray.DataArray` to another shape. The returned array must have the + same dimensionality as the input array. + + Parameters + ---------- + new_shape + The shape of the new `xarray.DataArray`, must be the same length as self.shape. + """ + + da = self._obj + + # Create resized data and Dataset + data_resized = _resize_ndarray(da.values, new_shape) + da_resized = da.copy() + da_resized = xr.DataArray( + data=data_resized, + dims=da.dims, + attrs=da.attrs, + ) + + original_cell_size_da = [] + + # Resize coordinates and add to DataArray + for i in range(len(da_resized.dims)): + coord = list(da_resized.dims)[i] + + if coord.endswith("_mid"): + # If the coordinate is a midpoint coordinate, care must be taken to resize correctly + vertex_coord = _recover_vertex_coord(da[coord]) + vertex_coord_resized = np.linspace( + vertex_coord[0], vertex_coord[-1], new_shape[i] + 1 + ) + # Turn the vertex coord back into a midpoint coord + da_resized[coord] = ( + (np.roll(vertex_coord_resized, 1) + vertex_coord_resized) / 2 + )[1:] + else: + # If not, the coordinate can be simply resized + coord_min = da[coord][0].values + coord_max = da[coord][-1].values + da_resized[coord] = np.linspace(coord_min, coord_max, new_shape[i]) + + da_resized[coord].attrs = da[coord].attrs + + # Add original information as attributes + if "original_shape" not in da.attrs: + da_resized[coord].attrs["original_size"] = da.shape[i] + original_cell_size = float((da[coord][1] - da[coord][0]).values) + original_cell_size_da.append(original_cell_size) + da_resized[coord].attrs["original_cell_size"] = original_cell_size + + if "original_shape" not in da.attrs: + da_resized.attrs["original_shape"] = da.shape + da_resized.attrs["original_cell_size"] = tuple(original_cell_size_da) + + return da_resized + + def limit( + self, + limits, + drop: bool = True, + ) -> xr.DataArray: + """ + Drops values outside the specified limits. + + Parameters + ---------- + limits + Array-like list of limits. + drop + Bool specifying whether to drop the co-ordinates outside the limits (default = True). + """ + da = self._obj + limits = list(limits) + + # List of dimension names + dims = da.dims + + original_lims_da = [] + for i in range(len(dims)): + limits[i] = list(limits[i]) + + # Find the original limits of the dataarray + original_lims = ( + float(da[dims[i]].values[0]), + float(da[dims[i]].values[-1]), + ) + original_lims_da.append(original_lims) + if "original_lims" not in da[dims[i]].attrs: + da[dims[i]].attrs["original_lims"] = original_lims + + # If None is passed into limit, will assume the existing limit + if limits[i][0] is None: + limits[i][0] = original_lims[0] + if limits[i][-1] is None: + limits[i][-1] = original_lims[1] + + # Limit dataarray + da = da.where(da[dims[i]] >= limits[i][0], drop=drop).where( + da[dims[i]] <= limits[i][1], drop=drop + ) + + if "original_lims" not in da.attrs: + da.attrs["original_lims"] = tuple(original_lims_da) + + return da diff --git a/src/sdf_xarray/plotting.py b/src/sdf_xarray/plotting.py index 723b6e4..72a582c 100644 --- a/src/sdf_xarray/plotting.py +++ b/src/sdf_xarray/plotting.py @@ -3,7 +3,7 @@ import warnings from collections.abc import Callable from dataclasses import dataclass -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Literal import numpy as np import xarray as xr @@ -11,6 +11,7 @@ if TYPE_CHECKING: import matplotlib.pyplot as plt from matplotlib.animation import FuncAnimation + from matplotlib.colors import ListedColormap @dataclass @@ -51,11 +52,68 @@ def get_frame_title( t_axis_units_formatted = f" [{t_axis_units}]" if t_axis_units else "" title_t_axis = f"{data[t].long_name} = {t_axis_value:.2e}{t_axis_units_formatted}" - # Adds sdf name to the title, if specifed + # Adds sdf name to the title, if specified title_sdf = f", {frame:04d}.sdf" if display_sdf_name else "" return f"{title_custom}{title_t_axis}{title_sdf}" +def get_axis_label(dim: xr.DataArray) -> str: + """Formats the axes label for a given dim in the form of ``dim.long_name [dim.units]``.""" + return f"{dim.long_name} [{dim.units}]" + + +def _recover_vertex_coord(w_mid: xr.DataArray) -> np.ndarray: + """Takes a midpoint coordinate, returns a vertex coordinate.""" + w_size = w_mid.size + dw = w_mid[1] - w_mid[0] + w = np.zeros(w_mid.size + 1) + w[:w_size] = w_mid - dw / 2 + w[w_size] = w[w_size - 1] + dw + return w + + +def shift_cmap( + cmap: str, vmin: float, vmax: float, vcenter: float, N: int = 1024 +) -> ListedColormap: + """ + Create a new colormap where the visual center of the original + colormap is shifted to a specific data value. + + Parameters + ---------- + cmap + The name of the original colormap (e.g., 'viridis') or the colormap object itself. + vmin + The minimum value of your data range. + vmax + The maximum value of your data range. + vcenter + The data value that should map to the visual midpoint (0.5) of the colormap. + N + The number of interpolation steps (color bins) in the new colormap. + + Returns + ------- + The newly constructed, shifted colormap. + """ + import matplotlib.colors as mc # noqa: PLC0415 + import matplotlib.pyplot as plt # noqa: PLC0415 + + # get the original colourmap + if type(cmap) is str: + cmap = plt.get_cmap(cmap) + + midpoint = (vcenter - vmin) / (vmax - vmin) + lower_size = int(N * midpoint) + upper_size = N - lower_size + + bottom_colors = cmap(np.linspace(0.0, 0.5, lower_size)) + top_colors = cmap(np.linspace(0.5, 1.0, upper_size)) + new_colors = np.vstack((bottom_colors, top_colors)) + + return mc.ListedColormap(new_colors, name=f"shifted_{cmap.name}") + + def calculate_window_boundaries( data: xr.DataArray, xlim: tuple[float, float] | None = None, @@ -63,7 +121,7 @@ def calculate_window_boundaries( t: str = "time", ) -> np.ndarray: """Calculate the boundaries a moving window frame. If the user specifies xlim, this will - be used as the initial boundaries and the window will move along acordingly. + be used as the initial boundaries and the window will move along accordingly. Parameters ---------- @@ -94,7 +152,7 @@ def calculate_window_boundaries( window_boundaries[i, 0] = x_grid_non_nan[0] - x_half_cell window_boundaries[i, 1] = x_grid_non_nan[-1] + x_half_cell - # User's choice for initial window edge supercedes the one calculated + # User's choice for initial window edge supersedes the one calculated if xlim is not None: window_boundaries = window_boundaries + xlim - window_boundaries[0] return window_boundaries @@ -135,7 +193,133 @@ def _set_axes_labels(ax: plt.Axes, axis_kwargs: dict) -> None: ax.set_ylabel(axis_kwargs["ylabel"]) -def _setup_2d_plot( +def voxel_plot( + da: xr.DataArray, + ax: plt.Axes | None = None, + vmin: float | None = None, + vmax: float | None = None, + vcenter: float | None = None, + mask: np.ndarray[bool] | None = None, + xlim: tuple[float | None, float | None] = (None, None), + ylim: tuple[float | None, float | None] = (None, None), + zlim: tuple[float | None, float | None] = (None, None), + aspect: Literal["equal", "auto"] | tuple[float, float, float] = "equal", + elev: float = 30, + azim: float = -60, + cmap: str = "viridis", + cbar_scale: float = 0.9, + **kwargs, +) -> tuple[plt.Figure, plt.Axes]: + """ + Plot 3-dimensional data as voxels. + + Parameters + ---------- + da + DataArray to be plotted. + ax + Matplotlib axes on which to plot (This must use a 3d projection). + vmin + Minimum value. If `mask` is not stated, will be used to define the mask. + vmax + Maximum value. If `mask` is not stated, will be used to define the mask. + vcenter + Center value of the colourmap. Useful for diverging colourmaps with non-symmetrical data. + mask + Array of bools specifying which cells to show. Must be same size as ``da`` + xlim, ylim, zlim + Sets the limits of the plot. + aspect + Aspect ratio of the plot. "equal", "auto" or list of floats. (default = "equal") + elev + Elevation angle in degrees. (default = 30) + azim + Azimuthal angle in degrees. (default = -60) + cmap + Colourmap (default = "viridis") + cbar_scale + Vertical scale of the colorbar (default = 0.9) + """ + import matplotlib.pyplot as plt # noqa: PLC0415 + + warnings.warn( + "Voxel plots can be extremely computationally expensive and may take longer than expected to plot.", + stacklevel=2, + ) + + if ax is None: + fig, ax = plt.subplots(figsize=(8, 6), subplot_kw={"projection": "3d"}) + else: + fig = ax.get_figure() + + # Limit arrays based on axis limits + da = da.epoch.limit((xlim, ylim, zlim)) + + dims = da.dims + + # Create W_Grid from W_Grid_mid coords + x = _recover_vertex_coord(da[dims[0]]) + y = _recover_vertex_coord(da[dims[1]]) + z = _recover_vertex_coord(da[dims[2]]) + + # Create mesh + x_mesh, y_mesh, z_mesh = np.meshgrid(x, y, z, indexing="ij") + + if vmin is None: + vmin = np.min(da.values) + if vmax is None: + vmax = np.max(da.values) + + # Mask out data + if mask is None: + mask = (da >= vmin) * (da <= vmax) + + # Plot the data array + ax.view_init(elev, azim) + + # Set axis labels + ax.set_xlabel(get_axis_label(da[dims[0]])) + ax.set_ylabel(get_axis_label(da[dims[1]])) + ax.set_zlabel(get_axis_label(da[dims[2]])) + + # Find and set axis limits + xlim, ylim, zlim = [ + (data.min() if low is None else low, data.max() if high is None else high) + for (low, high), data in zip([xlim, ylim, zlim], [x, y, z]) + ] + + ax.set_xlim(xlim) + ax.set_ylim(ylim) + ax.set_zlim(zlim) + + # Compute and set the box aspect ratio + if aspect == "equal": + box_aspect = (1, 1, 1) + elif aspect == "auto": + box_aspect = (xlim[1] - xlim[0], ylim[1] - ylim[0], zlim[1] - zlim[0]) + else: + box_aspect = aspect + + ax.set_box_aspect(box_aspect) + + # Colour bar and colour map + if vcenter is not None: + cmap = shift_cmap(cmap, vmin, vmax, vcenter) + norm = plt.Normalize(vmin=vmin, vmax=vmax) + cmap = plt.get_cmap(cmap) + colours = cmap(norm(da)) + + sm = plt.cm.ScalarMappable(cmap=cmap, norm=norm) + sm.set_array([]) # Required for the colorbar to function + cbar_label = get_axis_label(da) + fig.colorbar(sm, ax=ax, label=cbar_label, shrink=cbar_scale, aspect=20 * cbar_scale) + + ax.voxels(x_mesh, y_mesh, z_mesh, mask, facecolors=colours, **kwargs) + + return fig, ax + + +def _setup_line_plot( data: xr.DataArray, ax: plt.Axes, coord_names: list[str], @@ -145,7 +329,7 @@ def _setup_2d_plot( max_percentile: float, t: str, ) -> tuple[float, float]: - """Setup 2D plot initialization.""" + """Line animation initialization.""" kwargs.setdefault("x", coord_names[0]) @@ -161,7 +345,7 @@ def _setup_2d_plot( return global_min, global_max -def _setup_3d_plot( +def _setup_pcolormesh_plot( data: xr.DataArray, ax: plt.Axes, coord_names: list[str], @@ -172,7 +356,47 @@ def _setup_3d_plot( max_percentile: float, t: str, ) -> None: - """Setup 3D plot initialization.""" + """pcolormesh animation initialization.""" + import matplotlib.pyplot as plt # noqa: PLC0415 + + if "norm" not in kwargs: + global_min, global_max = compute_global_limits( + data, min_percentile, max_percentile + ) + kwargs["norm"] = plt.Normalize(vmin=global_min, vmax=global_max) + + kwargs["add_colorbar"] = False + kwargs.setdefault("x", coord_names[0]) + kwargs.setdefault("y", coord_names[1]) + + argmin_time = np.unravel_index(np.argmin(data.values), data.shape)[0] + plot = data.isel({t: argmin_time}).plot(ax=ax, **kwargs) + kwargs["cmap"] = plot.cmap + + _set_axes_labels(ax, axis_kwargs) + + if kwargs_original.get("add_colorbar", True): + long_name = data.attrs.get("long_name") + units = data.attrs.get("units") + fig = plot.get_figure() + fig.colorbar(plot, ax=ax, label=f"{long_name} [{units}]") + + +def _setup_voxel_plot( + data: xr.DataArray, + ax: plt.Axes, + coord_names: list[str], + kwargs: dict, + kwargs_original: dict, + axis_kwargs: dict, + min_percentile: float, + max_percentile: float, + t: str, +) -> None: + """ + Voxel animation initialization. + NOTE: this function exists for completeness, voxel plots don't work in animations yet + """ import matplotlib.pyplot as plt # noqa: PLC0415 if "norm" not in kwargs: @@ -262,7 +486,7 @@ def _generate_animation( global_min = global_max = None if data.ndim == 2: - global_min, global_max = _setup_2d_plot( + global_min, global_max = _setup_line_plot( data=data, ax=ax, coord_names=coord_names, @@ -273,7 +497,7 @@ def _generate_animation( t=t, ) elif data.ndim == 3: - _setup_3d_plot( + _setup_pcolormesh_plot( data=data, ax=ax, coord_names=coord_names, @@ -284,6 +508,19 @@ def _generate_animation( max_percentile=max_percentile, t=t, ) + elif data.ndim == 4: + raise NotImplementedError("Voxel animations are not currently supported.") + # _setup_voxel_plot( + # data=data, + # ax=ax, + # coord_names=coord_names, + # kwargs=kwargs, + # kwargs_original=kwargs_original, + # axis_kwargs=axis_kwargs, + # min_percentile=min_percentile, + # max_percentile=max_percentile, + # t=t, + # ) ax.set_title(get_frame_title(data, 0, display_sdf_name, title, t)) diff --git a/tests/test_epoch_dataarray_accessor.py b/tests/test_epoch_dataarray_accessor.py index d9b6513..4ec4249 100644 --- a/tests/test_epoch_dataarray_accessor.py +++ b/tests/test_epoch_dataarray_accessor.py @@ -7,7 +7,8 @@ import pytest import xarray as xr from matplotlib.animation import PillowWriter -from matplotlib.container import BarContainer +from matplotlib.colors import ListedColormap +from mpl_toolkits.mplot3d import Axes3D from packaging.version import Version import sdf_xarray as sdfxr @@ -25,11 +26,10 @@ TEST_FILES_DIR_3D = download.fetch_dataset("test_files_3D") -@pytest.fixture -def subplots(): - fig, ax = plt.subplots() - yield (fig, ax) - plt.close(fig) +@pytest.fixture(autouse=True) +def close_figs(): + yield + plt.close("all") def test_animation_accessor(): @@ -245,21 +245,21 @@ def test_compute_global_limits_NaNs(): assert result_max == pytest.approx(expected_result_max, abs=1e-1) -def test_epoch_plot_simple_1d_dataset(subplots): +def test_epoch_plot_simple_1d_dataset(): with xr.open_mfdataset( TEST_FILES_DIR_1D.glob("*.sdf"), compat="no_conflicts", join="outer", preprocess=SDFPreprocess(), ) as ds: - _, ax = subplots + _, ax = plt.subplots() ds["Derived_Number_Density_electron"].isel(time=0).epoch.plot(ax=ax) assert len(ax.lines) == 1 assert ax.get_xlabel() == "X [m]" -def test_epoch_plot_simple_2d_dataset(subplots): +def test_epoch_plot_simple_2d_dataset(): with xr.open_mfdataset( TEST_FILES_DIR_2D_MW.glob("*.sdf"), preprocess=SDFPreprocess(), @@ -267,7 +267,7 @@ def test_epoch_plot_simple_2d_dataset(subplots): compat="no_conflicts", join="outer", ) as ds: - _, ax = subplots + _, ax = plt.subplots() ds["Derived_Number_Density_electron"].isel(time=0).epoch.plot(ax=ax) assert len(ax.collections) > 0 @@ -275,9 +275,9 @@ def test_epoch_plot_simple_2d_dataset(subplots): assert ax.get_ylabel() == "Y [m]" -def test_epoch_plot_simple_3d_dataset_slice(subplots): +def test_epoch_plot_simple_3d_dataset_slice(): with xr.open_dataset(TEST_FILES_DIR_3D / "0001.sdf") as ds: - _, ax = subplots + _, ax = plt.subplots() ds["Derived_Number_Density_Electron"].isel(Z_Grid_mid=0).epoch.plot(ax=ax) assert len(ax.collections) > 0 @@ -285,7 +285,7 @@ def test_epoch_plot_simple_3d_dataset_slice(subplots): assert ax.get_ylabel() == "Y [m]" -def test_epoch_plot_flips_axis_order_for_2d_data(subplots): +def test_epoch_plot_flips_axis_order_for_2d_data(): with xr.open_mfdataset( TEST_FILES_DIR_2D_MW.glob("*.sdf"), preprocess=SDFPreprocess(), @@ -293,14 +293,14 @@ def test_epoch_plot_flips_axis_order_for_2d_data(subplots): compat="no_conflicts", join="outer", ) as ds: - _, ax = subplots + _, ax = plt.subplots() ds["Derived_Number_Density_electron"].isel(time=0).epoch.plot(ax=ax) assert ax.get_xlabel() == "X [m]" assert ax.get_ylabel() == "Y [m]" -def test_epoch_plot_flips_axis_order_for_2d_data_with_additional_params(subplots): +def test_epoch_plot_flips_axis_order_for_2d_data_with_additional_params(): with xr.open_mfdataset( TEST_FILES_DIR_2D_MW.glob("*.sdf"), preprocess=SDFPreprocess(), @@ -308,7 +308,7 @@ def test_epoch_plot_flips_axis_order_for_2d_data_with_additional_params(subplots compat="no_conflicts", join="outer", ) as ds: - _, ax = subplots + _, ax = plt.subplots() ds["Derived_Number_Density_electron"].isel(time=0).epoch.plot( ax=ax, xlim=(0.5, 1.0), @@ -321,17 +321,240 @@ def test_epoch_plot_flips_axis_order_for_2d_data_with_additional_params(subplots assert ax.get_ylim() == pytest.approx((0.0, 0.5), abs=1e-2) -def test_epoch_plot_flips_axis_order_for_2d_data_but_not_when_time_dim_present( - subplots, -): - with xr.open_mfdataset( - TEST_FILES_DIR_2D_MW.glob("*.sdf"), - preprocess=SDFPreprocess(), - combine="nested", - compat="no_conflicts", - join="outer", - ) as ds: - _, ax = subplots - plot = ds["Derived_Number_Density_electron"].epoch.plot(ax=ax) +def _make_3d_da(shape=(4, 5, 6)): + """Small synthetic 3-D DataArray with the metadata voxel_plot expects.""" + nx, ny, nz = shape + rng = np.random.default_rng(42) + data = rng.uniform(0.0, 1.0, (nx, ny, nz)).astype(np.float64) + return xr.DataArray( + data, + dims=["X_Grid_mid", "Y_Grid_mid", "Z_Grid_mid"], + coords={ + "X_Grid_mid": xr.Variable( + "X_Grid_mid", + np.linspace(0.0, 1e-5, nx), + attrs={"long_name": "X", "units": "m"}, + ), + "Y_Grid_mid": xr.Variable( + "Y_Grid_mid", + np.linspace(0.0, 2e-5, ny), + attrs={"long_name": "Y", "units": "m"}, + ), + "Z_Grid_mid": xr.Variable( + "Z_Grid_mid", + np.linspace(0.0, 3e-5, nz), + attrs={"long_name": "Z", "units": "m"}, + ), + }, + attrs={"long_name": "Test Density", "units": "1/m^3"}, + ) + + +def test_recover_vertex_coord_size(): + mid = xr.DataArray(np.linspace(0.5, 4.5, 5)) + vertex = sxp._recover_vertex_coord(mid) + assert vertex.size == mid.size + 1 + + +def test_recover_vertex_coord_values(): + mid = xr.DataArray(np.array([0.5, 1.5, 2.5])) + vertex = sxp._recover_vertex_coord(mid) + np.testing.assert_allclose(vertex, [0.0, 1.0, 2.0, 3.0]) + + +def test_shift_cmap_returns_listed_colormap(): + result = sxp.shift_cmap("RdBu", vmin=-1.0, vmax=1.0, vcenter=0.0) + assert isinstance(result, ListedColormap) + + +def test_shift_cmap_total_colors(): + result = sxp.shift_cmap("RdBu", vmin=-1.0, vmax=1.0, vcenter=0.0, N=100) + assert len(result.colors) == 100 + + +def test_shift_cmap_asymmetric_center(): + result = sxp.shift_cmap("viridis", vmin=0.0, vmax=1.0, vcenter=0.25, N=200) + assert isinstance(result, ListedColormap) + assert len(result.colors) == 200 + + +def test_voxel_plot_returns_fig_and_ax(): + da = _make_3d_da() + _, ax = sxp.voxel_plot(da) + assert isinstance(ax, Axes3D) + + +def test_voxel_plot_accepts_axes(): + da = _make_3d_da() + _, ax = plt.subplots(figsize=(8, 6), subplot_kw={"projection": "3d"}) + sxp.voxel_plot(da, ax=ax) + assert isinstance(ax, Axes3D) + + +def test_voxel_plot_axis_labels(): + da = _make_3d_da() + _, ax = sxp.voxel_plot(da) + assert ax.get_xlabel() == "X [m]" + assert ax.get_ylabel() == "Y [m]" + assert ax.get_zlabel() == "Z [m]" + + +def test_voxel_plot_default_axis_limits(): + da = _make_3d_da() + _, ax = sxp.voxel_plot(da) + xlim = ax.get_xlim() + ylim = ax.get_ylim() + zlim = ax.get_zlim() + assert xlim[0] < xlim[1] + assert ylim[0] < ylim[1] + assert zlim[0] < zlim[1] + + +def test_voxel_plot_with_xlim_ylim_zlim(): + da = _make_3d_da() + x0, x1 = 2e-6, 8e-6 + y0, y1 = 2e-6, 1.8e-5 + z0, z1 = 5e-6, 2.5e-5 + _, ax = sxp.voxel_plot(da, xlim=(x0, x1), ylim=(y0, y1), zlim=(z0, z1)) + assert ax.get_xlim() == pytest.approx((x0, x1), rel=0.1) + assert ax.get_ylim() == pytest.approx((y0, y1), rel=0.1) + assert ax.get_zlim() == pytest.approx((z0, z1), rel=0.1) + + +def test_voxel_plot_with_explicit_vmin_vmax(): + da = _make_3d_da() + _, ax = sxp.voxel_plot(da, vmin=0.2, vmax=0.8) + assert isinstance(ax, Axes3D) + + +def test_voxel_plot_with_vcenter(): + da = _make_3d_da() + _, ax = sxp.voxel_plot(da, cmap="RdBu", vcenter=1e-2) + assert isinstance(ax, Axes3D) + - assert type(plot[2]) is BarContainer +def test_voxel_plot_with_custom_mask(): + da = _make_3d_da() + mask = da.values > 0.5 + _, ax = sxp.voxel_plot(da, mask=mask) + assert isinstance(ax, Axes3D) + + +def test_voxel_plot_aspect_auto(): + da = _make_3d_da() + _, ax = sxp.voxel_plot(da, aspect="auto") + box = ax.get_box_aspect() + # voxel_plot uses vertex coords (half-cell beyond midpoints) for axis ranges; + # matplotlib normalizes the absolute values, so only ratios are stable + x_vert = sxp._recover_vertex_coord(da["X_Grid_mid"]) + y_vert = sxp._recover_vertex_coord(da["Y_Grid_mid"]) + z_vert = sxp._recover_vertex_coord(da["Z_Grid_mid"]) + x_range = x_vert.max() - x_vert.min() + y_range = y_vert.max() - y_vert.min() + z_range = z_vert.max() - z_vert.min() + assert box[1] / box[0] == pytest.approx(y_range / x_range, rel=0.05) + assert box[2] / box[0] == pytest.approx(z_range / x_range, rel=0.05) + + +def test_voxel_plot_aspect_custom_tuple(): + da = _make_3d_da() + _, ax = sxp.voxel_plot(da, aspect=(1.0, 2.0, 3.0)) + box = ax.get_box_aspect() + assert box[1] / box[0] == pytest.approx(2.0, rel=0.01) + assert box[2] / box[0] == pytest.approx(3.0, rel=0.01) + + +def test_epoch_plot_dispatches_to_voxel_for_3d_spatial_data(): + with xr.open_dataset(TEST_FILES_DIR_3D / "0001.sdf") as ds: + da = ds["Derived_Number_Density_Electron"].isel( + X_Grid_mid=slice(0, 4), + Y_Grid_mid=slice(0, 4), + Z_Grid_mid=slice(0, 4), + ) + _, ax = da.epoch.plot() + assert isinstance(ax, Axes3D) + assert ax.get_xlabel() == "X [m]" + assert ax.get_ylabel() == "Y [m]" + assert ax.get_zlabel() == "Z [m]" + + +def test_resize_basic(): + da = _make_3d_da(shape=(8, 10, 12)) + da_small = da.epoch.resize((4, 5, 6)) + assert da_small.shape == (4, 5, 6) + assert da_small.dims == da.dims + + +def test_resize_stores_original_shape_attrs(): + da = _make_3d_da(shape=(8, 10, 12)) + da_small = da.epoch.resize((4, 5, 6)) + assert da_small.attrs["original_shape"] == (8, 10, 12) + + +def test_resize_coord_range_preserved(): + da = _make_3d_da(shape=(8, 10, 12)) + da_small = da.epoch.resize((4, 5, 6)) + # For _mid coords, resize preserves the vertex (cell-edge) range rather than + # the midpoint values; check that the outer vertices are unchanged + orig_vertex = sxp._recover_vertex_coord(da["X_Grid_mid"]) + new_vertex = sxp._recover_vertex_coord(da_small["X_Grid_mid"]) + np.testing.assert_allclose(float(new_vertex[0]), float(orig_vertex[0]), rtol=1e-6) + np.testing.assert_allclose(float(new_vertex[-1]), float(orig_vertex[-1]), rtol=1e-6) + + +def test_resize_wrong_ndim_raises(): + da = _make_3d_da() + with pytest.raises(ValueError, match="dimensions"): + da.epoch.resize((4, 5)) + + +def test_resize_with_mid_coord(): + da = _make_3d_da(shape=(6, 8, 10)) + da_small = da.epoch.resize((3, 4, 5)) + assert da_small.shape == (3, 4, 5) + for dim in da_small.dims: + assert da_small[dim].size == da_small.sizes[dim] + + +def test_limit_reduces_coord_range(): + da = _make_3d_da() + x_mid = 5e-6 + da_lim = da.epoch.limit(((0.0, x_mid), (None, None), (None, None))) + assert float(da_lim["X_Grid_mid"][-1]) <= x_mid + 1e-7 + + +def test_limit_with_none_uses_existing_bounds(): + da = _make_3d_da() + da_lim = da.epoch.limit(((None, None), (None, None), (None, None))) + assert da_lim.shape == da.shape + + +def test_limit_stores_original_lims_on_coord(): + da = _make_3d_da() + x_min = float(da["X_Grid_mid"][0]) + x_max = float(da["X_Grid_mid"][-1]) + da_lim = da.epoch.limit(((0.0, 5e-6), (None, None), (None, None))) + assert da_lim["X_Grid_mid"].attrs["original_lims"] == pytest.approx( + (x_min, x_max), rel=1e-6 + ) + + +def test_limit_no_drop(): + da = _make_3d_da() + da_lim = da.epoch.limit(((0.0, 5e-6), (None, None), (None, None)), drop=False) + assert da_lim.shape == da.shape + assert np.any(np.isnan(da_lim.values)) + + +def test_animate_raises_for_4d_data(): + da = xr.DataArray( + np.zeros((3, 4, 5, 6)), + dims=["time", "X_Grid_mid", "Y_Grid_mid", "Z_Grid_mid"], + coords={ + "time": xr.Variable( + "time", [1.0, 2.0, 3.0], attrs={"long_name": "Time", "units": "s"} + ) + }, + ) + with pytest.raises(NotImplementedError, match="Voxel animations"): + da.epoch.animate() diff --git a/uv.lock b/uv.lock index 99d7307..1a4d309 100644 --- a/uv.lock +++ b/uv.lock @@ -2246,6 +2246,134 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/1c/1dbe51782c0e1e9cfce1d1004752672d2d4629ea46945d19d731ad772b3b/ruff-0.14.11-py3-none-win_arm64.whl", hash = "sha256:649fb6c9edd7f751db276ef42df1f3df41c38d67d199570ae2a7bd6cbc3590f0", size = 12938644, upload-time = "2026-01-08T19:11:50.027Z" }, ] +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.12'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, + { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, + { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, + { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, + { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, + { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, + { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, + { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "scipy" +version = "1.18.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.12'", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/25/c2700dfaf6442b4effaa91af24ebce5dc9d31bb4a69706313aae70d72cd0/scipy-1.18.0.tar.gz", hash = "sha256:67b2ad2ad54c72ca6d04975a9b2df8c3638c34ddd5b28738e94fc2b57929d378", size = 30774447, upload-time = "2026-06-19T15:01:43.456Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/19/ca10ead60b0acc80b2b833c2c4a4f2ff753d0f58b811f70d911c7e94a25c/scipy-1.18.0-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:7bd21faaf5a1a3b2eff922d02db5f191b99a6518db9078a8fb23169f6d22259a", size = 31056519, upload-time = "2026-06-19T14:59:45.203Z" }, + { url = "https://files.pythonhosted.org/packages/96/72/1e6442a00cd2924d361aa1b642ab6373ec35c6fabf311a760be9f76e0f13/scipy-1.18.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:265915e79107de9f946b855e50d7470d5893ec3f54b342e1aa6201cbdcd8bb6b", size = 28681889, upload-time = "2026-06-19T14:59:48.103Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2d/11dd93d21e147a73ba22bd75c0b9208d3a2e0ec76d53170ce7d9029b1015/scipy-1.18.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9ab7b758be6940954a713ee466e2043e9f6e2ed965c1fce5c91039f4be3d90a9", size = 20423580, upload-time = "2026-06-19T14:59:50.665Z" }, + { url = "https://files.pythonhosted.org/packages/9c/01/93552f75e0d2a7dd115a45e59209c51e8d514daff02fc887d2623be06fe1/scipy-1.18.0-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:97b6cddaaee0a779ef6b5ca83c9604b27cc16b2b8fc22c142652df8793319fb8", size = 23054441, upload-time = "2026-06-19T14:59:53.564Z" }, + { url = "https://files.pythonhosted.org/packages/3c/23/21f5e703643d66f21faa6b4c73195bfcad70c55efcb4f1ab327cd7c4101a/scipy-1.18.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:52a96e21517c7292375c0e27dd796a811f03fcea5fd4d108fdfea8145dcf17ab", size = 33968720, upload-time = "2026-06-19T14:59:56.415Z" }, + { url = "https://files.pythonhosted.org/packages/dd/aa/1b939f6c67ed68635bb538e6752d3dacc02f66535182e939a89581a44e9c/scipy-1.18.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f55797419e16e7f30cf88ffb3113ce0467f00cfe3f70d5c281730b21769bfc2", size = 35287115, upload-time = "2026-06-19T14:59:59.411Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ff/eec46be7e9234208f801062b53e1983085eddebd693f6c9bfb03b459830d/scipy-1.18.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ad033410e2e0672ffdc1042110cef20e1c46f8fd0616cee1d44d8d58fad8fc11", size = 35577989, upload-time = "2026-06-19T15:00:02.235Z" }, + { url = "https://files.pythonhosted.org/packages/84/ca/210d4759c7210bb7d269437421959b39a33434e2776b60c5cb8a763bb30a/scipy-1.18.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:4a55985d54c769c872e64b7f4c8a81cc30ef700cc04296abbbf3705439c126de", size = 37421717, upload-time = "2026-06-19T15:00:05.102Z" }, + { url = "https://files.pythonhosted.org/packages/2b/54/9a9edb45345bd6744da5ddfb6628e5d5185920494c6a67ec45b6381004cb/scipy-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:71ccc8faa2dd16ac310233203474a8b5cb67f10dedd54a3116d34943f4b19132", size = 36597428, upload-time = "2026-06-19T15:00:08.112Z" }, + { url = "https://files.pythonhosted.org/packages/99/0e/33f32a2a58987e26aec0f7df252cbbad1e90ae77bdbc76f40dd4ed0cf0ea/scipy-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:d88363fd9d8fbd3511bd273f1a49efb2a540773ddf92a91d57498ce7dd7f3e76", size = 24351481, upload-time = "2026-06-19T15:00:11.103Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9c0136c2de7ae0779b7b366447766cec6d9f0702c56bb8ffeb04c8fd3af4/scipy-1.18.0-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:09143f676d157d9f546d663504ef9c1becb819824f1afc018814176411942446", size = 31036107, upload-time = "2026-06-19T15:00:14.03Z" }, + { url = "https://files.pythonhosted.org/packages/02/73/0291a64843270f4efb86cdcf2ee0f2048631b65ec6b405398b2b4dbf11bf/scipy-1.18.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5efe260f69417b97ddae455bfb5a95e8359f7f66ad7fa9522a60feb66f169520", size = 28663303, upload-time = "2026-06-19T15:00:16.819Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0f/10ffa0b697a572f4e0d48b92a88895d366422f019f723e7e14a84c050dac/scipy-1.18.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:68363b7eaacd8b5dd426df56d782cc156468ac79a127a1b87ca597d6e2e82197", size = 20404960, upload-time = "2026-06-19T15:00:19.635Z" }, + { url = "https://files.pythonhosted.org/packages/7e/d2/e896cea21ba8edd6c81d4c55b1ffcc717e79698dcbebf9641b4cfb4c6622/scipy-1.18.0-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:c5557d8be5da8e41353fcd4d21491fdbab83b062fc579e94dc09a7c8ab4f669b", size = 23034074, upload-time = "2026-06-19T15:00:22.107Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b2/e83ea34279a52c03374477c74006256ec78df65fc877baa4617d6de1d202/scipy-1.18.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d13bca67c096d89fb95ced0d8921807300fce0275643aef9533cc63a0773468", size = 33942038, upload-time = "2026-06-19T15:00:24.964Z" }, + { url = "https://files.pythonhosted.org/packages/f6/af/e8fe5fb136f51e2b01678b92cb4106d10d8cd68ec147ead2e7cb0ac75398/scipy-1.18.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a46f9273dbd0eb1cefba61c9b8648b4dfe3cbc14a080176f9a73e44b8336dc7f", size = 35266390, upload-time = "2026-06-19T15:00:28.059Z" }, + { url = "https://files.pythonhosted.org/packages/3a/49/2c5cbb907b56695fc67517811d1db234dfd83381a84814ec220aded2794d/scipy-1.18.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5aba46108853ddfc77906b6557aac839d2b52e900c1d72a1180adaaab58d265f", size = 35551324, upload-time = "2026-06-19T15:00:31.014Z" }, + { url = "https://files.pythonhosted.org/packages/bb/73/eda39f7a2d306ff0ffc574afd13c0bbb6d10a603d9a413998ee269487a80/scipy-1.18.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6f758e35f12757b5d95c00bc6de2438e229c2664b7a92e96f205959d9f2dfa4", size = 37404785, upload-time = "2026-06-19T15:00:34.072Z" }, + { url = "https://files.pythonhosted.org/packages/b7/d2/ae881ee28d014f38e0ccbfd974a06a919ba9af34f1f74bf42b5301891d63/scipy-1.18.0-cp313-cp313-win_amd64.whl", hash = "sha256:1afac4a847207c7ff8efd321734a50b06d0280b3b2a2c0fc2f413101747ad7c7", size = 36554943, upload-time = "2026-06-19T15:00:36.903Z" }, + { url = "https://files.pythonhosted.org/packages/70/3a/21154e2d54eb3639c6bf4dbae2e531c68356bfe95990daa30df33b30d556/scipy-1.18.0-cp313-cp313-win_arm64.whl", hash = "sha256:c5dbddf60e58c2312316d097271a8e73d40eaf2eabfa4d95ed7d3695bbf2ce7b", size = 24350911, upload-time = "2026-06-19T15:00:40.062Z" }, + { url = "https://files.pythonhosted.org/packages/78/b5/915a19b3de2f7430062b509653563db1633ddbb6f021b06731521115d4e2/scipy-1.18.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4c256ee70c0d1a8a2ace807e199ccd4e3f57037433842abb3fb36bc17eaa9578", size = 31036253, upload-time = "2026-06-19T15:00:43.216Z" }, + { url = "https://files.pythonhosted.org/packages/d7/88/b72def7262e150d16be13fca37a96481138d624e700340bc3362a7588929/scipy-1.18.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:2ef3abc54a4ffc53765374b0d5728532dfdd2585ed23f6b11c206a1f0b1b9af8", size = 28673758, upload-time = "2026-06-19T15:00:46.663Z" }, + { url = "https://files.pythonhosted.org/packages/91/02/2e636a61a525632c373cf6a9c24442a3ffb79e364d38e98b32042964ac32/scipy-1.18.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:f2a6af57bd9e4a75d70e4117e78a1bbee84f79ae3fbb6d0111005d6ebcc4cb8d", size = 20415514, upload-time = "2026-06-19T15:00:49.399Z" }, + { url = "https://files.pythonhosted.org/packages/c9/b6/2135974442f6aba159d9d39d774a1c8cb19947016725d69fecc685df45bf/scipy-1.18.0-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:3f1ac564d3bf6c03d861d2cd87a1bea0da2887136f7fb1bf519c05a8971452d6", size = 23034398, upload-time = "2026-06-19T15:00:51.941Z" }, + { url = "https://files.pythonhosted.org/packages/f6/e6/ba89ec5abf6ee9257c0d1ec985573f3ae32742c24bc03e016388a40b1b15/scipy-1.18.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40395a5fcd1abee49a5c7aaa98c29db393eedc835138560a588c47ec16156690", size = 33998032, upload-time = "2026-06-19T15:00:54.838Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c4/bc41eb19b0fd0db868f4132920879019318d80cc522ad8f2bca4611af808/scipy-1.18.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8ca01e8ae69f1b18e9a58d91afead31be3cef0dd905a10249dac559ee15460a0", size = 35283333, upload-time = "2026-06-19T15:00:58.152Z" }, + { url = "https://files.pythonhosted.org/packages/53/a4/cbdeef6eb3830a8462a9d4ada814de5fc984345cc9ecf17cbec51a036f1e/scipy-1.18.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7a7f3b01647384dbc3a711e8c6778e0aabbe93959249fef5c7393396bcac0867", size = 35610216, upload-time = "2026-06-19T15:01:01.155Z" }, + { url = "https://files.pythonhosted.org/packages/80/4d/b2b82502b65f661d1b789c1665dcdf315d5f12194e06fc0b37946294ebae/scipy-1.18.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6aa94e78ec192a30063a5e72e561c28af769dc311190b24fe91774eff1969709", size = 37418960, upload-time = "2026-06-19T15:01:04.155Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/902d836831474b0ab5a37d16404f7bc5fafd9efba632890e271ba952635f/scipy-1.18.0-cp314-cp314-win_amd64.whl", hash = "sha256:2d8bbdc6c817f5b4006a54d799d4f5bab6f910193cbb9a1ff310833d4d270f61", size = 37288845, upload-time = "2026-06-19T15:01:07.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/43/8d73b337a3bdb14daa0314f0434210747c02d79d729ce1777574a817dcf6/scipy-1.18.0-cp314-cp314-win_arm64.whl", hash = "sha256:18e9575f1569b2c54174e6159d32942e03731177f63dce7975f0a0c88d102f5b", size = 24988971, upload-time = "2026-06-19T15:01:11.076Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b4/f11918b0508a2787031a0499a03fbe3546f3bb5ca05d01038c45b278c09a/scipy-1.18.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f351e0dd702687d12a402b867a1b4146a256923e1c38317cbc472f6372b94707", size = 31399325, upload-time = "2026-06-19T15:01:13.723Z" }, + { url = "https://files.pythonhosted.org/packages/7b/d1/1f287b57c0ff0ee5185dff3946d92c8017d39b0e431f0ae79a3ff1859512/scipy-1.18.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7c7a51b33ce387193c97f228320cf8e87361daa1bba750638677729598b3e677", size = 29092110, upload-time = "2026-06-19T15:01:16.908Z" }, + { url = "https://files.pythonhosted.org/packages/ff/1a/7b74eb6c392fdcb27d414c0e7558a6d0231eb3b6d73571f479bb81ea8794/scipy-1.18.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:84031d7b052a54fae2f8632e0ec802073d385476eb9a63079bce6e23ef9283d4", size = 20833811, upload-time = "2026-06-19T15:01:20.488Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ad/f3941716320a7b9cb4d68734a903b45fe16eff5fb7da7e16f2e619304979/scipy-1.18.0-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:56abf29a7c067dde59be8b9a22d606a4ea1b2f2a4b756d9d903c62818f5dacce", size = 23396644, upload-time = "2026-06-19T15:01:23.364Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/1446b62ffe07f9719b7d9b1b6a4e05a772833ae8f441fe4c22c34c9b250f/scipy-1.18.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ad44305cfa24b1ba5803cbbebf033590ccbac1aa5d612d727b785325ab408b0", size = 34079318, upload-time = "2026-06-19T15:01:26.002Z" }, + { url = "https://files.pythonhosted.org/packages/56/3b/b87da667098bb470fa30c7011b0ba351ee976dd395c78798c66e941665a3/scipy-1.18.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:945c1761b93f38d7f99ae81ae80c63e621471608c7eeead563f6df025585cd58", size = 35324320, upload-time = "2026-06-19T15:01:28.881Z" }, + { url = "https://files.pythonhosted.org/packages/f8/a1/c7932f91909759b0267f75fdea34e91309f96b895757534b76a90b6b4344/scipy-1.18.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:1a4441f15d620578772a49e5ab48c0ee1f7a0220e387110283062729136b2553", size = 35699541, upload-time = "2026-06-19T15:01:31.968Z" }, + { url = "https://files.pythonhosted.org/packages/f7/86/5185061a1fcc41d18c5dc2463969b3a3964b31d9ac67b2fb05d4c7ff7670/scipy-1.18.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9aac6192fac56bf2ca534389d24623f07b39ff83317d58287285e7fbd622ff76", size = 37472480, upload-time = "2026-06-19T15:01:35.136Z" }, + { url = "https://files.pythonhosted.org/packages/31/8e/f04c68e39919a010d34f2ee1367fd705b0a25a02f609d755f0bfbc0a15fc/scipy-1.18.0-cp314-cp314t-win_amd64.whl", hash = "sha256:e40baea28ae7f5475c779741e2d90b1247c78531207b49c7030e698ff81cee3f", size = 37365390, upload-time = "2026-06-19T15:01:38.091Z" }, + { url = "https://files.pythonhosted.org/packages/d5/19/969dc072906c84dd0a3b05dcf57ea750936087d7873549e408b35cfc3f97/scipy-1.18.0-cp314-cp314t-win_arm64.whl", hash = "sha256:368e0a705903c466aa5f08eefb39e6b1b6b2d659e7352a31fd9e2438365be0f8", size = 25279661, upload-time = "2026-06-19T15:01:40.817Z" }, +] + [[package]] name = "sdf-xarray" source = { editable = "." } @@ -2281,6 +2409,8 @@ dev = [ { name = "pooch" }, { name = "pytest" }, { name = "ruff" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx" }, { name = "sphinx-argparse-cli" }, { name = "sphinx-autobuild" }, @@ -2299,6 +2429,8 @@ docs = [ { name = "pint" }, { name = "pint-xarray" }, { name = "pooch" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "sphinx" }, { name = "sphinx-argparse-cli" }, { name = "sphinx-autobuild" }, @@ -2316,8 +2448,12 @@ lint = [ test = [ { name = "dask", extra = ["complete"] }, { name = "matplotlib" }, + { name = "pint" }, + { name = "pint-xarray" }, { name = "pooch" }, { name = "pytest" }, + { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "scipy", version = "1.18.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "tqdm" }, ] @@ -2347,6 +2483,7 @@ dev = [ { name = "pooch", specifier = ">=1.8.2" }, { name = "pytest", specifier = ">=3.3.0" }, { name = "ruff" }, + { name = "scipy" }, { name = "sphinx", specifier = ">=5.3" }, { name = "sphinx-argparse-cli", specifier = ">=1.10.0" }, { name = "sphinx-autobuild", specifier = ">=2025.8.25" }, @@ -2365,6 +2502,7 @@ docs = [ { name = "pint" }, { name = "pint-xarray" }, { name = "pooch", specifier = ">=1.8.2" }, + { name = "scipy" }, { name = "sphinx", specifier = ">=5.3" }, { name = "sphinx-argparse-cli", specifier = ">=1.10.0" }, { name = "sphinx-autobuild", specifier = ">=2025.8.25" }, @@ -2380,8 +2518,11 @@ lint = [{ name = "ruff" }] test = [ { name = "dask", extras = ["complete"] }, { name = "matplotlib" }, + { name = "pint" }, + { name = "pint-xarray" }, { name = "pooch", specifier = ">=1.8.2" }, { name = "pytest", specifier = ">=3.3.0" }, + { name = "scipy" }, { name = "tqdm" }, ]