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 unwrap
from ess import odin
import ess.odin.data # noqa: F401
from ess.imaging.types import *
Create and configure the workflow#
[2]:
wf = odin.OdinBraggEdgeWorkflow(wavelength_from="analytical")
wf[Filename[SampleRun]] = odin.data.iron_simulation_sample_small()
wf[NeXusDetectorName] = "event_mode_detectors/timepix3"
Downloading file 'iron_simulation_sample_small.nxs' from 'https://public.esss.dk/groups/scipp/ess/odin/2/iron_simulation_sample_small.nxs' to '/home/runner/.cache/ess/odin'.
Patch the choppers in the file#
The chopper settings in the file contain errors. We patch them here, but note that this step should go away in the future.
[3]:
from ess.odin.beamline import choppers
disk_choppers = choppers(source_position=wf.compute(Position[snx.NXsource, SampleRun]))
wf[unwrap.DiskChoppers[SampleRun]] = disk_choppers
First look at the data#
We load the raw detector data and perform a quick visualization of the event_time_offset spectrum.
[4]:
tmpx3 = wf.compute(RawDetector[SampleRun])
tmpx3
[4]:
- x_pixel_offset: 4096
- y_pixel_offset: 4096
- detector_number(x_pixel_offset, y_pixel_offset)int320, 1, ..., 16777214, 16777215
Values:
array([[ 0, 1, 2, ..., 4093, 4094, 4095], [ 4096, 4097, 4098, ..., 8189, 8190, 8191], [ 8192, 8193, 8194, ..., 12285, 12286, 12287], ..., [16764928, 16764929, 16764930, ..., 16769021, 16769022, 16769023], [16769024, 16769025, 16769026, ..., 16773117, 16773118, 16773119], [16773120, 16773121, 16773122, ..., 16777213, 16777214, 16777215]], shape=(4096, 4096), dtype=int32) - position(x_pixel_offset, y_pixel_offset)vector3m[-0.00703828 -0.00703828 0. ], [-0.00703828 -0.00703484 0. ], ..., [0.00703828 0.00703484 0. ], [0.00703828 0.00703828 0. ]
Values:
array([[[-0.00703828, -0.00703828, 0. ], [-0.00703828, -0.00703484, 0. ], [-0.00703828, -0.00703141, 0. ], ..., [-0.00703828, 0.00703141, 0. ], [-0.00703828, 0.00703484, 0. ], [-0.00703828, 0.00703828, 0. ]], [[-0.00703484, -0.00703828, 0. ], [-0.00703484, -0.00703484, 0. ], [-0.00703484, -0.00703141, 0. ], ..., [-0.00703484, 0.00703141, 0. ], [-0.00703484, 0.00703484, 0. ], [-0.00703484, 0.00703828, 0. ]], [[-0.00703141, -0.00703828, 0. ], [-0.00703141, -0.00703484, 0. ], [-0.00703141, -0.00703141, 0. ], ..., [-0.00703141, 0.00703141, 0. ], [-0.00703141, 0.00703484, 0. ], [-0.00703141, 0.00703828, 0. ]], ..., [[ 0.00703141, -0.00703828, 0. ], [ 0.00703141, -0.00703484, 0. ], [ 0.00703141, -0.00703141, 0. ], ..., [ 0.00703141, 0.00703141, 0. ], [ 0.00703141, 0.00703484, 0. ], [ 0.00703141, 0.00703828, 0. ]], [[ 0.00703484, -0.00703828, 0. ], [ 0.00703484, -0.00703484, 0. ], [ 0.00703484, -0.00703141, 0. ], ..., [ 0.00703484, 0.00703141, 0. ], [ 0.00703484, 0.00703484, 0. ], [ 0.00703484, 0.00703828, 0. ]], [[ 0.00703828, -0.00703828, 0. ], [ 0.00703828, -0.00703484, 0. ], [ 0.00703828, -0.00703141, 0. ], ..., [ 0.00703828, 0.00703141, 0. ], [ 0.00703828, 0.00703484, 0. ], [ 0.00703828, 0.00703828, 0. ]]], shape=(4096, 4096, 3)) - x_pixel_offset(x_pixel_offset)float32m-0.0070382813, -0.007034844, ..., 0.007034844, 0.0070382813
Values:
array([-0.00703828, -0.00703484, -0.00703141, ..., 0.00703141, 0.00703484, 0.00703828], shape=(4096,), dtype=float32) - y_pixel_offset(y_pixel_offset)float32m-0.0070382813, -0.007034844, ..., 0.007034844, 0.0070382813
Values:
array([-0.00703828, -0.00703484, -0.00703141, ..., 0.00703141, 0.00703484, 0.00703828], shape=(4096,), dtype=float32)
- (x_pixel_offset, y_pixel_offset)float32countsbinned data [len=1, len=1, ..., len=0, len=0]
dim='event', content=DataArray( dims=(event: 1000000), data=float32[counts], coords={'event_time_offset':float64[ns], 'event_time_zero':datetime64[ns]})
[5]:
tmpx3.bins.concat().hist(event_time_offset=300).plot()
[5]:
Compute neutron wavelengths#
We will now use the workflow to compute the neutron wavelengths using a lookup table built from the beamline chopper information.
[6]:
wf.visualize(WavelengthDetector[SampleRun], graph_attr={"rankdir": "LR"})
[6]:
Inspect the lookup table#
It is always a good idea to quickly plot the wavelength lookup table, as a sanity check.
[7]:
table = wf.compute(unwrap.LookupTable[SampleRun, NXdetector])
table.plot(figsize=(9, 4))
[7]:
Compute neutron wavelengths#
[8]:
sample_wavs = wf.compute(WavelengthDetector[SampleRun])
sample_wavs.bins.concat().hist(wavelength=300).plot()
[8]:
Process the open-beam run#
We now reuse the same workflow to process the open-beam run that will be used later for normalization.
[9]:
wf[Filename[OpenBeamRun]] = odin.data.iron_simulation_ob_small()
# Also need to patch choppers here
wf[unwrap.DiskChoppers[OpenBeamRun]] = disk_choppers
openbeam_wavs = wf.compute(WavelengthDetector[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/2/iron_simulation_ob_small.nxs' to '/home/runner/.cache/ess/odin'.
[9]:
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.
[10]:
# ODIN timepix has 4096x4096 pixels by default, which is too many to visualize without `ignore_size=True` option.
# we will use 256x256 pixels instead for visualization in the documentation.
vis_resolution = {"x_pixel_offset": 256, "y_pixel_offset": 256}
[11]:
sample_wavs.hist(vis_resolution).plot(aspect='equal')
[11]:
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:
[12]:
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))
}
[13]:
masked = wf.compute(CorrectedDetector[SampleRun])
masked.hist(vis_resolution).plot(aspect='equal')
[13]:
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:
[14]:
wbins = sc.linspace('wavelength', 1.1, 9.4, 301, unit='angstrom')
normalized = (
wf.compute(CorrectedDetector[SampleRun]).bins.concat().hist(wavelength=wbins) /
wf.compute(CorrectedDetector[OpenBeamRun]).bins.concat().hist(wavelength=wbins)
)
normalized.plot()
[14]:
Save the final result#
[15]:
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)