Sans2d Data Reduction#
Introduction#
This notebook gives an overview of how to use the esssans
package with Sciline, on the example of the data reduction of a Sans2d experiment. We begin with relevant imports:
[1]:
import scipp as sc
import plopp as pp
from ess import sans
from ess import isissans as isis
import ess.isissans.data # noqa: F401
from ess.sans.types import *
Create and configure the workflow#
We begin by creating the Sans2d workflow object (this is a sciline.Pipeline which can be consulted for advanced usage). The Sans2d workflow uses Mantid to load files. This tutorial comes with files that do not require Mantid, so we use a slightly modified workflow that does not require Mantid. The workflow is otherwise identical to the full Mantid-based workflow:
[2]:
workflow = isis.sans2d.Sans2dTutorialWorkflow()
# For real data use:
# workflow = isis.sans2d.Sans2dWorkflow()
We can insert steps for configuring the workflow. In this case, we would like to use the transmission monitor from the regular background and sample runs since there was no separate transmission run. We also want to compute the beam center using a simple center-of-mass estimation:
[3]:
workflow.insert(isis.io.transmission_from_background_run)
workflow.insert(isis.io.transmission_from_sample_run)
The workflow can be visualized as a graph. For readability we show only sub-workflow for computing IofQ[Sample]
. The workflow can actually compute the full BackgroundSubtractedIofQ
, which applies and equivalent workflow to the background run, before a subtraction step:
[4]:
# left-right layout works better for this graph
workflow.visualize(IofQ[SampleRun], graph_attr={"rankdir": "LR"})
[4]:
Note the red boxes which indicate missing input parameters. We can set these missing parameters, as well as parameters where we do not want to use the defaults:
[5]:
workflow[OutFilename] = 'reduced.nxs'
workflow[NeXusMonitorName[Incident]] = 'monitor2'
workflow[NeXusMonitorName[Transmission]] = 'monitor4'
workflow[isis.SampleOffset] = sc.vector([0.0, 0.0, 0.053], unit='m')
workflow[isis.MonitorOffset[Transmission]] = sc.vector([0.0, 0.0, -6.719], unit='m')
workflow[WavelengthBins] = sc.linspace(
'wavelength', start=2.0, stop=16.0, num=141, unit='angstrom'
)
workflow[isis.sans2d.LowCountThreshold] = sc.scalar(100, unit='counts')
mask_interval = sc.array(dims=['wavelength'], values=[2.21, 2.59], unit='angstrom')
workflow[WavelengthMask] = sc.DataArray(
sc.array(dims=['wavelength'], values=[True]),
coords={'wavelength': mask_interval},
)
workflow[QBins] = sc.linspace(dim='Q', start=0.01, stop=0.6, num=141, unit='1/angstrom')
workflow[NonBackgroundWavelengthRange] = sc.array(
dims=['wavelength'], values=[0.7, 17.1], unit='angstrom'
)
workflow[CorrectForGravity] = True
workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound
workflow[ReturnEvents] = True
Configuring data to load#
We have not configured which files we want to load. In this tutorial, we use helpers to fetch the tutorial data which return the filenames of the cached files. In a real use case, you would set these parameters manually:
[6]:
workflow[DirectBeamFilename] = isis.data.sans2d_tutorial_direct_beam()
workflow[Filename[SampleRun]] = isis.data.sans2d_tutorial_sample_run()
workflow[Filename[BackgroundRun]] = isis.data.sans2d_tutorial_background_run()
workflow[Filename[EmptyBeamRun]] = isis.data.sans2d_tutorial_empty_beam_run()
Downloading file 'DIRECT_SANS2D_REAR_34327_4m_8mm_16Feb16.dat.h5' from 'https://public.esss.dk/groups/scipp/ess/sans2d/1/DIRECT_SANS2D_REAR_34327_4m_8mm_16Feb16.dat.h5' to '/home/runner/.cache/ess/sans2d/1'.
Downloading file 'SANS2D00063159.nxs.h5' from 'https://public.esss.dk/groups/scipp/ess/sans2d/1/SANS2D00063159.nxs.h5' to '/home/runner/.cache/ess/sans2d/1'.
The beam center is not set yet. Unless we have a previously computed value, we can do so now:
[7]:
center = sans.beam_center_from_center_of_mass(workflow)
center
[7]:
- ()vector3m[ 0.09007484 -0.08263978 0. ]
Values:
array([ 0.09007484, -0.08263978, 0. ])
Remember to update the workflow with the new center:
[8]:
workflow[BeamCenter] = center
Use the workflow#
Compute final result#
We can now compute the background-subtracted \(I(Q)\):
[9]:
result = workflow.compute(BackgroundSubtractedIofQ)
result.hist().plot(scale={'Q': 'log'}, norm='log')
[9]:
As the result was computed in event-mode, we can also use a different \(Q\)-binning, without re-reducing the data:
[10]:
result.hist(Q=60).plot(scale={'Q': 'log'}, norm='log')
[10]:
In the above we used an upper bound for the uncertainties of the normalization factors. We can also compute the result with dropped normalization-factor uncertainties. This is incorrect, but is useful for understanding whether the normalization factors significantly contribute to the uncertainty of the result:
[11]:
workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.drop
result_drop = workflow.compute(BackgroundSubtractedIofQ)
# Reset the UnsertaintyBroadcastMode to the old value
workflow[UncertaintyBroadcastMode] = UncertaintyBroadcastMode.upper_bound
sc.DataGroup(upper_bound=result, dropped=result_drop).hist().plot(norm='log')
[11]:
Save reduced data to file#
ess.sans
provides a function for saving the reduced data as an NXcanSAS file. It could be used directly with the result
computed above, but we would have to provide the required metadata ourselves. Instead, we use Sciline to get all required information directly from the pipeline: (See also the File output docs.)
[12]:
from ess.sans.io import save_background_subtracted_iofq
workflow.bind_and_call(save_background_subtracted_iofq)
Compute intermediate results#
For inspection and debugging purposes we can also compute intermediate results. To avoid repeated computation (including costly loading of files) we can request multiple results at once, including the final result, if desired. For example:
[13]:
monitors = (
WavelengthMonitor[SampleRun, Incident],
WavelengthMonitor[SampleRun, Transmission],
WavelengthMonitor[BackgroundRun, Incident],
WavelengthMonitor[BackgroundRun, Transmission],
)
parts = (CleanSummedQ[SampleRun, Numerator], CleanSummedQ[SampleRun, Denominator])
iofqs = (IofQ[SampleRun], IofQ[BackgroundRun], BackgroundSubtractedIofQ)
keys = (*monitors, MaskedData[SampleRun], *parts, *iofqs)
results = workflow.compute(keys)
display(sc.plot({str(key): results[key] for key in monitors}, norm='log'))
display(
isis.plot_flat_detector_xy(
results[MaskedData[SampleRun]]['spectrum', :61440].hist(), norm='log'
)
)
wavelength = workflow.compute(WavelengthBins)
display(
results[CleanSummedQ[SampleRun, Numerator]]
.hist(wavelength=wavelength)
.transpose()
.plot(norm='log')
)
display(results[CleanSummedQ[SampleRun, Denominator]].plot(norm='log'))
parts = {str(key): results[key].sum('wavelength') for key in parts}
display(sc.plot(parts, norm='log'))
iofqs = {str(key): results[key] for key in iofqs}
iofqs = {key: val if val.bins is None else val.hist() for key, val in iofqs.items()}
display(sc.plot(iofqs, norm='log'))
Wavelength bands#
We can also compute \(I(Q)\) inside a set of wavelength bands, instead of using the full wavelength range in one go. This is useful for debugging purposes.
To achieve this, we need to supply the WavelengthBands
parameter (as a two-dimensional variable), representing the wavelength range for each band.
[14]:
workflow[WavelengthBands] = sc.linspace(
'wavelength', start=2.0, stop=16.0, num=11, unit='angstrom'
)
Compute the result:
[15]:
result = workflow.compute(BackgroundSubtractedIofQ)
result
[15]:
- band: 10
- Q: 140
- L1()float64m19.334
Values:
array(19.334) - Q(Q [bin-edge])float641/Å0.01, 0.014, ..., 0.596, 0.6
Values:
array([0.01 , 0.01421429, 0.01842857, 0.02264286, 0.02685714, 0.03107143, 0.03528571, 0.0395 , 0.04371429, 0.04792857, 0.05214286, 0.05635714, 0.06057143, 0.06478571, 0.069 , 0.07321429, 0.07742857, 0.08164286, 0.08585714, 0.09007143, 0.09428571, 0.0985 , 0.10271429, 0.10692857, 0.11114286, 0.11535714, 0.11957143, 0.12378571, 0.128 , 0.13221429, 0.13642857, 0.14064286, 0.14485714, 0.14907143, 0.15328571, 0.1575 , 0.16171429, 0.16592857, 0.17014286, 0.17435714, 0.17857143, 0.18278571, 0.187 , 0.19121429, 0.19542857, 0.19964286, 0.20385714, 0.20807143, 0.21228571, 0.2165 , 0.22071429, 0.22492857, 0.22914286, 0.23335714, 0.23757143, 0.24178571, 0.246 , 0.25021429, 0.25442857, 0.25864286, 0.26285714, 0.26707143, 0.27128571, 0.2755 , 0.27971429, 0.28392857, 0.28814286, 0.29235714, 0.29657143, 0.30078571, 0.305 , 0.30921429, 0.31342857, 0.31764286, 0.32185714, 0.32607143, 0.33028571, 0.3345 , 0.33871429, 0.34292857, 0.34714286, 0.35135714, 0.35557143, 0.35978571, 0.364 , 0.36821429, 0.37242857, 0.37664286, 0.38085714, 0.38507143, 0.38928571, 0.3935 , 0.39771429, 0.40192857, 0.40614286, 0.41035714, 0.41457143, 0.41878571, 0.423 , 0.42721429, 0.43142857, 0.43564286, 0.43985714, 0.44407143, 0.44828571, 0.4525 , 0.45671429, 0.46092857, 0.46514286, 0.46935714, 0.47357143, 0.47778571, 0.482 , 0.48621429, 0.49042857, 0.49464286, 0.49885714, 0.50307143, 0.50728571, 0.5115 , 0.51571429, 0.51992857, 0.52414286, 0.52835714, 0.53257143, 0.53678571, 0.541 , 0.54521429, 0.54942857, 0.55364286, 0.55785714, 0.56207143, 0.56628571, 0.5705 , 0.57471429, 0.57892857, 0.58314286, 0.58735714, 0.59157143, 0.59578571, 0.6 ]) - gravity()vector3m/s^2[-0. -9.80665 -0. ]
Values:
array([-0. , -9.80665, -0. ]) - incident_beam()vector3m[ 0. 0. 19.334]
Values:
array([ 0. , 0. , 19.334]) - wavelength(wavelength [bin-edge], band)float64Å2.0, 3.4, ..., 14.6, 16.0
Values:
array([[ 2. , 3.4, 4.8, 6.2, 7.6, 9. , 10.4, 11.8, 13.2, 14.6], [ 3.4, 4.8, 6.2, 7.6, 9. , 10.4, 11.8, 13.2, 14.6, 16. ]])
- (band, Q)DataArrayViewbinned data [len=236, len=2086, ..., len=0, len=0]
dim='event', content=DataArray( dims=(event: 6172466), data=float32[dimensionless], coords={'pulse_time':datetime64[ns], 'wavelength':float64[Å], 'phi':float64[rad], 'Q':float64[1/Å]})
The result is two-dimensional and we over-plot all the bands onto the same axes:
[16]:
pp.plot(sc.collapse(result.hist(), keep='Q'), norm='log')
[16]: