Skip to content

Commit aecd398

Browse files
committed
refactor: move orso providers to generic module
1 parent b4053c2 commit aecd398

File tree

3 files changed

+116
-210
lines changed

3 files changed

+116
-210
lines changed

src/ess/amor/orso.py

Lines changed: 9 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,16 @@
11
# SPDX-License-Identifier: BSD-3-Clause
22
# Copyright (c) 2024 Scipp contributors (https://github.com/scipp)
3-
"""ORSO utilities for Amor."""
3+
from ess.reflectometry.orso import OrsoCorrectionList
44

5-
import numpy as np
6-
import scipp as sc
7-
from orsopy.fileio import base as orso_base
8-
from orsopy.fileio import data_source as orso_data_source
9-
from orsopy.fileio.orso import Column, Orso, OrsoDataset
105

11-
from ..reflectometry.orso import (
12-
OrsoDataSource,
13-
OrsoInstrument,
14-
OrsoIofQDataset,
15-
OrsoReduction,
16-
)
17-
from ..reflectometry.types import ReflectivityOverQ
18-
19-
20-
def build_orso_instrument(events: ReflectivityOverQ) -> OrsoInstrument:
21-
"""Build ORSO instrument metadata from intermediate reduction results for Amor.
22-
23-
This assumes specular reflection and sets the incident angle equal to the computed
24-
scattering angle.
25-
"""
26-
return OrsoInstrument(
27-
orso_data_source.InstrumentSettings(
28-
wavelength=orso_base.ValueRange(*_limits_of_coord(events, "wavelength")),
29-
incident_angle=orso_base.ValueRange(*_limits_of_coord(events, "theta")),
30-
polarization=None, # TODO how can we determine this from the inputs?
31-
)
32-
)
33-
34-
35-
def build_orso_iofq_dataset(
36-
iofq: ReflectivityOverQ,
37-
data_source: OrsoDataSource,
38-
reduction: OrsoReduction,
39-
) -> OrsoIofQDataset:
40-
"""Build an ORSO dataset for reduced I-of-Q data and associated metadata."""
41-
header = Orso(
42-
data_source=data_source,
43-
reduction=reduction,
44-
columns=[
45-
Column("Qz", "1/angstrom", "wavevector transfer"),
46-
Column("R", None, "reflectivity"),
47-
Column("sR", None, "standard deviation of reflectivity"),
48-
Column(
49-
"sQz",
50-
"1/angstrom",
51-
"standard deviation of wavevector transfer resolution",
52-
),
53-
],
6+
def orso_amor_corrections() -> OrsoCorrectionList:
7+
return OrsoCorrectionList(
8+
[
9+
"chopper ToF correction",
10+
"footprint correction",
11+
"supermirror calibration",
12+
]
5413
)
55-
iofq = iofq.hist()
56-
57-
qz = iofq.coords["Q"].to(unit="1/angstrom", copy=False)
58-
if iofq.coords.is_edges("Q"):
59-
qz = sc.midpoints(qz)
60-
r = sc.values(iofq.data)
61-
sr = sc.stddevs(iofq.data)
62-
sqz = iofq.coords["Q_resolution"].to(unit="1/angstrom", copy=False)
63-
64-
data = np.column_stack(tuple(map(_extract_values_array, (qz, r, sr, sqz))))
65-
data = data[np.isfinite(data).all(axis=-1)]
66-
ds = OrsoIofQDataset(OrsoDataset(header, data))
67-
ds.info.reduction.corrections = [
68-
"chopper ToF correction",
69-
"footprint correction",
70-
"supermirror calibration",
71-
]
72-
return ds
73-
74-
75-
def _extract_values_array(var: sc.Variable) -> np.ndarray:
76-
if var.variances is not None:
77-
raise sc.VariancesError(
78-
"ORT columns must not have variances. "
79-
"Store the uncertainties as standard deviations in a separate column."
80-
)
81-
if var.ndim != 1:
82-
raise sc.DimensionError(f"ORT columns must be one-dimensional, got {var.sizes}")
83-
return var.values
84-
85-
86-
def _limits_of_coord(data: sc.DataArray, name: str) -> tuple[float, float, str] | None:
87-
if (coord := _get_coord(data, name)) is None:
88-
return None
89-
min_ = coord.min().value
90-
max_ = coord.max().value
91-
# Explicit conversions to float because orsopy does not like np.float* types.
92-
return float(min_), float(max_), _ascii_unit(coord.unit)
93-
94-
95-
def _get_coord(data: sc.DataArray, name: str) -> sc.Variable | None:
96-
if name in data.coords:
97-
return sc.DataArray(data=data.coords[name], masks=data.masks)
98-
if (data.bins is not None) and (name in data.bins.coords):
99-
# Note that .bins.concat() applies the top-level masks
100-
events = data.bins.concat().value
101-
return sc.DataArray(data=events.coords[name], masks=events.masks)
102-
return None
103-
104-
105-
def _ascii_unit(unit: sc.Unit) -> str:
106-
unit = str(unit)
107-
if unit == "Å":
108-
return "angstrom"
109-
return unit
11014

11115

112-
providers = (build_orso_instrument, build_orso_iofq_dataset)
16+
providers = (orso_amor_corrections,)

src/ess/estia/orso.py

Lines changed: 9 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,112 +1,16 @@
11
# SPDX-License-Identifier: BSD-3-Clause
22
# Copyright (c) 2025 Scipp contributors (https://github.com/scipp)
3-
"""ORSO utilities for Estia."""
3+
from ess.reflectometry.orso import OrsoCorrectionList
44

5-
import numpy as np
6-
import scipp as sc
7-
from orsopy.fileio import base as orso_base
8-
from orsopy.fileio import data_source as orso_data_source
9-
from orsopy.fileio.orso import Column, Orso, OrsoDataset
105

11-
from ..reflectometry.orso import (
12-
OrsoDataSource,
13-
OrsoInstrument,
14-
OrsoIofQDataset,
15-
OrsoReduction,
16-
)
17-
from ..reflectometry.types import ReducibleData, ReflectivityOverQ, SampleRun
18-
19-
20-
def build_orso_instrument(events: ReducibleData[SampleRun]) -> OrsoInstrument:
21-
"""Build ORSO instrument metadata from intermediate reduction results for Estia.
22-
23-
This assumes specular reflection and sets the incident angle equal to the computed
24-
scattering angle.
25-
"""
26-
return OrsoInstrument(
27-
orso_data_source.InstrumentSettings(
28-
wavelength=orso_base.ValueRange(*_limits_of_coord(events, "wavelength")),
29-
incident_angle=orso_base.ValueRange(*_limits_of_coord(events, "theta")),
30-
polarization=None, # TODO how can we determine this from the inputs?
31-
)
32-
)
33-
34-
35-
def build_orso_iofq_dataset(
36-
iofq: ReflectivityOverQ,
37-
data_source: OrsoDataSource,
38-
reduction: OrsoReduction,
39-
) -> OrsoIofQDataset:
40-
"""Build an ORSO dataset for reduced I-of-Q data and associated metadata."""
41-
header = Orso(
42-
data_source=data_source,
43-
reduction=reduction,
44-
columns=[
45-
Column("Qz", "1/angstrom", "wavevector transfer"),
46-
Column("R", None, "reflectivity"),
47-
Column("sR", None, "standard deviation of reflectivity"),
48-
Column(
49-
"sQz",
50-
"1/angstrom",
51-
"standard deviation of wavevector transfer resolution",
52-
),
53-
],
6+
def orso_estia_corrections() -> OrsoCorrectionList:
7+
return OrsoCorrectionList(
8+
[
9+
"chopper ToF correction",
10+
"footprint correction",
11+
"supermirror calibration",
12+
]
5413
)
55-
iofq = iofq.hist()
56-
57-
qz = iofq.coords["Q"].to(unit="1/angstrom", copy=False)
58-
if iofq.coords.is_edges("Q"):
59-
qz = sc.midpoints(qz)
60-
r = sc.values(iofq.data)
61-
sr = sc.stddevs(iofq.data)
62-
sqz = iofq.coords["Q_resolution"].to(unit="1/angstrom", copy=False)
63-
64-
data = np.column_stack(tuple(map(_extract_values_array, (qz, r, sr, sqz))))
65-
data = data[np.isfinite(data).all(axis=-1)]
66-
ds = OrsoIofQDataset(OrsoDataset(header, data))
67-
ds.info.reduction.corrections = [
68-
"chopper ToF correction",
69-
"footprint correction",
70-
"supermirror calibration",
71-
]
72-
return ds
73-
74-
75-
def _extract_values_array(var: sc.Variable) -> np.ndarray:
76-
if var.variances is not None:
77-
raise sc.VariancesError(
78-
"ORT columns must not have variances. "
79-
"Store the uncertainties as standard deviations in a separate column."
80-
)
81-
if var.ndim != 1:
82-
raise sc.DimensionError(f"ORT columns must be one-dimensional, got {var.sizes}")
83-
return var.values
84-
85-
86-
def _limits_of_coord(data: sc.DataArray, name: str) -> tuple[float, float, str] | None:
87-
if (coord := _get_coord(data, name)) is None:
88-
return None
89-
min_ = coord.min().value
90-
max_ = coord.max().value
91-
# Explicit conversions to float because orsopy does not like np.float* types.
92-
return float(min_), float(max_), _ascii_unit(coord.unit)
93-
94-
95-
def _get_coord(data: sc.DataArray, name: str) -> sc.Variable | None:
96-
if name in data.coords:
97-
return sc.DataArray(data=data.coords[name], masks=data.masks)
98-
if (data.bins is not None) and (name in data.bins.coords):
99-
# Note that .bins.concat() applies the top-level masks
100-
events = data.bins.concat().value
101-
return sc.DataArray(data=events.coords[name], masks=events.masks)
102-
return None
103-
104-
105-
def _ascii_unit(unit: sc.Unit) -> str:
106-
unit = str(unit)
107-
if unit == "Å":
108-
return "angstrom"
109-
return unit
11014

11115

112-
providers = (build_orso_instrument, build_orso_iofq_dataset)
16+
providers = (orso_estia_corrections,)

src/ess/reflectometry/orso.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,19 @@
1111
from datetime import datetime, timezone
1212
from typing import NewType
1313

14+
import numpy as np
15+
import scipp as sc
1416
from dateutil.parser import parse as parse_datetime
1517
from orsopy.fileio import base as orso_base
1618
from orsopy.fileio import data_source, orso, reduction
19+
from orsopy.fileio.orso import Column, Orso, OrsoDataset
1720

1821
from .load import load_nx
1922
from .types import (
2023
Filename,
24+
ReducibleData,
2125
ReferenceRun,
26+
ReflectivityOverQ,
2227
SampleRun,
2328
)
2429

@@ -52,6 +57,8 @@
5257
OrsoSampleFilenames = NewType("OrsoSampleFilenames", list[orso_base.File])
5358
"""Collection of filenames used to create the ORSO file"""
5459

60+
OrsoCorrectionList = NewType("OrsoCorrectionList", list[str])
61+
5562

5663
def parse_orso_experiment(filename: Filename[SampleRun]) -> OrsoExperiment:
5764
"""Parse ORSO experiment metadata from raw NeXus data."""
@@ -171,6 +178,95 @@ def build_orso_data_source(
171178
)
172179

173180

181+
def build_orso_instrument(events: ReducibleData[SampleRun]) -> OrsoInstrument:
182+
"""Build ORSO instrument metadata from intermediate reduction results.
183+
184+
This assumes specular reflection and sets the incident angle equal to the computed
185+
scattering angle.
186+
"""
187+
return OrsoInstrument(
188+
data_source.InstrumentSettings(
189+
wavelength=orso_base.ValueRange(*_limits_of_coord(events, "wavelength")),
190+
incident_angle=orso_base.ValueRange(*_limits_of_coord(events, "theta")),
191+
polarization=None, # TODO how can we determine this from the inputs?
192+
)
193+
)
194+
195+
196+
def build_orso_iofq_dataset(
197+
iofq: ReflectivityOverQ,
198+
data_source: OrsoDataSource,
199+
reduction: OrsoReduction,
200+
corrections: OrsoCorrectionList,
201+
) -> OrsoIofQDataset:
202+
"""Build an ORSO dataset for reduced I-of-Q data and associated metadata."""
203+
header = Orso(
204+
data_source=data_source,
205+
reduction=reduction,
206+
columns=[
207+
Column("Qz", "1/angstrom", "wavevector transfer"),
208+
Column("R", None, "reflectivity"),
209+
Column("sR", None, "standard deviation of reflectivity"),
210+
Column(
211+
"sQz",
212+
"1/angstrom",
213+
"standard deviation of wavevector transfer resolution",
214+
),
215+
],
216+
)
217+
iofq = iofq.hist()
218+
219+
qz = iofq.coords["Q"].to(unit="1/angstrom", copy=False)
220+
if iofq.coords.is_edges("Q"):
221+
qz = sc.midpoints(qz)
222+
r = sc.values(iofq.data)
223+
sr = sc.stddevs(iofq.data)
224+
sqz = iofq.coords["Q_resolution"].to(unit="1/angstrom", copy=False)
225+
226+
data = np.column_stack(tuple(map(_extract_values_array, (qz, r, sr, sqz))))
227+
data = data[np.isfinite(data).all(axis=-1)]
228+
ds = OrsoIofQDataset(OrsoDataset(header, data))
229+
ds.info.reduction.corrections = list(corrections)
230+
return ds
231+
232+
233+
def _extract_values_array(var: sc.Variable) -> np.ndarray:
234+
if var.variances is not None:
235+
raise sc.VariancesError(
236+
"ORT columns must not have variances. "
237+
"Store the uncertainties as standard deviations in a separate column."
238+
)
239+
if var.ndim != 1:
240+
raise sc.DimensionError(f"ORT columns must be one-dimensional, got {var.sizes}")
241+
return var.values
242+
243+
244+
def _limits_of_coord(data: sc.DataArray, name: str) -> tuple[float, float, str] | None:
245+
if (coord := _get_coord(data, name)) is None:
246+
return None
247+
min_ = coord.min().value
248+
max_ = coord.max().value
249+
# Explicit conversions to float because orsopy does not like np.float* types.
250+
return float(min_), float(max_), _ascii_unit(coord.unit)
251+
252+
253+
def _get_coord(data: sc.DataArray, name: str) -> sc.Variable | None:
254+
if name in data.coords:
255+
return sc.DataArray(data=data.coords[name], masks=data.masks)
256+
if (data.bins is not None) and (name in data.bins.coords):
257+
# Note that .bins.concat() applies the top-level masks
258+
events = data.bins.concat().value
259+
return sc.DataArray(data=events.coords[name], masks=events.masks)
260+
return None
261+
262+
263+
def _ascii_unit(unit: sc.Unit) -> str:
264+
unit = str(unit)
265+
if unit == "Å":
266+
return "angstrom"
267+
return unit
268+
269+
174270
providers = (
175271
build_orso_data_source,
176272
build_orso_measurement,
@@ -179,4 +275,6 @@ def build_orso_data_source(
179275
parse_orso_owner,
180276
parse_orso_sample,
181277
orso_data_files,
278+
build_orso_instrument,
279+
build_orso_iofq_dataset,
182280
)

0 commit comments

Comments
 (0)