Skip to content

Commit 7058109

Browse files
authored
Merge pull request #4 from scipp/reflectometry-base
Add reflectometry related code from ess
2 parents 7609614 + 0b7c12b commit 7058109

28 files changed

+2022
-4
lines changed

.copier-answers.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ _src_path: gh:scipp/copier_template
44
description: Reflectometry data reduction for the European Spallation Source
55
github_linux_image: ubuntu-20.04
66
max_python: '3.11'
7-
min_python: '3.8'
7+
min_python: '3.10'
88
orgname: scipp
99
projectname: essreflectometry
1010
year: 2023

docs/developer/getting-started.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ Alternatively, if you want a different workflow, take a look at ``tox.ini`` or `
4040
Run the tests using
4141
4242
```sh
43-
tox -e py38
43+
tox -e py310
4444
```
4545
4646
(or just `tox` if you want to run all environments).
@@ -88,4 +88,4 @@ python -m sphinx -v -b doctest -d build/.doctrees docs build/html
8888
python -m sphinx -v -b linkcheck -d build/.doctrees docs build/html
8989
```
9090
````
91-
`````
91+
`````

pyproject.toml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,14 @@ classifiers = [
2727
]
2828
requires-python = ">=3.8"
2929
dependencies = [
30+
"dask",
31+
"graphviz",
32+
"plopp",
33+
"pythreejs",
34+
"orsopy",
35+
"sciline>=23.9.1",
36+
"scipp>=23.8.0",
37+
"scippneutron>=23.9.0",
3038
]
3139
dynamic = ["version"]
3240

@@ -42,6 +50,8 @@ addopts = "-ra -v"
4250
testpaths = "tests"
4351
filterwarnings = [
4452
"error",
53+
'ignore:\n.*Sentinel is not a public part of the traitlets API.*:DeprecationWarning',
54+
"ignore:.*metadata to be logged in the data array, it is necessary to install the orsopy package.:UserWarning",
4555
]
4656

4757
[tool.bandit]
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
3+
# flake8: noqa: F401
4+
from . import calibrations, conversions, data, normalize, resolution, tools
5+
from .beamline import instrument_view_components, make_beamline
6+
from .instrument_view import instrument_view
7+
from .load import load
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
3+
import scipp as sc
4+
from scipp.constants import g
5+
6+
from ..choppers import make_chopper
7+
from ..logging import log_call
8+
9+
10+
@log_call(
11+
instrument='amor', message='Constructing AMOR beamline from default parameters'
12+
)
13+
def make_beamline(
14+
sample_rotation: sc.Variable,
15+
beam_size: sc.Variable = None,
16+
sample_size: sc.Variable = None,
17+
detector_spatial_resolution: sc.Variable = None,
18+
gravity: sc.Variable = None,
19+
chopper_frequency: sc.Variable = None,
20+
chopper_phase: sc.Variable = None,
21+
chopper_1_position: sc.Variable = None,
22+
chopper_2_position: sc.Variable = None,
23+
) -> dict:
24+
"""
25+
Amor beamline components.
26+
27+
Parameters
28+
----------
29+
sample_rotation:
30+
Sample rotation (omega) angle.
31+
beam_size:
32+
Size of the beam perpendicular to the scattering surface. Default is `0.001 m`.
33+
sample_size:
34+
Size of the sample in direction of the beam. Default :code:`0.01 m`.
35+
detector_spatial_resolution:
36+
Spatial resolution of the detector. Default is `2.5 mm`.
37+
gravity:
38+
Vector representing the direction and magnitude of the Earth's gravitational
39+
field. Default is `[0, -g, 0]`.
40+
chopper_frequency:
41+
Rotational frequency of the chopper. Default is `6.6666... Hz`.
42+
chopper_phase:
43+
Phase offset between chopper pulse and ToF zero. Default is `-8. degrees of
44+
arc`.
45+
chopper_position:
46+
Position of the chopper. Default is `-15 m`.
47+
48+
Returns
49+
-------
50+
:
51+
A dict.
52+
"""
53+
if beam_size is None:
54+
beam_size = 2.0 * sc.units.mm
55+
if sample_size is None:
56+
sample_size = 10.0 * sc.units.mm
57+
if detector_spatial_resolution is None:
58+
detector_spatial_resolution = 0.0025 * sc.units.m
59+
if gravity is None:
60+
gravity = sc.vector(value=[0, -1, 0]) * g
61+
if chopper_frequency is None:
62+
chopper_frequency = sc.scalar(20 / 3, unit='Hz')
63+
if chopper_phase is None:
64+
chopper_phase = sc.scalar(-8.0, unit='deg')
65+
if chopper_1_position is None:
66+
chopper_1_position = sc.vector(value=[0, 0, -15.5], unit='m')
67+
if chopper_2_position is None:
68+
chopper_2_position = sc.vector(value=[0, 0, -14.5], unit='m')
69+
beamline = {
70+
'sample_rotation': sample_rotation,
71+
'beam_size': beam_size,
72+
'sample_size': sample_size,
73+
'detector_spatial_resolution': detector_spatial_resolution,
74+
'gravity': gravity,
75+
}
76+
# TODO: in scn.load_nexus, the chopper parameters are stored as coordinates
77+
# of a DataArray, and the data value is a string containing the name of the
78+
# chopper. This does not allow storing e.g. chopper cutout angles.
79+
# We should change this to be a Dataset, which is what we do here.
80+
beamline["source_chopper_2"] = sc.scalar(
81+
make_chopper(
82+
frequency=chopper_frequency,
83+
phase=chopper_phase,
84+
position=chopper_2_position,
85+
)
86+
)
87+
beamline["source_chopper_1"] = sc.scalar(
88+
make_chopper(
89+
frequency=chopper_frequency,
90+
phase=chopper_phase,
91+
position=chopper_1_position,
92+
)
93+
)
94+
return beamline
95+
96+
97+
@log_call(instrument='amor', level='DEBUG')
98+
def instrument_view_components(da: sc.DataArray) -> dict:
99+
"""
100+
Create a dict of instrument view components, containing:
101+
- the sample
102+
- the source chopper
103+
104+
Parameters
105+
----------
106+
da:
107+
The DataArray containing the sample and source chopper coordinates.
108+
109+
Returns
110+
-------
111+
:
112+
Dict of instrument view definitions.
113+
"""
114+
return {
115+
"sample": {
116+
'center': da.meta['sample_position'],
117+
'color': 'red',
118+
'size': sc.vector(value=[0.2, 0.01, 0.2], unit=sc.units.m),
119+
'type': 'box',
120+
},
121+
"source_chopper_2": {
122+
'center': da.meta['source_chopper_2'].value["position"].data,
123+
'color': 'blue',
124+
'size': sc.vector(value=[0.5, 0, 0], unit=sc.units.m),
125+
'type': 'disk',
126+
},
127+
"source_chopper_1": {
128+
'center': da.meta['source_chopper_1'].value["position"].data,
129+
'color': 'blue',
130+
'size': sc.vector(value=[0.5, 0, 0], unit=sc.units.m),
131+
'type': 'disk',
132+
},
133+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
3+
import scipp as sc
4+
5+
from ..reflectometry import orso
6+
7+
8+
def supermirror_calibration(
9+
data_array: sc.DataArray,
10+
m_value: sc.Variable = None,
11+
critical_edge: sc.Variable = None,
12+
alpha: sc.Variable = None,
13+
) -> sc.Variable:
14+
"""
15+
Calibrate supermirror measurements
16+
17+
Parameters
18+
----------
19+
data_array:
20+
Data array to get q-bins/values from.
21+
m_value:
22+
m-value for the supermirror. Defaults to 5.
23+
critical_edge:
24+
Supermirror critical edge. Defaults to 0.022 1/angstrom.
25+
alpha:
26+
Supermirror alpha value. Defaults to 0.25 / 0.088 angstrom.
27+
28+
Returns
29+
-------
30+
:
31+
Calibrated supermirror measurement.
32+
"""
33+
if m_value is None:
34+
m_value = sc.scalar(5, unit=sc.units.dimensionless)
35+
if critical_edge is None:
36+
critical_edge = 0.022 * sc.Unit('1/angstrom')
37+
if alpha is None:
38+
alpha = sc.scalar(0.25 / 0.088, unit=sc.units.angstrom)
39+
calibration = calibration_factor(data_array, m_value, critical_edge, alpha)
40+
data_array_cal = data_array * calibration
41+
try:
42+
data_array_cal.attrs['orso'].value.reduction.corrections += [
43+
'supermirror calibration'
44+
]
45+
except KeyError:
46+
orso.not_found_warning()
47+
return data_array_cal
48+
49+
50+
def calibration_factor(
51+
data_array: sc.DataArray,
52+
m_value: sc.Variable = None,
53+
critical_edge: sc.Variable = None,
54+
alpha: sc.Variable = None,
55+
) -> sc.Variable:
56+
"""
57+
Return the calibration factor for the supermirror.
58+
59+
Parameters
60+
----------
61+
data_array:
62+
Data array to get q-bins/values from.
63+
m_value:
64+
m-value for the supermirror. Defaults to 5.
65+
critical_edge:
66+
Supermirror critical edge. Defaults to 0.022 1/angstrom.
67+
alpha:
68+
Supermirror alpha value. Defaults to 0.25 / 0.088 angstrom.
69+
70+
Returns
71+
-------
72+
:
73+
Calibration factor at the midpoint of each Q-bin.
74+
"""
75+
if m_value is None:
76+
m_value = sc.scalar(5, unit=sc.units.dimensionless)
77+
if critical_edge is None:
78+
critical_edge = 0.022 * sc.Unit('1/angstrom')
79+
if alpha is None:
80+
alpha = sc.scalar(0.25 / 0.088, unit=sc.units.angstrom)
81+
q = data_array.coords['Q']
82+
if data_array.coords.is_edges('Q'):
83+
q = sc.midpoints(q)
84+
max_q = m_value * critical_edge
85+
lim = (q < critical_edge).astype(float)
86+
lim.unit = 'one'
87+
nq = 1.0 / (1.0 - alpha * (q - critical_edge))
88+
calibration_factor = sc.where(q < max_q, lim + (1 - lim) * nq, sc.scalar(1.0))
89+
return calibration_factor
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
3+
import scipp as sc
4+
5+
from ..reflectometry.conversions import specular_reflection as spec_relf_graph
6+
7+
8+
def incident_beam(
9+
*,
10+
source_chopper_1: sc.Variable,
11+
source_chopper_2: sc.Variable,
12+
sample_position: sc.Variable,
13+
) -> sc.Variable:
14+
"""
15+
Compute the incident beam vector from the source chopper position vector,
16+
instead of the source_position vector.
17+
"""
18+
chopper_midpoint = (
19+
source_chopper_1.value['position'].data
20+
+ source_chopper_2.value['position'].data
21+
) * sc.scalar(0.5)
22+
return sample_position - chopper_midpoint
23+
24+
25+
def specular_reflection() -> dict:
26+
"""
27+
Generate a coordinate transformation graph for Amor reflectometry.
28+
"""
29+
graph = spec_relf_graph()
30+
graph['incident_beam'] = incident_beam
31+
return graph

src/essreflectometry/amor/data.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
3+
_version = '1'
4+
5+
__all__ = ['get_path']
6+
7+
8+
def _make_pooch():
9+
import pooch
10+
11+
return pooch.create(
12+
path=pooch.os_cache('ess/amor'),
13+
env='ESS_AMOR_DATA_DIR',
14+
base_url='https://public.esss.dk/groups/scipp/ess/amor/{version}/',
15+
version=_version,
16+
registry={
17+
"reference.nxs": "md5:56d493c8051e1c5c86fb7a95f8ec643b",
18+
"sample.nxs": "md5:4e07ccc87b5c6549e190bc372c298e83",
19+
},
20+
)
21+
22+
23+
_pooch = _make_pooch()
24+
25+
26+
def get_path(name: str) -> str:
27+
"""
28+
Return the path to a data file bundled with scippneutron.
29+
30+
This function only works with example data and cannot handle
31+
paths to custom files.
32+
"""
33+
return _pooch.fetch(name)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# SPDX-License-Identifier: BSD-3-Clause
2+
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
3+
import scipp as sc
4+
import scippneutron as scn
5+
6+
from .beamline import instrument_view_components
7+
8+
9+
def instrument_view(
10+
da: sc.DataArray, components: dict = None, pixel_size: float = 0.0035, **kwargs
11+
):
12+
"""
13+
Instrument view for the Amor instrument, which automatically populates a list of
14+
additional beamline components, and sets the pixel size.
15+
16+
:param da: The input data for which to display the instrument view.
17+
:param components: A dict of additional components to display. By default, a
18+
set of components defined in `beamline.instrument_view_components()` are added.
19+
:param pixel_size: The detector pixel size. Default is 0.0035.
20+
"""
21+
default_components = instrument_view_components(da)
22+
if components is not None:
23+
default_components = {**default_components, **components}
24+
25+
return scn.instrument_view(
26+
da, components=default_components, pixel_size=pixel_size, **kwargs
27+
)

0 commit comments

Comments
 (0)