diff --git a/packages/esssans/docs/_static/thumbnails/skadi_detector_view_dark.svg b/packages/esssans/docs/_static/thumbnails/skadi_detector_view_dark.svg
new file mode 100644
index 000000000..ace5d5582
--- /dev/null
+++ b/packages/esssans/docs/_static/thumbnails/skadi_detector_view_dark.svg
@@ -0,0 +1,42 @@
+
+
+
diff --git a/packages/esssans/docs/_static/thumbnails/skadi_detector_view_light.svg b/packages/esssans/docs/_static/thumbnails/skadi_detector_view_light.svg
new file mode 100644
index 000000000..a097c3105
--- /dev/null
+++ b/packages/esssans/docs/_static/thumbnails/skadi_detector_view_light.svg
@@ -0,0 +1,42 @@
+
+
+
diff --git a/packages/esssans/docs/index.md b/packages/esssans/docs/index.md
index c05f86c99..1b4d61f7e 100644
--- a/packages/esssans/docs/index.md
+++ b/packages/esssans/docs/index.md
@@ -37,6 +37,11 @@
:::
+:::{grid-item-card} SKADI
+:link: user-guide/skadi/index.md
+
+:::
+
:::{grid-item-card} ISIS instruments
:link: user-guide/isis/index.md
diff --git a/packages/esssans/docs/user-guide/index.md b/packages/esssans/docs/user-guide/index.md
index 1433947b6..f661a3a17 100644
--- a/packages/esssans/docs/user-guide/index.md
+++ b/packages/esssans/docs/user-guide/index.md
@@ -7,6 +7,7 @@ maxdepth: 1
installation
loki/index
+skadi/index
isis/index
common/index
```
diff --git a/packages/esssans/docs/user-guide/skadi/index.md b/packages/esssans/docs/user-guide/skadi/index.md
new file mode 100644
index 000000000..19a51d85b
--- /dev/null
+++ b/packages/esssans/docs/user-guide/skadi/index.md
@@ -0,0 +1,31 @@
+# SKADI
+
+## Detector diagnostics
+
+::::{grid} 3
+
+:::{grid-item-card} Detector view
+:link: skadi-detector-view.ipynb
+:text-align: center
+
+```{image} ../../_static/thumbnails/skadi_detector_view_light.svg
+:class: only-light
+:width: 100%
+```
+
+```{image} ../../_static/thumbnails/skadi_detector_view_dark.svg
+:class: only-dark
+:width: 100%
+```
+
+:::
+
+::::
+
+```{toctree}
+---
+hidden:
+---
+
+skadi-detector-view
+```
diff --git a/packages/esssans/docs/user-guide/skadi/skadi-detector-view.ipynb b/packages/esssans/docs/user-guide/skadi/skadi-detector-view.ipynb
new file mode 100644
index 000000000..11a52d685
--- /dev/null
+++ b/packages/esssans/docs/user-guide/skadi/skadi-detector-view.ipynb
@@ -0,0 +1,74 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0",
+ "metadata": {},
+ "source": [
+ "# SKADI detector view\n",
+ "\n",
+ "This example uses the SKADI McStas workflow to load detector events and display them in an interactive instrument view. The reduced example file is downloaded on first use and cached by pooch."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import scippneutron as scn\n",
+ "\n",
+ "from ess.skadi import SkadiMcStasWorkflow\n",
+ "from ess.skadi.data import skadi_mcstas_sample\n",
+ "from ess.sans.types import Filename, RawDetector, SampleRun"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "workflow = SkadiMcStasWorkflow()\n",
+ "workflow[Filename[SampleRun]] = skadi_mcstas_sample()\n",
+ "detector = workflow.compute(RawDetector[SampleRun])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "detector_view = scn.instrument_view(\n",
+ " detector.bins.sum(), size=0.006, norm=\"log\", cbar=True\n",
+ ")\n",
+ "detector_view"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.11.15"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/packages/esssans/pyproject.toml b/packages/esssans/pyproject.toml
index 8a5cd1048..4331cdab2 100644
--- a/packages/esssans/pyproject.toml
+++ b/packages/esssans/pyproject.toml
@@ -32,6 +32,7 @@ dynamic = ["version"]
dependencies = [
"dask>=2022.1.0",
"graphviz>=0.20",
+ "h5py>=3.11",
"essreduce>=26.6.0",
"numpy>=1.26.4",
"pandas>=2.1.2",
diff --git a/packages/esssans/src/ess/sans/normalization.py b/packages/esssans/src/ess/sans/normalization.py
index cc32bdcbe..65ae6a649 100644
--- a/packages/esssans/src/ess/sans/normalization.py
+++ b/packages/esssans/src/ess/sans/normalization.py
@@ -27,6 +27,7 @@
Numerator,
Position,
ProcessedWavelengthBands,
+ RawDetector,
ReducedQ,
ReducedQxQy,
ReturnEvents,
@@ -90,6 +91,49 @@ def solid_angle(
)
+def rectangular_pixel_solid_angle(
+ detector: RawDetector[RunType],
+ sample_position: Position[snx.NXsample, RunType],
+) -> SolidAngle[RunType]:
+ r"""Approximate the solid angle of flat rectangular detector pixels.
+
+ The approximation is :math:`\Delta\Omega = A |\hat{n}\cdot\hat{r}| / r^2`,
+ where :math:`A` is the pixel area, :math:`\hat{n}` is its unit normal, and
+ :math:`r` is the distance from sample to pixel. It is valid when that distance
+ is much larger than the pixel width and height.
+
+ Parameters
+ ----------
+ detector:
+ Detector data with calibrated geometry coordinates: ``position`` contains
+ pixel centers, the x and y components of ``pixel_size`` give the local
+ pixel width and height, and ``detector_normal`` contains dimensionless
+ unit normals. Positions and normals must share a coordinate system with
+ ``sample_position``. Pixel sizes and normals may vary between pixels.
+ sample_position:
+ Position of the sample.
+
+ Returns
+ -------
+ :
+ Solid angles with the detector's pixel coordinates and masks preserved.
+ """
+ scattered_beam = detector.coords['position'] - sample_position
+ pixel_size = detector.coords['pixel_size']
+ area = pixel_size.fields.x * pixel_size.fields.y
+ omega = (
+ area
+ * sc.abs(sc.dot(detector.coords['detector_normal'], scattered_beam))
+ / sc.norm(scattered_beam) ** 3
+ ).to(unit='dimensionless')
+
+ return SolidAngle[RunType](
+ concepts.rewrap_reduced_data(
+ prototype=detector, data=omega, dim=set(detector.dims) - set(omega.dims)
+ )
+ )
+
+
def mask_solid_angle(
solid_angle: SolidAngle[RunType],
masks: DetectorMasks,
diff --git a/packages/esssans/src/ess/skadi/__init__.py b/packages/esssans/src/ess/skadi/__init__.py
new file mode 100644
index 000000000..0709df48d
--- /dev/null
+++ b/packages/esssans/src/ess/skadi/__init__.py
@@ -0,0 +1,24 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright (c) 2026 Scipp contributors (https://github.com/scipp)
+
+import importlib.metadata
+
+from . import mcstas, workflow
+from .mcstas import load_skadi_mcstas
+from .workflow import SkadiMcStasWorkflow, SkadiWorkflow, skadi_default_parameters
+
+try:
+ __version__ = importlib.metadata.version("esssans")
+except importlib.metadata.PackageNotFoundError:
+ __version__ = "0.0.0"
+
+del importlib
+
+__all__ = [
+ 'SkadiMcStasWorkflow',
+ 'SkadiWorkflow',
+ 'load_skadi_mcstas',
+ 'mcstas',
+ 'skadi_default_parameters',
+ 'workflow',
+]
diff --git a/packages/esssans/src/ess/skadi/data.py b/packages/esssans/src/ess/skadi/data.py
new file mode 100644
index 000000000..da53dc5e8
--- /dev/null
+++ b/packages/esssans/src/ess/skadi/data.py
@@ -0,0 +1,25 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright (c) 2026 Scipp contributors (https://github.com/scipp)
+"""Data for SKADI documentation examples."""
+
+from pathlib import Path
+
+from ess.reduce.data import make_registry
+
+_registry = make_registry(
+ "ess/skadi",
+ files={
+ "skadi_mcstas_1e8_sample10_1_of_50.h5": (
+ "md5:37883335a05c41d420cff2b38f883fc2"
+ ),
+ },
+ version="1",
+)
+
+
+def skadi_mcstas_sample() -> Path:
+ """Return the reduced SKADI McStas sample used in the user guide."""
+ return _registry.get_path("skadi_mcstas_1e8_sample10_1_of_50.h5")
+
+
+__all__ = ["skadi_mcstas_sample"]
diff --git a/packages/esssans/src/ess/skadi/mcstas.py b/packages/esssans/src/ess/skadi/mcstas.py
new file mode 100644
index 000000000..b9b73a7ed
--- /dev/null
+++ b/packages/esssans/src/ess/skadi/mcstas.py
@@ -0,0 +1,399 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright (c) 2026 Scipp contributors (https://github.com/scipp)
+"""McStas input adapter for the SKADI workflow."""
+
+from __future__ import annotations
+
+import re
+from dataclasses import dataclass
+from pathlib import Path
+
+import h5py
+import numpy as np
+import scipp as sc
+import scippnexus as snx
+from scippneutron.conversion.graph import tof
+
+from ..sans.conversions import ElasticCoordTransformGraph, sans_elastic
+from ..sans.types import (
+ CorrectForGravity,
+ Filename,
+ GravityVector,
+ MonitorTerm,
+ Position,
+ RawDetector,
+ RunType,
+ WavelengthBins,
+ WavelengthDetector,
+)
+
+# The McStas instrument places sourceMantid this far upstream of sample_position
+# along the final collimation axis. The pruned files omit that virtual component.
+_SOURCE_TO_SAMPLE_DISTANCE = sc.scalar(38.42, unit='m')
+
+# The McStas detector geometry description needs some adjustments.
+# Specifically, the outermost pixels on the banks cover a slightly larger
+# area than the rest. But that is not reflected in the McStas geometry data.
+# These parameters are used to make the adjustments:
+# Width and height assigned to low-resolution pixels.
+LOW_RES_PIXEL_SIZE = sc.scalar(0.006, unit='m')
+# Width and height assigned to high-resolution pixels.
+HIGH_RES_PIXEL_SIZE = sc.scalar(0.003, unit='m')
+# Added to the width or height of perimeter pixels; their centers move outward by half.
+PERIMETER_PIXEL_EXTENSION = sc.scalar(0.00025, unit='m')
+# Depth assigned to every pixel.
+PIXEL_DEPTH = sc.scalar(0.001, unit='m')
+
+
+@dataclass(frozen=True)
+class _DetectorSpec:
+ group: h5py.Group
+ component_name: str
+ pixel_min: int
+ x_limits: tuple[float, float]
+ y_limits: tuple[float, float]
+ shape: tuple[int, int]
+
+ @property
+ def event_count(self) -> int:
+ return self.group['events'].shape[0]
+
+ @property
+ def pixel_count(self) -> int:
+ return self.shape[0] * self.shape[1]
+
+
+_NUMBER = r"[-+]?\d+(?:\.\d*)?(?:[eE][-+]?\d+)?"
+_X_OPTIONS = re.compile(
+ rf"x limits=\[\s*({_NUMBER})\s*,\s*({_NUMBER})\s*\]\s+bins=(\d+)"
+)
+_Y_OPTIONS = re.compile(
+ rf"y limits=\[\s*({_NUMBER})\s*,\s*({_NUMBER})\s*\]\s+bins=(\d+)"
+)
+_PIXEL_MIN = re.compile(r"pixel min=(\d+)")
+
+
+def _decode(value: str | bytes) -> str:
+ return value.decode('utf-8') if isinstance(value, bytes) else value
+
+
+def _mcstas_path(filename: str | Path) -> Path:
+ path = Path(filename)
+ if path.is_dir():
+ path = path / 'mccode.h5'
+ return path
+
+
+def _parse_detector_spec(group: h5py.Group) -> _DetectorSpec:
+ options = _decode(group.attrs['options'])
+ x_match = _X_OPTIONS.search(options)
+ y_match = _Y_OPTIONS.search(options)
+ pixel_match = _PIXEL_MIN.search(options)
+ if x_match is None or y_match is None or pixel_match is None:
+ raise ValueError(f"Cannot parse detector geometry options for {group.name!r}")
+ return _DetectorSpec(
+ group=group,
+ component_name=_decode(group.attrs['component']),
+ pixel_min=int(pixel_match.group(1)),
+ x_limits=(float(x_match.group(1)), float(x_match.group(2))),
+ y_limits=(float(y_match.group(1)), float(y_match.group(2))),
+ shape=(int(y_match.group(3)), int(x_match.group(3))),
+ )
+
+
+def corrected_pixel_geometry(
+ *,
+ component_position: sc.Variable,
+ component_rotation: np.ndarray,
+ x_limits: tuple[float, float],
+ y_limits: tuple[float, float],
+ shape: tuple[int, int],
+) -> tuple[sc.Variable, sc.Variable, sc.Variable]:
+ """Build corrected global pixel positions, sizes, and surface normals.
+
+ McStas records an even grid of pixel centers. The outermost pixels represent the
+ remaining quarter-millimetre perimeter of each detector tile. Their centers are
+ shifted outwards by half that amount and their corresponding size is enlarged.
+ This is the correction used by the scripts supplied with the SKADI example data.
+
+ Parameters
+ ----------
+ component_position:
+ Global position of the McStas detector component in metres.
+ component_rotation:
+ McStas rotation matrix for the detector component.
+ x_limits:
+ Limits of the local pixel grid in metres.
+ y_limits:
+ Limits of the local pixel grid in metres.
+ shape:
+ Number of pixels as ``(y, x)``.
+
+ Returns
+ -------
+ positions: scipp.Variable
+ Corrected global pixel centers.
+ sizes: scipp.Variable
+ Corrected pixel sizes as ``(x, y, depth)``.
+ normals: scipp.Variable
+ Global detector surface normals.
+ """
+ ny, nx = shape
+ if nx != ny or nx not in (8, 16):
+ raise ValueError(
+ f"Unsupported SKADI McStas detector shape {shape}; expected 8x8 or 16x16"
+ )
+ nominal_size = LOW_RES_PIXEL_SIZE if nx == 8 else HIGH_RES_PIXEL_SIZE
+
+ x = sc.midpoints(sc.linspace('x', *x_limits, num=nx + 1, unit='m'))
+ y = sc.midpoints(sc.linspace('y', *y_limits, num=ny + 1, unit='m'))
+ shift = PERIMETER_PIXEL_EXTENSION / 2
+ x['x', 0] -= shift
+ x['x', -1] += shift
+ y['y', 0] -= shift
+ y['y', -1] += shift
+
+ local = (
+ sc.spatial.as_vectors(x=x, y=y, z=sc.scalar(0.0, unit='m'))
+ .transpose(['y', 'x'])
+ .flatten(to='detector_number')
+ )
+ # McStas applies the stored matrix to row vectors. Scipp transformations act on
+ # column vectors, so the matrix must be transposed.
+ rotation = sc.spatial.linear_transform(value=component_rotation.T)
+ positions = component_position + rotation * local
+
+ width = sc.broadcast(nominal_size, sizes={'y': ny, 'x': nx}).copy()
+ height = width.copy()
+ width['x', 0] += PERIMETER_PIXEL_EXTENSION
+ width['x', -1] += PERIMETER_PIXEL_EXTENSION
+ height['y', 0] += PERIMETER_PIXEL_EXTENSION
+ height['y', -1] += PERIMETER_PIXEL_EXTENSION
+ sizes = sc.spatial.as_vectors(
+ width,
+ height,
+ sc.broadcast(PIXEL_DEPTH, sizes=width.sizes),
+ ).flatten(to='detector_number')
+
+ normal = rotation * sc.vector([0.0, 0.0, 1.0])
+ normals = sc.broadcast(normal, sizes=positions.sizes)
+ return positions, sizes, normals
+
+
+def _component_map(components: h5py.Group) -> dict[str, h5py.Group]:
+ return {
+ name.split('_', maxsplit=1)[-1]: group
+ for name, group in components.items()
+ if isinstance(group, h5py.Group)
+ }
+
+
+def load_skadi_mcstas(filename: str | Path) -> sc.DataArray:
+ """Load SKADI McStas detector events and apply the geometry correction.
+
+ The McStas event probability is retained as the event value, with its square as
+ the variance. Events are grouped by the global McStas pixel ID. Source and sample
+ positions are supplied separately by the workflow's ``Position`` providers.
+
+ Parameters
+ ----------
+ filename:
+ McStas ``mccode.h5`` file or its containing directory.
+
+ Returns
+ -------
+ :
+ Calibrated, event-mode detector data suitable for the SKADI workflow.
+ """
+ path = _mcstas_path(filename)
+ with h5py.File(path, 'r') as file:
+ data_groups = file['entry1/data']
+ specs = sorted(
+ (
+ _parse_detector_spec(group)
+ for group in data_groups.values()
+ if isinstance(group, h5py.Group) and 'events' in group
+ ),
+ key=lambda spec: spec.pixel_min,
+ )
+ if not specs:
+ raise ValueError(f"No McStas detector event groups found in {path}")
+
+ total_pixels = 0
+ for spec in specs:
+ if spec.pixel_min != total_pixels:
+ raise ValueError(
+ "McStas detector groups do not cover a contiguous pixel-ID "
+ f"range; expected ID {total_pixels}, got {spec.pixel_min}"
+ )
+ total_pixels += spec.pixel_count
+
+ total_events = sum(spec.event_count for spec in specs)
+ positions = []
+ pixel_sizes = []
+ detector_normals = []
+ weights = np.empty(total_events, dtype=np.float64)
+ event_time_offset = np.empty(total_events, dtype=np.float64)
+ pixel_ids = np.empty(total_events, dtype=np.int64)
+
+ components = file['entry1/instrument/components']
+ component_by_name = _component_map(components)
+ event_cursor = 0
+ for spec in specs:
+ pixel_stop = spec.pixel_min + spec.pixel_count
+ component = component_by_name.get(spec.component_name)
+ if component is None:
+ raise ValueError(
+ f"No instrument component found for {spec.component_name!r}"
+ )
+ geometry = corrected_pixel_geometry(
+ component_position=sc.vector(component['Position'][()], unit='m'),
+ component_rotation=np.asarray(component['Rotation'][()]),
+ x_limits=spec.x_limits,
+ y_limits=spec.y_limits,
+ shape=spec.shape,
+ )
+ positions.append(geometry[0])
+ pixel_sizes.append(geometry[1])
+ detector_normals.append(geometry[2])
+
+ group = spec.group
+ columns = _decode(group.attrs['variables']).split()
+ column = {name: i for i, name in enumerate(columns)}
+ events = group['events'][()]
+ ids = events[:, column['id']]
+ if not np.all(
+ (ids >= spec.pixel_min) & (ids < pixel_stop) & (ids == np.floor(ids))
+ ):
+ raise ValueError(f"Invalid pixel ID in {group.name!r}")
+
+ event_stop = event_cursor + spec.event_count
+ weights[event_cursor:event_stop] = events[:, column['p']]
+ event_time_offset[event_cursor:event_stop] = events[:, column['t']]
+ pixel_ids[event_cursor:event_stop] = ids
+ event_cursor = event_stop
+
+ events = sc.DataArray(
+ sc.array(
+ dims=['event'],
+ values=weights,
+ variances=np.square(weights),
+ ),
+ coords={
+ 'event_time_offset': sc.array(
+ dims=['event'], values=event_time_offset, unit='s'
+ ),
+ 'detector_number': sc.array(dims=['event'], values=pixel_ids, unit=None),
+ },
+ )
+ detector_dim = 'detector_number'
+ detector_numbers = sc.arange(detector_dim, total_pixels, unit=None)
+ return events.group(detector_numbers).assign_coords(
+ position=sc.concat(positions, detector_dim),
+ pixel_size=sc.concat(pixel_sizes, detector_dim),
+ detector_normal=sc.concat(detector_normals, detector_dim),
+ )
+
+
+def load_skadi_mcstas_provider(
+ filename: Filename[RunType],
+) -> RawDetector[RunType]:
+ """Load a McStas file for a run in the SKADI workflow."""
+ return RawDetector[RunType](load_skadi_mcstas(filename))
+
+
+def source_position_from_mcstas(
+ filename: Filename[RunType],
+ sample_position: Position[snx.NXsample, RunType],
+) -> Position[snx.NXsource, RunType]:
+ """Construct the effective source using the incident direction at the sample.
+
+ The local +z axis of ``sample_position`` follows the final collimation axis.
+ Using the physical moderator position instead would point across the bends in
+ the guide. Retain the flight-path length while aligning the source-to-sample
+ vector with the incident beam. Only component metadata is read.
+ """
+ with h5py.File(_mcstas_path(filename), 'r') as file:
+ components = _component_map(file['entry1/instrument/components'])
+ rotation = sc.spatial.linear_transform(
+ value=components['sample_position']['Rotation'][()].T
+ )
+ incident_direction = rotation * sc.vector([0.0, 0.0, 1.0])
+ flight_path = _SOURCE_TO_SAMPLE_DISTANCE.to(unit=sample_position.unit)
+ return Position[snx.NXsource, RunType](
+ sample_position - flight_path * incident_direction / sc.norm(incident_direction)
+ )
+
+
+def sample_position_from_mcstas(
+ filename: Filename[RunType],
+) -> Position[snx.NXsample, RunType]:
+ """Read the sample position without loading detector events."""
+ with h5py.File(_mcstas_path(filename), 'r') as file:
+ components = _component_map(file['entry1/instrument/components'])
+ return Position[snx.NXsample, RunType](
+ sc.vector(components['sample_position']['Position'][()], unit='m')
+ )
+
+
+def mcstas_detector_coord_transform_graph(
+ correct_for_gravity: CorrectForGravity,
+ *,
+ sample_position: Position[snx.NXsample, RunType],
+ source_position: Position[snx.NXsource, RunType],
+ gravity: GravityVector,
+) -> ElasticCoordTransformGraph[RunType]:
+ """Build the coordinate graph for McStas, whose event time is already TOF."""
+ graph = sans_elastic(
+ correct_for_gravity=correct_for_gravity,
+ sample_position=sample_position,
+ source_position=source_position,
+ gravity=gravity,
+ )
+ return ElasticCoordTransformGraph[RunType](
+ {**graph, **tof.elastic_wavelength('tof')}
+ )
+
+
+def mcstas_data_to_wavelength(
+ detector: RawDetector[RunType],
+ graph: ElasticCoordTransformGraph[RunType],
+) -> WavelengthDetector[RunType]:
+ """Convert McStas time-of-flight events to wavelength."""
+ event_time_offset = detector.bins.coords['event_time_offset']
+ detector = detector.bins.drop_coords('event_time_offset')
+ detector.bins.coords['tof'] = event_time_offset
+ return WavelengthDetector[RunType](
+ detector.transform_coords(
+ 'wavelength', graph=graph, keep_intermediate=False, rename_dims=False
+ )
+ )
+
+
+def flat_monitor_term(wavelength_bins: WavelengthBins) -> MonitorTerm[RunType]:
+ """Assume unit incident intensity per angstrom when no monitor is available.
+
+ The SANS denominator expects intensity integrated over each wavelength bin.
+ Weight by bin width so changing the binning does not rescale the result.
+ This flat spectrum is a placeholder for relative intensities; calibrated
+ reductions require a monitor spectrum and transmission correction.
+ """
+ weights = (wavelength_bins[1:] - wavelength_bins[:-1]) / sc.scalar(
+ 1.0, unit='angstrom'
+ )
+ return MonitorTerm[RunType](
+ sc.DataArray(
+ weights.to(unit='dimensionless'),
+ coords={'wavelength': sc.midpoints(wavelength_bins)},
+ )
+ )
+
+
+mcstas_providers = (
+ load_skadi_mcstas_provider,
+ source_position_from_mcstas,
+ sample_position_from_mcstas,
+ mcstas_detector_coord_transform_graph,
+ mcstas_data_to_wavelength,
+ flat_monitor_term,
+)
diff --git a/packages/esssans/src/ess/skadi/workflow.py b/packages/esssans/src/ess/skadi/workflow.py
new file mode 100644
index 000000000..394ca95d8
--- /dev/null
+++ b/packages/esssans/src/ess/skadi/workflow.py
@@ -0,0 +1,66 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright (c) 2026 Scipp contributors (https://github.com/scipp)
+"""Default providers and parameters for the SKADI SANS workflow."""
+
+import sciline
+from ess import sans
+from ess.sans.normalization import rectangular_pixel_solid_angle
+from ess.sans.parameters import typical_outputs
+
+from ess.reduce.uncertainty import UncertaintyBroadcastMode
+from ess.reduce.unwrap import WavelengthLutMode
+from ess.reduce.workflow import register_workflow
+
+from ..sans.types import (
+ DetectorMasks,
+ DirectBeam,
+ ReturnEvents,
+)
+from .mcstas import mcstas_providers
+
+
+def skadi_default_parameters() -> dict:
+ """Return defaults for a minimal SKADI reduction."""
+ return {
+ DetectorMasks: {},
+ DirectBeam: None,
+ ReturnEvents: False,
+ }
+
+
+@register_workflow
+def SkadiWorkflow(
+ wavelength_from: WavelengthLutMode = "file",
+) -> sciline.Pipeline:
+ """Create a basic, data-source-independent SKADI reduction workflow.
+
+ Parameters
+ ----------
+ wavelength_from:
+ Mode used by the common SANS workflow to obtain wavelength.
+
+ Returns
+ -------
+ :
+ The SKADI reduction workflow.
+ """
+ workflow = sans.SansWorkflow(wavelength_from=wavelength_from)
+ workflow.insert(rectangular_pixel_solid_angle)
+ for key, value in skadi_default_parameters().items():
+ workflow[key] = value
+ workflow.typical_outputs = typical_outputs
+ return workflow
+
+
+@register_workflow
+def SkadiMcStasWorkflow() -> sciline.Pipeline:
+ """Create the SKADI McStas workflow for relative intensity reductions.
+
+ In the absence of monitor data, assume a flat incident spectrum with unit
+ intensity per angstrom and unit transmission.
+ """
+ workflow = SkadiWorkflow()
+ for provider in mcstas_providers:
+ workflow.insert(provider)
+ workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.drop
+ return workflow
diff --git a/packages/esssans/tests/normalization_test.py b/packages/esssans/tests/normalization_test.py
index 4ea4e7a57..f7b24f600 100644
--- a/packages/esssans/tests/normalization_test.py
+++ b/packages/esssans/tests/normalization_test.py
@@ -7,6 +7,7 @@
import scippnexus as snx
from ess.isissans.data import sans2d_solid_angle_reference
from ess.sans import normalization
+from scipp.testing import assert_allclose
from ess.reduce.nexus.types import NeXusTransformation, SampleRun
@@ -89,6 +90,55 @@ def test_solid_angle_compare_to_reference_file():
)
+@pytest.mark.parametrize('size_unit', ['m', 'mm'])
+@pytest.mark.parametrize('normal_sign', [-1, 1])
+def test_rectangular_pixel_solid_angle(size_unit, normal_sign):
+ sample_position = sc.vector([1.0, -2.0, 0.5], unit='m')
+ # Normal incidence, double distance, off-axis, tilted, edge-on, double area.
+ detector = sc.DataArray(
+ sc.ones(sizes={'pixel': 6}),
+ coords={
+ 'position': sc.vectors(
+ dims=['pixel'],
+ values=[
+ [0, 0, 2],
+ [0, 0, 4],
+ [1, 0, 1],
+ [0, 0, 2],
+ [0, 0, 2],
+ [0, 0, 2],
+ ],
+ unit='m',
+ )
+ + sample_position,
+ 'pixel_size': sc.vectors(
+ dims=['pixel'],
+ values=[[0.02, 0.03, 0.001]] * 5 + [[0.02, 0.06, 0.01]],
+ unit='m',
+ ).to(unit=size_unit),
+ 'detector_normal': normal_sign
+ * sc.vectors(
+ dims=['pixel'],
+ values=[[0, 0, 1]] * 3
+ + [[np.sqrt(3) / 2, 0, 0.5], [1, 0, 0], [0, 0, 1]],
+ ),
+ },
+ )
+
+ solid_angle = normalization.rectangular_pixel_solid_angle(
+ detector, sample_position=sample_position
+ )
+
+ assert_allclose(
+ solid_angle.data,
+ sc.array(
+ dims=['pixel'],
+ values=[0.00015, 0.0000375, 0.0003 / np.sqrt(2), 0.000075, 0.0, 0.0003],
+ unit='dimensionless',
+ ),
+ )
+
+
def test_transmission_fraction():
N = 100
wavelength = sc.linspace(
diff --git a/packages/esssans/tests/package_test.py b/packages/esssans/tests/package_test.py
index 46906b2ce..b0bc9bda8 100644
--- a/packages/esssans/tests/package_test.py
+++ b/packages/esssans/tests/package_test.py
@@ -1,6 +1,6 @@
# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
-from ess import isissans, loki, sans
+from ess import isissans, loki, sans, skadi
"""Tests of package integrity.
@@ -13,6 +13,7 @@ def test_has_version():
assert hasattr(isissans, '__version__')
assert hasattr(loki, '__version__')
assert hasattr(sans, '__version__')
+ assert skadi.__version__ == sans.__version__
if __name__ == '__main__':
diff --git a/packages/esssans/tests/skadi/mcstas_test.py b/packages/esssans/tests/skadi/mcstas_test.py
new file mode 100644
index 000000000..00f32968c
--- /dev/null
+++ b/packages/esssans/tests/skadi/mcstas_test.py
@@ -0,0 +1,345 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright (c) 2026 Scipp contributors (https://github.com/scipp)
+
+from pathlib import Path
+
+import h5py
+import numpy as np
+import pytest
+import scipp as sc
+import scippnexus as snx
+from ess.sans.conversions import ElasticCoordTransformGraph
+from ess.sans.types import (
+ DetectorMasks,
+ Filename,
+ IntensityQ,
+ Position,
+ QBins,
+ RawDetector,
+ SampleRun,
+ SolidAngle,
+ WavelengthBins,
+ WavelengthDetector,
+)
+from ess.skadi import SkadiMcStasWorkflow, load_skadi_mcstas
+from ess.skadi.mcstas import flat_monitor_term
+from scipp.testing import assert_allclose, assert_identical
+
+
+def _component(
+ components: h5py.Group,
+ name: str,
+ position: list[float],
+ rotation: np.ndarray | None = None,
+) -> None:
+ group = components.create_group(name)
+ group.create_dataset('Position', data=position)
+ group.create_dataset('Rotation', data=np.eye(3) if rotation is None else rotation)
+
+
+def _small_mcstas_file(
+ path: Path,
+ *,
+ detector_rotation: np.ndarray | None = None,
+ sample_rotation: np.ndarray | None = None,
+ pixels_per_side: int = 8,
+) -> Path:
+ with h5py.File(path, 'w') as file:
+ entry = file.create_group('entry1')
+ data = entry.create_group('data')
+ detector = data.create_group('detector_events')
+ detector.attrs['component'] = 'detector_0'
+ detector.attrs['variables'] = 'p x y n id t '
+ detector.attrs['options'] = (
+ f'mantid square x limits=[-0.024,0.024] bins={pixels_per_side} '
+ f'y limits=[-0.024,0.024] bins={pixels_per_side}, neutron pixel min=0 t, '
+ 'list all neutrons'
+ )
+ detector.create_dataset(
+ 'events',
+ data=np.array(
+ [
+ [2.0, 0.0, 0.0, 1.0, 1.0, 0.014],
+ [1.0, 0.0, 0.0, 2.0, 0.0, 0.012],
+ [3.0, 0.0, 0.0, 3.0, 1.0, 0.016],
+ ]
+ ),
+ )
+
+ instrument = entry.create_group('instrument')
+ components = instrument.create_group('components')
+ _component(components, '0001_sourceESS', [0.0, 0.0, 0.0])
+ _component(
+ components,
+ '0002_sample_position',
+ [0.0, 0.0, 10.0],
+ rotation=sample_rotation,
+ )
+ _component(
+ components,
+ '0003_detector_0',
+ [0.0, 0.0, 12.0],
+ rotation=detector_rotation,
+ )
+ return path
+
+
+def test_mcstas_loader_groups_weighted_events_across_tiles(tmp_path: Path) -> None:
+ filename = _small_mcstas_file(tmp_path / 'mccode.h5')
+ with h5py.File(filename, 'r+') as file:
+ data = file['entry1/data']
+ # Group names deliberately sort in the opposite order to pixel IDs.
+ for name, start, count in [('b_second', 64, 3), ('a_empty', 128, 0)]:
+ file.copy(data['detector_events'], data, name=name)
+ group = data[name]
+ group.attrs['options'] = group.attrs['options'].replace(
+ 'pixel min=0', f'pixel min={start}'
+ )
+ group.attrs['component'] = f'detector_{start}'
+ _component(
+ file['entry1/instrument/components'],
+ f'0004_detector_{start}',
+ [float(start), 0.0, 12.0],
+ )
+ events = group['events'][()]
+ events[:, 4] += start
+ # Exercise column metadata rather than assuming a fixed column order.
+ group.attrs['variables'] = np.bytes_('t id p x y n')
+ del group['events']
+ group.create_dataset('events', data=events[:count, [5, 4, 0, 1, 2, 3]])
+
+ detector = load_skadi_mcstas(filename)
+ events_per_pixel = detector.bins.size()
+
+ assert detector.sizes == {'detector_number': 192}
+ for start in (0, 64):
+ assert events_per_pixel['detector_number', start].value == 1
+ assert events_per_pixel['detector_number', start + 1].value == 2
+ assert (
+ events_per_pixel['detector_number', start + 2 : start + 64].sum().value == 0
+ )
+ events = detector['detector_number', start + 1].value
+ assert_identical(
+ events.data,
+ sc.array(dims=['event'], values=[2.0, 3.0], variances=[4.0, 9.0]),
+ )
+ assert_identical(
+ events.coords['event_time_offset'],
+ sc.array(dims=['event'], values=[0.014, 0.016], unit='s'),
+ )
+ assert events_per_pixel['detector_number', 128:].sum().value == 0
+ for start in (64, 128):
+ assert_allclose(
+ detector.coords['position'][start] - detector.coords['position'][0],
+ sc.vector([float(start), 0.0, 0.0], unit='m'),
+ )
+
+
+@pytest.mark.parametrize('pixel_id', [-1.0, -0.5, 1.9, 64.0, np.nan, np.inf])
+def test_mcstas_loader_rejects_invalid_pixel_ids(
+ tmp_path: Path, pixel_id: float
+) -> None:
+ filename = _small_mcstas_file(tmp_path / 'mccode.h5')
+ with h5py.File(filename, 'r+') as file:
+ file['entry1/data/detector_events/events'][0, 4] = pixel_id
+
+ with pytest.raises(ValueError, match='Invalid pixel ID'):
+ load_skadi_mcstas(filename)
+
+
+@pytest.mark.parametrize('use_directory', [False, True])
+def test_mcstas_positions_do_not_require_detector_data(
+ tmp_path: Path, use_directory: bool
+) -> None:
+ # The sample frame is rotated so the outgoing collimation axis is global -x.
+ rotation = np.array([[0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [-1.0, 0.0, 0.0]])
+ filename = _small_mcstas_file(tmp_path / 'mccode.h5', sample_rotation=rotation)
+ with h5py.File(filename, 'r+') as file:
+ del file['entry1/data']
+ workflow = SkadiMcStasWorkflow()
+ workflow[Filename[SampleRun]] = tmp_path if use_directory else filename
+
+ sample = workflow.compute(Position[snx.NXsample, SampleRun])
+ source = workflow.compute(Position[snx.NXsource, SampleRun])
+
+ assert_identical(sample, sc.vector([0.0, 0.0, 10.0], unit='m'))
+ # Preserve the instrument's 38.42 m flight path, not the 10 m straight-line
+ # separation between moderator and sample in this synthetic file.
+ assert_allclose(source, sc.vector([38.42, 0.0, 10.0], unit='m'))
+
+
+def test_mcstas_effective_source_gives_zero_q_along_incident_beam(
+ tmp_path: Path,
+) -> None:
+ rotation = np.array([[0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [-1.0, 0.0, 0.0]])
+ filename = _small_mcstas_file(tmp_path / 'mccode.h5', sample_rotation=rotation)
+ workflow = SkadiMcStasWorkflow()
+ workflow[Filename[SampleRun]] = filename
+ graph = workflow.compute(ElasticCoordTransformGraph[SampleRun])
+ detector = sc.DataArray(
+ sc.scalar(1.0),
+ coords={
+ 'position': sc.vector([-2.0, 0.0, 10.0], unit='m'),
+ 'tof': sc.scalar(0.012, unit='s'),
+ },
+ )
+
+ converted = detector.transform_coords(['Q', 'L1'], graph=graph)
+
+ assert_allclose(converted.coords['L1'].to(unit='m'), sc.scalar(38.42, unit='m'))
+ assert_allclose(
+ converted.coords['Q'],
+ sc.scalar(0.0, unit='1/angstrom'),
+ atol=sc.scalar(1e-14, unit='1/angstrom'),
+ )
+
+
+@pytest.mark.parametrize(
+ ('component', 'position'),
+ [(snx.NXsource, [0.0, 0.0, 5.0]), (snx.NXsample, [1.0, 0.0, 10.0])],
+)
+def test_mcstas_workflow_uses_position_overrides_for_wavelength(
+ tmp_path: Path, component: type, position: list[float]
+) -> None:
+ filename = _small_mcstas_file(tmp_path / 'mccode.h5')
+ workflow = SkadiMcStasWorkflow()
+ workflow[Filename[SampleRun]] = filename
+ source = sc.vector([0.0, 0.0, 0.0], unit='m')
+ sample = sc.vector([0.0, 0.0, 10.0], unit='m')
+ workflow[Position[snx.NXsource, SampleRun]] = source
+ workflow[Position[snx.NXsample, SampleRun]] = sample
+ original = workflow.compute(WavelengthDetector[SampleRun])
+ updated = sc.vector(position, unit='m')
+ workflow[Position[component, SampleRun]] = updated
+ if component is snx.NXsource:
+ source = updated
+ else:
+ sample = updated
+
+ detector = workflow.compute(WavelengthDetector[SampleRun])
+
+ distance = sc.norm(sample - source) + sc.norm(
+ detector.coords['position'][0] - sample
+ )
+ expected = (
+ sc.constants.h / sc.constants.m_n * sc.scalar(0.012, unit='s') / distance
+ ).to(unit='angstrom')
+ assert_allclose(
+ detector.bins.constituents['data'].coords['wavelength'][0], expected
+ )
+ assert not sc.allclose(
+ detector.bins.constituents['data'].coords['wavelength'],
+ original.bins.constituents['data'].coords['wavelength'],
+ )
+
+
+@pytest.mark.parametrize(
+ ('pixels_per_side', 'pixel_width', 'corner'),
+ [(8, 0.006, 0.021125), (16, 0.003, 0.022625)],
+)
+def test_mcstas_loader_corrects_and_rotates_pixel_geometry(
+ tmp_path: Path, pixels_per_side: int, pixel_width: float, corner: float
+) -> None:
+ # McStas stores matrices for multiplication from the left by row vectors.
+ rotation = np.array([[0.0, 0.0, 1.0], [0.0, 1.0, 0.0], [-1.0, 0.0, 0.0]])
+ detector = load_skadi_mcstas(
+ _small_mcstas_file(
+ tmp_path / 'mccode.h5',
+ detector_rotation=rotation,
+ pixels_per_side=pixels_per_side,
+ )
+ )
+
+ assert_allclose(
+ detector.coords['position'][0],
+ sc.vector([0.0, -corner, 12.0 - corner], unit='m'),
+ )
+ assert_allclose(
+ detector.coords['position'][-1],
+ sc.vector([0.0, corner, 12.0 + corner], unit='m'),
+ )
+ assert_allclose(
+ detector.coords['position'][1] - detector.coords['position'][0],
+ sc.vector([0.0, 0.0, pixel_width + 0.000125], unit='m'),
+ )
+ assert_allclose(
+ detector.coords['position'][pixels_per_side] - detector.coords['position'][0],
+ sc.vector([0.0, pixel_width + 0.000125, 0.0], unit='m'),
+ )
+ assert_identical(detector.coords['detector_normal'][0], sc.vector([-1.0, 0.0, 0.0]))
+ for index in (0, pixels_per_side**2 - 1):
+ assert_allclose(
+ detector.coords['pixel_size'][index],
+ sc.vector([pixel_width + 0.00025, pixel_width + 0.00025, 0.001], unit='m'),
+ )
+ assert_allclose(
+ detector.coords['pixel_size'][pixels_per_side + 1],
+ sc.vector([pixel_width, pixel_width, 0.001], unit='m'),
+ )
+
+
+@pytest.mark.parametrize(
+ 'edges',
+ [np.linspace(2.0, 8.0, 16), np.linspace(2.0, 8.0, 31), [2.0, 3.0, 5.0, 8.0]],
+ ids=['coarse', 'fine', 'uneven'],
+)
+def test_mcstas_intensity_normalization_is_independent_of_wavelength_binning(
+ tmp_path: Path, edges: list[float] | np.ndarray
+) -> None:
+ filename = _small_mcstas_file(tmp_path / 'mccode.h5')
+ workflow = SkadiMcStasWorkflow()
+ workflow[Filename[SampleRun]] = filename
+ workflow[Position[snx.NXsource, SampleRun]] = sc.vector([0.0, 0.0, 0.0], unit='m')
+ workflow[WavelengthBins] = sc.array(
+ dims=['wavelength'], values=edges, unit='angstrom'
+ )
+ workflow[QBins] = sc.linspace('Q', 0.0, 1.0, 2, unit='1/angstrom')
+
+ intensity = workflow.compute(IntensityQ[SampleRun])
+
+ # All events and pixels contribute to one Q bin over a six-angstrom band.
+ solid_angle = workflow.compute(SolidAngle[SampleRun]).sum().data
+ expected = sc.scalar(6.0, variance=14.0) / (6.0 * solid_angle)
+ assert intensity.sizes == {'Q': 1}
+ assert_allclose(intensity.data[0], expected)
+
+
+def test_flat_monitor_term_accounts_for_wavelength_units() -> None:
+ edges = sc.array(dims=['wavelength'], values=[0.2, 0.3, 0.5, 0.8], unit='nm')
+
+ monitor = flat_monitor_term(edges)
+
+ assert_allclose(
+ monitor,
+ sc.DataArray(
+ sc.array(dims=['wavelength'], values=[1.0, 2.0, 3.0]),
+ coords={
+ 'wavelength': sc.array(
+ dims=['wavelength'], values=[0.25, 0.4, 0.65], unit='nm'
+ )
+ },
+ ),
+ )
+
+
+def test_mcstas_reduction_preserves_masks_attached_to_raw_detector(
+ tmp_path: Path,
+) -> None:
+ filename = _small_mcstas_file(tmp_path / 'mccode.h5')
+ workflow = SkadiMcStasWorkflow()
+ workflow[Filename[SampleRun]] = filename
+ workflow[Position[snx.NXsource, SampleRun]] = sc.vector([0.0, 0.0, 0.0], unit='m')
+ workflow[WavelengthBins] = sc.linspace('wavelength', 2.0, 8.0, 31, unit='angstrom')
+ workflow[QBins] = sc.linspace('Q', 0.0, 1.0, 2, unit='1/angstrom')
+ raw = workflow.compute(RawDetector[SampleRun])
+ mask = raw.coords['detector_number'] > sc.scalar(0, unit=None)
+ workflow[DetectorMasks] = {'pixel_mask': mask}
+ expected = workflow.compute(IntensityQ[SampleRun])
+ workflow[DetectorMasks] = {}
+ workflow[RawDetector[SampleRun]] = raw.assign_masks(pixel_mask=mask)
+
+ actual = workflow.compute(IntensityQ[SampleRun])
+
+ assert sc.isfinite(expected.data).all().value
+ assert (expected.data > sc.scalar(0.0)).all().value
+ assert_allclose(actual, expected)
diff --git a/packages/esssans/tests/skadi/workflow_test.py b/packages/esssans/tests/skadi/workflow_test.py
new file mode 100644
index 000000000..20f457256
--- /dev/null
+++ b/packages/esssans/tests/skadi/workflow_test.py
@@ -0,0 +1,57 @@
+# SPDX-License-Identifier: BSD-3-Clause
+# Copyright (c) 2026 Scipp contributors (https://github.com/scipp)
+
+import scipp as sc
+import scippnexus as snx
+from ess.sans.types import Position, RawDetector, SampleRun, SolidAngle
+from ess.skadi import SkadiWorkflow
+from scipp.testing import assert_allclose, assert_identical
+
+
+def test_workflow_computes_solid_angle_preserving_pixel_masks() -> None:
+ detector = sc.DataArray(
+ sc.ones(sizes={'detector_number': 2}),
+ coords={
+ 'position': sc.vectors(
+ dims=['detector_number'],
+ values=[[0.0, 0.0, 2.0], [1.0, 0.0, 2.0]],
+ unit='m',
+ ),
+ 'pixel_size': sc.vectors(
+ dims=['detector_number'],
+ values=[[0.02, 0.03, 0.001], [0.02, 0.03, 0.001]],
+ unit='m',
+ ),
+ 'detector_normal': sc.vectors(
+ dims=['detector_number'],
+ values=[[0.0, 0.0, -1.0], [0.0, 0.0, -1.0]],
+ unit='dimensionless',
+ ),
+ },
+ )
+ detector = sc.broadcast(
+ detector, sizes={'detector_number': 2, 'wavelength': 3}
+ ).copy()
+ pixel_mask = sc.array(dims=['detector_number'], values=[False, True])
+ detector.masks['pixel_mask'] = pixel_mask
+ detector.masks['wavelength_mask'] = sc.array(
+ dims=['wavelength'], values=[False, True, False]
+ )
+ detector.coords['wavelength'] = sc.arange('wavelength', 4, unit='angstrom')
+ workflow = SkadiWorkflow()
+ workflow[RawDetector[SampleRun]] = detector
+ workflow[Position[snx.NXsample, SampleRun]] = sc.vector([0.0, 0.0, 0.0], unit='m')
+
+ solid_angle = workflow.compute(SolidAngle[SampleRun])
+
+ assert_allclose(
+ solid_angle.data,
+ sc.array(
+ dims=['detector_number'],
+ values=[0.00015, 0.0012 / 5**1.5],
+ unit='dimensionless',
+ ),
+ )
+ assert_identical(solid_angle.masks['pixel_mask'], pixel_mask)
+ assert 'wavelength_mask' not in solid_angle.masks
+ assert 'wavelength' not in solid_angle.coords
diff --git a/packages/esssans/tools/docs/skadi-thumbnails.ipynb b/packages/esssans/tools/docs/skadi-thumbnails.ipynb
new file mode 100644
index 000000000..e7813fa0e
--- /dev/null
+++ b/packages/esssans/tools/docs/skadi-thumbnails.ipynb
@@ -0,0 +1,129 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0",
+ "metadata": {},
+ "source": [
+ "# SKADI thumbnails\n",
+ "\n",
+ "This notebook generates the thumbnails used in the SKADI user guide."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import plopp as pp\n",
+ "import scipp as sc\n",
+ "\n",
+ "from ess.skadi import SkadiMcStasWorkflow\n",
+ "from ess.skadi.data import skadi_mcstas_sample\n",
+ "from ess.sans.types import Filename, RawDetector, SampleRun"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "workflow = SkadiMcStasWorkflow()\n",
+ "workflow[Filename[SampleRun]] = skadi_mcstas_sample()\n",
+ "detector = workflow.compute(RawDetector[SampleRun])\n",
+ "detector_intensity = detector.bins.sum()\n",
+ "\n",
+ "position = detector.coords[\"position\"]\n",
+ "\n",
+ "\n",
+ "def normalize(coord):\n",
+ " lower, upper = sc.min(coord), sc.max(coord)\n",
+ " return (coord - 0.5 * (lower + upper)) / (upper - lower)\n",
+ "\n",
+ "\n",
+ "x, y, z = (normalize(position.fields[axis]) for axis in (\"x\", \"y\", \"z\"))\n",
+ "detector_intensity.coords[\"view_x\"] = 0.82 * z + 0.57 * x\n",
+ "detector_intensity.coords[\"view_y\"] = -0.15 * z + 0.21 * x + 0.97 * y"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def detector_view_plot():\n",
+ " intensity = detector_intensity.values\n",
+ " positive = intensity[intensity > 0]\n",
+ " figure = pp.scatter(\n",
+ " detector_intensity,\n",
+ " x=\"view_x\",\n",
+ " y=\"view_y\",\n",
+ " cbar=True,\n",
+ " logc=True,\n",
+ " cmin=np.percentile(positive, 5),\n",
+ " cmax=np.percentile(positive, 99.5),\n",
+ " size=0.4,\n",
+ " figsize=(3, 2.5),\n",
+ " aspect=\"equal\",\n",
+ " rasterized=True,\n",
+ " linewidths=0,\n",
+ " )\n",
+ " figure.ax.set_axis_off()\n",
+ " figure.cax.set_axis_off()\n",
+ " return figure"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fig = detector_view_plot()\n",
+ "fig.save(\n",
+ " \"../../docs/_static/thumbnails/skadi_detector_view_light.svg\",\n",
+ " transparent=True,\n",
+ " bbox_inches=\"tight\",\n",
+ " pad_inches=0,\n",
+ ")\n",
+ "fig"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fig.save(\n",
+ " \"../../docs/_static/thumbnails/skadi_detector_view_dark.svg\",\n",
+ " transparent=True,\n",
+ " bbox_inches=\"tight\",\n",
+ " pad_inches=0,\n",
+ ")\n",
+ "fig"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/packages/esssans/tools/shrink_skadi_mcstas.ipynb b/packages/esssans/tools/shrink_skadi_mcstas.ipynb
new file mode 100644
index 000000000..03258fe7a
--- /dev/null
+++ b/packages/esssans/tools/shrink_skadi_mcstas.ipynb
@@ -0,0 +1,204 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "0",
+ "metadata": {},
+ "source": [
+ "# Make the reduced SKADI tutorial data\n",
+ "\n",
+ "This notebook creates the small McStas file used by the SKADI user-guide notebook. It keeps every 50th event across the complete detector, removes histogram outputs and their stale plotting metadata, and retains only the component positions and rotations required to reconstruct the detector geometry. The final `h5repack` step removes the vacated HDF5 space.\n",
+ "\n",
+ "The original simulation is not downloaded because of its size. Place `mccode.h5` from `all_banks_1e8_mpi4_sample10` in `SKADI_example_data` before running the notebook. The destination must not already exist."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import shutil\n",
+ "import subprocess\n",
+ "import tempfile\n",
+ "from pathlib import Path\n",
+ "\n",
+ "import h5py"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "2",
+ "metadata": {},
+ "source": [
+ "## Reduction function\n",
+ "\n",
+ "Events are sampled using their global index rather than sampling each detector group independently. This produces exactly $\\lfloor N / 50 \\rfloor$ events while preserving their original detector groups and metadata."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def shrink_skadi_mcstas(\n",
+ " source: Path, destination: Path, *, factor: int\n",
+ ") -> dict[str, int | Path]:\n",
+ " \"\"\"Subsample events and retain only geometry needed by the SKADI loader.\"\"\"\n",
+ " if factor < 1:\n",
+ " raise ValueError(\"factor must be at least 1\")\n",
+ " if destination.exists():\n",
+ " raise FileExistsError(f\"Destination already exists: {destination}\")\n",
+ " h5repack = shutil.which(\"h5repack\")\n",
+ " if h5repack is None:\n",
+ " raise RuntimeError(\"h5repack must be available on PATH\")\n",
+ "\n",
+ " destination.parent.mkdir(parents=True, exist_ok=True)\n",
+ " with tempfile.TemporaryDirectory(\n",
+ " prefix=\"shrink-skadi-mcstas-\", dir=destination.parent\n",
+ " ) as tmpdir:\n",
+ " working_copy = Path(tmpdir) / source.name\n",
+ " repacked = Path(tmpdir) / f\"repacked-{source.name}\"\n",
+ " shutil.copyfile(source, working_copy)\n",
+ "\n",
+ " original_event_count = 0\n",
+ " retained_event_count = 0\n",
+ " with h5py.File(working_copy, \"r+\") as file:\n",
+ " data_groups = file[\"entry1/data\"]\n",
+ " for name, group in list(data_groups.items()):\n",
+ " if not isinstance(group, h5py.Group) or \"events\" not in group:\n",
+ " del data_groups[name]\n",
+ " event_groups = list(data_groups.values())\n",
+ " required_component_names = {\n",
+ " group.attrs[\"component\"].decode()\n",
+ " if isinstance(group.attrs[\"component\"], bytes)\n",
+ " else group.attrs[\"component\"]\n",
+ " for group in event_groups\n",
+ " }\n",
+ " required_component_names.add(\"sample_position\")\n",
+ "\n",
+ " components = file[\"entry1/instrument/components\"]\n",
+ " component_names = {\n",
+ " name.split(\"_\", maxsplit=1)[-1]\n",
+ " for name, group in components.items()\n",
+ " if isinstance(group, h5py.Group)\n",
+ " }\n",
+ " missing = required_component_names - component_names\n",
+ " if missing:\n",
+ " raise ValueError(\n",
+ " \"Required instrument components are missing: \"\n",
+ " + \", \".join(sorted(missing))\n",
+ " )\n",
+ "\n",
+ " for name in list(components):\n",
+ " component_name = name.split(\"_\", maxsplit=1)[-1]\n",
+ " component = components[name]\n",
+ " if component_name not in required_component_names:\n",
+ " del components[name]\n",
+ " continue\n",
+ "\n",
+ " for child in list(component):\n",
+ " if child not in {\"Position\", \"Rotation\"}:\n",
+ " del component[child]\n",
+ "\n",
+ " for group in event_groups:\n",
+ " events = group[\"events\"]\n",
+ " event_count = events.shape[0]\n",
+ " start = (factor - 1 - original_event_count % factor) % factor\n",
+ " retained = events[start::factor]\n",
+ " attrs = dict(events.attrs)\n",
+ " del group[\"events\"]\n",
+ " events = group.create_dataset(\"events\", data=retained)\n",
+ " events.attrs.update(attrs)\n",
+ " for attr in (\n",
+ " \"signal\",\n",
+ " \"statistics\",\n",
+ " \"target\",\n",
+ " \"type\",\n",
+ " \"values\",\n",
+ " \"xylimits\",\n",
+ " ):\n",
+ " if attr in group.attrs:\n",
+ " del group.attrs[attr]\n",
+ "\n",
+ " original_event_count += event_count\n",
+ " retained_event_count += retained.shape[0]\n",
+ "\n",
+ " expected_event_count = original_event_count // factor\n",
+ " if retained_event_count != expected_event_count:\n",
+ " raise RuntimeError(\n",
+ " f\"Expected {expected_event_count} retained events, got \"\n",
+ " f\"{retained_event_count}\"\n",
+ " )\n",
+ "\n",
+ " subprocess.run( # noqa: S603\n",
+ " [h5repack, str(working_copy), str(repacked)],\n",
+ " check=True,\n",
+ " )\n",
+ " shutil.move(repacked, destination)\n",
+ "\n",
+ " return {\n",
+ " \"source_events\": original_event_count,\n",
+ " \"retained_events\": retained_event_count,\n",
+ " \"output_bytes\": destination.stat().st_size,\n",
+ " \"destination\": destination,\n",
+ " }"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "4",
+ "metadata": {},
+ "source": [
+ "## Create the tutorial file\n",
+ "\n",
+ "The path setup works when Jupyter is launched from either the repository root, the `esssans` package directory, or this `tools` directory."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "working_directory = Path.cwd()\n",
+ "if (working_directory / \"packages/esssans\").is_dir():\n",
+ " package_root = working_directory / \"packages/esssans\"\n",
+ "elif working_directory.name == \"tools\":\n",
+ " package_root = working_directory.parent\n",
+ "else:\n",
+ " package_root = working_directory\n",
+ "\n",
+ "data_directory = package_root / \"SKADI_example_data\"\n",
+ "source = data_directory / \"all_banks_1e8_mpi4_sample10/mccode.h5\"\n",
+ "destination = data_directory / \"skadi_mcstas_1e8_sample10_1_of_50.h5\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "shrink_skadi_mcstas(source, destination, factor=50)"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "name": "python"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/pixi.lock b/pixi.lock
index 657c07c42..843b70c7e 100644
--- a/pixi.lock
+++ b/pixi.lock
@@ -6,13 +6,13 @@ platforms:
- __linux=4.18
- __glibc=2.28
- __archspec=0=x86_64
-- name: p1
+- name: osx-64-macos-14-0
subdir: osx-64
virtual-packages:
- __osx=14.0
- __unix=0=0
- __archspec=0=x86_64
-- name: p2
+- name: osx-arm64-macos-14-0
subdir: osx-arm64
virtual-packages:
- __osx=14.0
@@ -292,7 +292,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -509,7 +509,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -1205,7 +1205,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -1411,7 +1411,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -2078,7 +2078,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -2275,7 +2275,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -2925,7 +2925,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -3123,7 +3123,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -3761,7 +3761,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -3948,7 +3948,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -4582,7 +4582,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -4784,7 +4784,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -5440,7 +5440,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -5634,7 +5634,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -6269,7 +6269,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f8/9b/c108cdb55560eaf253f0cbdb61b29971e9fb34d9c3499b0e96e4e60ed8a5/pyzmq-27.1.0-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -6458,7 +6458,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/d8/502954a4ec0efcf264f99b65b41c3c54e65a647d9f0d6f62cd02227d242c/ipykernel-6.31.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -7036,7 +7036,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -7177,7 +7177,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -7661,7 +7661,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -7803,7 +7803,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -8282,7 +8282,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -8417,7 +8417,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -8872,7 +8872,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -9000,7 +9000,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -9447,7 +9447,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -9578,7 +9578,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -10035,7 +10035,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -10170,7 +10170,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -10629,7 +10629,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -10758,7 +10758,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/f6/c5/db2ac3685833d626c0dcae6bd2330cd68433e1fd248d15f70998160d3ad7/numba-0.65.1.tar.gz
- pypi: https://files.pythonhosted.org/packages/fb/12/5911ae3eeec47800503a238d971e51722ccea5feb8569b735184d5fcdbc0/toolz-1.1.0-py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/adwaita-icon-theme-49.0-unix_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/font-ttf-dejavu-sans-mono-2.37-hab24e00_0.tar.bz2
@@ -11063,7 +11063,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl
- p1:
+ osx-64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-64/bzip2-1.0.8-h500dc9f_9.conda
@@ -11089,7 +11089,7 @@ environments:
- pypi: https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl
- pypi: https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl
- pypi: https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl
- p2:
+ osx-arm64-macos-14-0:
- conda: https://conda.anaconda.org/conda-forge/noarch/ca-certificates-2026.5.20-hbd8a1cb_0.conda
- conda: https://conda.anaconda.org/conda-forge/noarch/tzdata-2025c-hc9c84f9_1.conda
- conda: https://conda.anaconda.org/conda-forge/osx-arm64/bzip2-1.0.8-hd037594_9.conda
@@ -14460,7 +14460,7 @@ packages:
- spglib>=2.0.0,!=2.7
- pandas>=2.1.2 ; extra == 'test'
- pooch>=1.5 ; extra == 'test'
- - pytest>=7.0 ; extra == 'test'
+ - pytest>=9.0 ; extra == 'test'
- ipywidgets>=8.1.7 ; extra == 'test'
- anywidget>=0.8.0 ; extra == 'test'
- pythreejs>=2.4.1 ; extra == 'test'
@@ -14490,7 +14490,7 @@ packages:
- tifffile>=2024.7.2
- essreduce>=26.6.2
- scitiff>=26.6
- - pytest>=8.0 ; extra == 'test'
+ - pytest>=9.0 ; extra == 'test'
- pooch>=1.5 ; extra == 'test'
- tof>=26.3.0 ; extra == 'test'
- scitiff>=24.6.0 ; extra == 'test'
@@ -14523,7 +14523,7 @@ packages:
- tof>=25.12.1
- numpy>=2.0.0
- tof>=26.6.0
- - pytest>=8.0 ; extra == 'test'
+ - pytest>=9.0 ; extra == 'test'
- bitshuffle>=0.5.2 ; os_name == 'posix' and extra == 'test'
- autodoc-pydantic>=2.0.0 ; extra == 'docs'
- ipykernel>=6.20.0 ; extra == 'docs'
@@ -14553,7 +14553,7 @@ packages:
- matplotlib>=3.10.7 ; extra == 'test'
- numba>=0.63 ; extra == 'test'
- pooch>=1.9.0 ; extra == 'test'
- - pytest>=7.0 ; extra == 'test'
+ - pytest>=9.0 ; extra == 'test'
- tof>=25.12.0 ; extra == 'test'
- pandas>=2.1.2 ; extra == 'test'
- autodoc-pydantic>=2.0.0 ; extra == 'docs'
@@ -14585,7 +14585,7 @@ packages:
- essreduce>=26.6.0
- esspolarization>=25.7.0
- pandas>=2.1.2
- - pytest>=7.0 ; extra == 'test'
+ - pytest>=9.0 ; extra == 'test'
- pooch>=1.5 ; extra == 'test'
- ipywidgets>=8.1.7 ; extra == 'gui'
- ipydatagrid>=1.3.0 ; extra == 'gui'
@@ -14615,12 +14615,13 @@ packages:
requires_dist:
- dask>=2022.1.0
- graphviz>=0.20
+ - h5py>=3.11
- essreduce>=26.6.0
- numpy>=1.26.4
- pandas>=2.1.2
- plopp>=26.5.0
- ipympl>=0.9.4 ; extra == 'test'
- - pytest>=7.0 ; extra == 'test'
+ - pytest>=9.0 ; extra == 'test'
- pooch>=1.5 ; extra == 'test'
- scipy>=1.15.0 ; extra == 'test'
- anywidget>=0.8.0 ; extra == 'test'
@@ -14648,7 +14649,7 @@ packages:
- pandas>=2.1.2
- tof>=25.11.0
- pooch>=1.5 ; extra == 'test'
- - pytest>=7.0 ; extra == 'test'
+ - pytest>=9.0 ; extra == 'test'
- autodoc-pydantic>=2.0.0 ; extra == 'docs'
- ipykernel>=6.20.0 ; extra == 'docs'
- ipympl>=0.9.0 ; extra == 'docs'