Bragg-edge imaging with ODIN#
This notebook illustrates how to convert recorded events on the ODIN detector to a single wavelength spectrum, revealing a Bragg edge in the data. WFM mode was used in the chopper cascade.
[1]:
import scipp as sc
from ess.reduce import time_of_flight
from ess import odin
import ess.odin.data # noqa: F401
from ess.imaging.types import *
Create and configure the workflow#
[2]:
wf = odin.OdinBraggEdgeWorkflow()
wf[Filename[SampleRun]] = odin.data.iron_simulation_sample_small()
wf[NeXusDetectorName] = "event_mode_detectors/timepix3"
wf[time_of_flight.TimeOfFlightLookupTableFilename] = odin.data.odin_tof_lookup_table()
Downloading file 'iron_simulation_sample_small.nxs' from 'https://public.esss.dk/groups/scipp/ess/odin/1/iron_simulation_sample_small.nxs' to '/home/runner/.cache/ess/odin/1'.
Downloading file 'ODIN-tof-lookup-table.h5' from 'https://public.esss.dk/groups/scipp/ess/odin/1/ODIN-tof-lookup-table.h5' to '/home/runner/.cache/ess/odin/1'.
First look at the data#
We load the raw detector data and perform a quick visualization of the event_time_offset
spectrum.
[3]:
tmpx3 = wf.compute(DetectorData[SampleRun])
tmpx3
[3]:
- dim_0: 1024
- dim_1: 1024
- detector_number(dim_0, dim_1)int321, 2, ..., 1048575, 1048576
Values:
array([[ 1, 2, 3, ..., 1022, 1023, 1024], [ 1025, 1026, 1027, ..., 2046, 2047, 2048], [ 2049, 2050, 2051, ..., 3070, 3071, 3072], ..., [1045505, 1045506, 1045507, ..., 1046526, 1046527, 1046528], [1046529, 1046530, 1046531, ..., 1047550, 1047551, 1047552], [1047553, 1047554, 1047555, ..., 1048574, 1048575, 1048576]], shape=(1024, 1024), dtype=int32) - gravity()vector3m/s^2[ 0. -9.80665 0. ]
Values:
array([ 0. , -9.80665, 0. ]) - position(dim_0, dim_1)vector3m[-0.007033 0.007033 0. ], [-0.007019 0.007033 0. ], ..., [ 0.007019 -0.007033 0. ], [ 0.007033 -0.007033 0. ]
Values:
array([[[-0.007033, 0.007033, 0. ], [-0.007019, 0.007033, 0. ], [-0.007006, 0.007033, 0. ], ..., [ 0.007006, 0.007033, 0. ], [ 0.007019, 0.007033, 0. ], [ 0.007033, 0.007033, 0. ]], [[-0.007033, 0.007019, 0. ], [-0.007019, 0.007019, 0. ], [-0.007006, 0.007019, 0. ], ..., [ 0.007006, 0.007019, 0. ], [ 0.007019, 0.007019, 0. ], [ 0.007033, 0.007019, 0. ]], [[-0.007033, 0.007006, 0. ], [-0.007019, 0.007006, 0. ], [-0.007006, 0.007006, 0. ], ..., [ 0.007006, 0.007006, 0. ], [ 0.007019, 0.007006, 0. ], [ 0.007033, 0.007006, 0. ]], ..., [[-0.007033, -0.007006, 0. ], [-0.007019, -0.007006, 0. ], [-0.007006, -0.007006, 0. ], ..., [ 0.007006, -0.007006, 0. ], [ 0.007019, -0.007006, 0. ], [ 0.007033, -0.007006, 0. ]], [[-0.007033, -0.007019, 0. ], [-0.007019, -0.007019, 0. ], [-0.007006, -0.007019, 0. ], ..., [ 0.007006, -0.007019, 0. ], [ 0.007019, -0.007019, 0. ], [ 0.007033, -0.007019, 0. ]], [[-0.007033, -0.007033, 0. ], [-0.007019, -0.007033, 0. ], [-0.007006, -0.007033, 0. ], ..., [ 0.007006, -0.007033, 0. ], [ 0.007019, -0.007033, 0. ], [ 0.007033, -0.007033, 0. ]]], shape=(1024, 1024, 3)) - sample_position()vector3m[0. 0. 0.]
Values:
array([0., 0., 0.]) - source_position()vector3m[ 0. 0. -60.5]
Values:
array([ 0. , 0. , -60.5]) - x_pixel_offset(dim_0, dim_1)float32m-0.007033, -0.007019, ..., 0.007019, 0.007033
Values:
array([[-0.007033, -0.007019, -0.007006, ..., 0.007006, 0.007019, 0.007033], [-0.007033, -0.007019, -0.007006, ..., 0.007006, 0.007019, 0.007033], [-0.007033, -0.007019, -0.007006, ..., 0.007006, 0.007019, 0.007033], ..., [-0.007033, -0.007019, -0.007006, ..., 0.007006, 0.007019, 0.007033], [-0.007033, -0.007019, -0.007006, ..., 0.007006, 0.007019, 0.007033], [-0.007033, -0.007019, -0.007006, ..., 0.007006, 0.007019, 0.007033]], shape=(1024, 1024), dtype=float32) - y_pixel_offset(dim_0, dim_1)float32m0.007033, 0.007033, ..., -0.007033, -0.007033
Values:
array([[ 0.007033, 0.007033, 0.007033, ..., 0.007033, 0.007033, 0.007033], [ 0.007019, 0.007019, 0.007019, ..., 0.007019, 0.007019, 0.007019], [ 0.007006, 0.007006, 0.007006, ..., 0.007006, 0.007006, 0.007006], ..., [-0.007006, -0.007006, -0.007006, ..., -0.007006, -0.007006, -0.007006], [-0.007019, -0.007019, -0.007019, ..., -0.007019, -0.007019, -0.007019], [-0.007033, -0.007033, -0.007033, ..., -0.007033, -0.007033, -0.007033]], shape=(1024, 1024), dtype=float32)
- (dim_0, dim_1)float32countsbinned data [len=2, len=0, ..., len=1, len=3]
dim='event', content=DataArray( dims=(event: 1000000), data=float32[counts], coords={'event_time_offset':float64[ns], 'event_time_zero':datetime64[ns]})
[4]:
tmpx3.bins.concat().hist(event_time_offset=300).plot()
[4]:
Compute neutron time-of-flight/wavelength#
We will now use the workflow to compute the neutron time-of-flight (equivalent to wavelength) using a lookup table built from the beamline chopper information.
[5]:
wf.visualize(DetectorTofData[SampleRun], graph_attr={"rankdir": "LR"})
[5]:
Inspect the lookup table#
It is always a good idea to quickly plot the TOF lookup table, as a sanity check.
[6]:
table = wf.compute(time_of_flight.TimeOfFlightLookupTable)
table.plot(figsize=(9, 4))
[6]:
Compute neutron wavelengths#
[7]:
sample_wavs = wf.compute(CountsWavelength[SampleRun])
sample_wavs.bins.concat().hist(wavelength=300).plot()
[7]:
Process the open-beam run#
We now reuse the same workflow to process the open-beam run that will be used later for normalization.
[8]:
wf[Filename[OpenBeamRun]] = odin.data.iron_simulation_ob_small()
openbeam_wavs = wf.compute(CountsWavelength[OpenBeamRun])
openbeam_wavs.bins.concat().hist(wavelength=300).plot()
Downloading file 'iron_simulation_ob_small.nxs' from 'https://public.esss.dk/groups/scipp/ess/odin/1/iron_simulation_ob_small.nxs' to '/home/runner/.cache/ess/odin/1'.
[8]:
Select region of interest by masking outer regions#
Making a 2D histogram of the data shows a dark square region in the centre of the detector panel; this is the region of interest, where the square sample has absorbed neutrons.
[9]:
sample_wavs.hist().plot(aspect='equal')
[9]:
The brighter areas around the edges are regions where neutrons did not travel through the sample. We thus want to mask those out using masking rules based on the spatial coordinates of the data:
[10]:
wf[MaskingRules] = {
'x_pixel_offset': lambda x: (x < sc.scalar(-5.8e-3, unit='m').to(unit=x.unit)) | (x > sc.scalar(5.8e-3, unit='m').to(unit=x.unit)),
'y_pixel_offset': lambda y: (y < sc.scalar(-5.8e-3, unit='m').to(unit=y.unit)) | (y > sc.scalar(5.8e-3, unit='m').to(unit=y.unit))
}
[11]:
masked = wf.compute(CountsMasked[SampleRun])
masked.hist().plot(aspect='equal')
[11]:
Normalize to open beam#
Finally, we use the masked sample and open-beam data to obtain a normalized signal, which reveals the Fe Bragg edges:
[12]:
wbins = sc.linspace('wavelength', 1.1, 9.4, 301, unit='angstrom')
normalized = (
wf.compute(CountsMasked[SampleRun]).bins.concat().hist(wavelength=wbins) /
wf.compute(CountsMasked[OpenBeamRun]).bins.concat().hist(wavelength=wbins)
)
normalized.plot()
[12]:
Save the final result#
[13]:
from scippneutron.io import save_xye
to_disk = normalized.copy(deep=False)
to_disk.coords['wavelength'] = sc.midpoints(to_disk.coords['wavelength'])
save_xye('fe_bragg_edge.xye', to_disk)