Divergent data reduction for Amor#

In this notebook, we will look at how to use the essreflectometry package with Sciline, for reflectometry data collected from the PSI instrument Amor in divergent beam mode.

We will begin by importing the modules that are necessary for this notebook.

Setup#

[1]:
import scipp as sc
from ess import amor
from ess.amor import data  # noqa: F401
from ess.reflectometry.types import *
from ess.amor.types import *

Create and configure the workflow#

We begin by creating the Amor workflow object which is a skeleton for reducing Amor data, with pre-configured steps.

[2]:
workflow = amor.AmorWorkflow()

We then need to set the missing parameters which are specific to each experiment:

[3]:
workflow[SampleSize[SampleRun]] = sc.scalar(10.0, unit='mm')
workflow[SampleSize[ReferenceRun]] = sc.scalar(10.0, unit='mm')

workflow[ChopperPhase[ReferenceRun]] = sc.scalar(-7.5, unit='deg')
workflow[ChopperPhase[SampleRun]] = sc.scalar(-7.5, unit='deg')

workflow[QBins] = sc.geomspace(dim='Q', start=0.005, stop=0.3, num=391, unit='1/angstrom')
workflow[WavelengthBins] = sc.geomspace('wavelength', 2.8, 12, 301, unit='angstrom')

# The YIndexLimits and ZIndexLimits define ranges on the detector where
# data is considered to be valid signal.
# They represent the lower and upper boundaries of a range of pixel indices.
workflow[YIndexLimits] = sc.scalar(11, unit=None), sc.scalar(41, unit=None)
workflow[ZIndexLimits] = sc.scalar(80, unit=None), sc.scalar(370, unit=None)
[4]:
workflow.visualize(NormalizedIofQ, graph_attr={'rankdir': 'LR'})
[4]:
../../_images/user-guide_amor_amor-reduction_7_0.svg

Caching the reference result#

The reference result (used for normalizing the sample data) only needs to be computed once. It represents the intensity reflected by the super-mirror.

We compute it using the pipeline and thereafter set the result back on the original pipeline.

[5]:
workflow[Filename[ReferenceRun]] = amor.data.amor_reference_run()
# The sample rotation value in the file is slightly off, so we set it manually
workflow[SampleRotation[ReferenceRun]] = sc.scalar(0.65, unit='deg')

reference_result = workflow.compute(IdealReferenceIntensity)
# Set the result back onto the pipeline to cache it
workflow[IdealReferenceIntensity] = reference_result
Downloading file 'amor2023n000614.hdf' from 'https://public.esss.dk/groups/scipp/ess/amor/2/amor2023n000614.hdf' to '/home/runner/.cache/ess/amor/2'.
/home/runner/work/essreflectometry/essreflectometry/.tox/docs/lib/python3.10/site-packages/scippnexus/nxtransformations.py:222: UserWarning: Failed to convert /entry1/Amor/detector/transformation/height into a transformation: A transformation needs a vector attribute. Falling back to returning underlying value.
  transform = Transformation(obj).make_transformation(
/home/runner/work/essreflectometry/essreflectometry/.tox/docs/lib/python3.10/site-packages/scippnexus/nxtransformations.py:222: UserWarning: Failed to convert /entry1/Amor/detector/transformation/rotation into a transformation: A transformation needs a vector attribute. Falling back to returning underlying value.
  transform = Transformation(obj).make_transformation(

If we now visualize the pipeline again, we can see that the reference is not re-computed:

[6]:
workflow.visualize(NormalizedIofQ, graph_attr={'rankdir': 'LR'})
[6]:
../../_images/user-guide_amor_amor-reduction_11_0.svg

Computing sample reflectivity#

We now compute the sample reflectivity from 3 runs that used different sample rotation angles. The different rotation angles cover different ranges in \(Q\).

[7]:
runs = {
    '608': sc.scalar(0.85, unit='deg'),
    '609': sc.scalar(2.25, unit='deg'),
    '610': sc.scalar(3.65, unit='deg'),
    '611': sc.scalar(5.05, unit='deg'),
}

results = {}
for file, angle in runs.items():
    workflow[Filename[SampleRun]] = amor.data.amor_sample_run(file)
    workflow[SampleRotation[SampleRun]] = angle
    results[file] = workflow.compute(NormalizedIofQ).hist()

sc.plot(results, norm='log', vmin=1e-4)
Downloading file 'amor2023n000608.hdf' from 'https://public.esss.dk/groups/scipp/ess/amor/2/amor2023n000608.hdf' to '/home/runner/.cache/ess/amor/2'.
Downloading file 'amor2023n000609.hdf' from 'https://public.esss.dk/groups/scipp/ess/amor/2/amor2023n000609.hdf' to '/home/runner/.cache/ess/amor/2'.
Downloading file 'amor2023n000610.hdf' from 'https://public.esss.dk/groups/scipp/ess/amor/2/amor2023n000610.hdf' to '/home/runner/.cache/ess/amor/2'.
Downloading file 'amor2023n000611.hdf' from 'https://public.esss.dk/groups/scipp/ess/amor/2/amor2023n000611.hdf' to '/home/runner/.cache/ess/amor/2'.
[7]:
../../_images/user-guide_amor_amor-reduction_13_1.svg
[8]:
from ess.reflectometry.tools import scale_reflectivity_curves_to_overlap
results_scaled = dict(zip(
    results.keys(),
    scale_reflectivity_curves_to_overlap(results.values()),
    strict=True
))
sc.plot(results_scaled, norm='log', vmin=1e-5)
[8]:
../../_images/user-guide_amor_amor-reduction_14_0.svg
[9]:
from ess.reflectometry.tools import combine_curves
combine_curves(results_scaled.values(), workflow.compute(QBins)).plot(norm='log')
[9]:
../../_images/user-guide_amor_amor-reduction_15_0.svg

Additional diagnostics plots#

[10]:
workflow[Filename[SampleRun]] = amor.data.amor_sample_run(608)
workflow[SampleRotation[SampleRun]] = sc.scalar(0.85, unit='deg')
workflow.compute(ReflectivityDiagnosticsView)
[10]:
../../_images/user-guide_amor_amor-reduction_17_0.svg

Make a \((\lambda, \theta)\) map#

A good sanity check is to create a two-dimensional map of the counts in \(\lambda\) and \(\theta\) bins and make sure the triangles converge at the origin.

[11]:
workflow.compute(WavelengthThetaFigure)
[11]:
../../_images/user-guide_amor_amor-reduction_19_0.svg

This plot can be used to check if the value of the sample rotation angle \(\omega\) is correct. The bright triangles should be pointing back to the origin \(\lambda = \theta = 0\). In the figure above the black lines are all passing through the origin.

Save data#

We can save the computed \(I(Q)\) to an ORSO .ort file using the orsopy package.

First, we need to collect the metadata for that file. To this end, we build a pipeline with additional providers. We also insert a parameter to indicate the creator of the processed data.

[12]:
from ess.reflectometry import orso
from orsopy import fileio
[13]:
workflow[orso.OrsoCreator] = orso.OrsoCreator(
    fileio.base.Person(
        name='Max Mustermann',
        affiliation='European Spallation Source ERIC',
        contact='max.mustermann@ess.eu',
    )
)
[14]:
workflow.visualize(orso.OrsoIofQDataset, graph_attr={'rankdir': 'LR'})
[14]:
../../_images/user-guide_amor_amor-reduction_24_0.svg

We build our ORSO dataset from the computed \(I(Q)\) and the ORSO metadata:

[15]:
iofq_dataset = workflow.compute(orso.OrsoIofQDataset)
iofq_dataset
[15]:
OrsoDataset(info=Orso(
     data_source=DataSource(
                            owner=Person(name='J. Stahn', affiliation=None, contact='', comment=None),
                            experiment=Experiment(title='commissioning', instrument='Amor', start_date=datetime.datetime(2023, 11, 13, 13, 27, 8), probe='neutron', facility=None, proposalID=None, doi=None, comment=None),
                            sample=Sample(name='SM5', category=None, composition=None, description=None, size=None, environment=None, sample_parameters=None, model=SampleModel(stack='air | 20 ( Ni 7 | Ti 7 ) | Al2O3', origin=None, sub_stacks=None, layers=None, materials=None, composits=None, globals=None, reference=None, comment=None), comment=None),
                            measurement=Measurement(
                                                    instrument_settings=InstrumentSettings(
                                                                                           incident_angle=ValueRange(min=0.004563767609003476, max=0.028431498466197823, unit='rad', individual_magnitudes=None, offset=None, comment=None),
                                                                                           wavelength=ValueRange(min=2.8000202343174574, max=11.999998758060757, unit='angstrom', individual_magnitudes=None, offset=None, comment=None),
                                                                                           polarization=None,
                                                                                           ),
                                                    data_files=[File(file='amor2023n000608.hdf', timestamp=None, comment=None)],
                                                    additional_files=[File(file='amor2023n000614.hdf', timestamp=None, comment='supermirror')],
                                                    ),
                            ),
     reduction=Reduction(
                         software=Software(name='ess.reflectometry', version='24.10.0', platform='Linux', comment=None),
                         timestamp=datetime.datetime(2024, 9, 27, 12, 31, 5, 42017, tzinfo=datetime.timezone.utc),
                         creator=Person(name='Max Mustermann', affiliation='European Spallation Source ERIC', contact='max.mustermann@ess.eu', comment=None),
                         corrections=[],
                         ),
     columns=[Column(name='Qz', unit='1/angstrom', physical_quantity='wavevector transfer', flag_is=None, comment=None), Column(name='R', unit=None, physical_quantity='reflectivity', flag_is=None, comment=None), Column(name='sR', unit=None, physical_quantity='standard deviation of reflectivity', flag_is=None, comment=None), Column(name='sQz', unit='1/angstrom', physical_quantity='standard deviation of wavevector transfer resolution', flag_is=None, comment=None)],
     ), data=array([[5.02638405e-03, 2.62029935e-01, 4.70619668e-02, 3.00881770e-04],
       [5.07943060e-03, 3.53651911e-01, 6.06535962e-02, 3.00748615e-04],
       [5.13303698e-03, 3.76012557e-01, 5.11731358e-02, 3.02774350e-04],
       ...,
       [2.92232715e-01, 0.00000000e+00, 0.00000000e+00,            nan],
       [2.95316828e-01, 0.00000000e+00, 0.00000000e+00,            nan],
       [2.98433489e-01, 0.00000000e+00, 0.00000000e+00,            nan]]))

We also add the URL of this notebook to make it easier to reproduce the data:

[16]:
iofq_dataset.info.reduction.script = (
    'https://scipp.github.io/essreflectometry/examples/amor.html'
)

To support tracking provenance, we also list the corrections that were done by the workflow and store them in the dataset:

[17]:
iofq_dataset.info.reduction.corrections = orso.find_corrections(
    workflow.get(orso.OrsoIofQDataset)
)

Finally, we can save the data to a file. Note that iofq_dataset is an orsopy.fileio.orso.OrsoDataset.

[18]:
iofq_dataset.save('amor_reduced_iofq.ort')

Look at the first 50 lines of the file to inspect the metadata:

[19]:
!head amor_reduced_iofq.ort -n50
# # ORSO reflectivity data file | 1.1 standard | YAML encoding | https://www.reflectometry.org/
# data_source:
#   owner:
#     name: J. Stahn
#     affiliation: null
#     contact: ''
#   experiment:
#     title: commissioning
#     instrument: Amor
#     start_date: 2023-11-13T13:27:08
#     probe: neutron
#   sample:
#     name: SM5
#     model:
#       stack: air | 20 ( Ni 7 | Ti 7 ) | Al2O3
#   measurement:
#     instrument_settings:
#       incident_angle: {min: 0.004563767609003476, max: 0.028431498466197823, unit: rad}
#       wavelength: {min: 2.8000202343174574, max: 11.999998758060757, unit: angstrom}
#       polarization: null
#     data_files:
#     - file: amor2023n000608.hdf
#     additional_files:
#     - file: amor2023n000614.hdf
#       comment: supermirror
# reduction:
#   software: {name: ess.reflectometry, version: 24.10.0, platform: Linux}
#   timestamp: 2024-09-27T12:31:05.042017+00:00
#   creator:
#     name: Max Mustermann
#     affiliation: European Spallation Source ERIC
#     contact: max.mustermann@ess.eu
#   corrections:
#   - chopper ToF correction
#   - footprint correction
#   script: https://scipp.github.io/essreflectometry/examples/amor.html
# data_set: 0
# columns:
# - {name: Qz, unit: 1/angstrom, physical_quantity: wavevector transfer}
# - {name: R, physical_quantity: reflectivity}
# - {name: sR, physical_quantity: standard deviation of reflectivity}
# - {name: sQz, unit: 1/angstrom, physical_quantity: standard deviation of wavevector
#     transfer resolution}
# # Qz (1/angstrom)    R                      sR                     sQz (1/angstrom)
5.0263840502434900e-03 2.6202993538553171e-01 4.7061966835727186e-02 3.0088177049042544e-04
5.0794305979733716e-03 3.5365191059187928e-01 6.0653596244305108e-02 3.0074861530848113e-04
5.1330369788154728e-03 3.7601255664396532e-01 5.1173135798138966e-02 3.0277435001431252e-04
5.1872091010357786e-03 5.1627447498916823e-01 5.6674907281939961e-02 3.0471441015956428e-04
5.2419529352538633e-03 3.7502059580367697e-01 3.6261782312799105e-02 3.0685539409401694e-04
5.2972745151009613e-03 5.8035468085748432e-01 5.5599519897208405e-02 3.0924822169676032e-04