Powder diffraction data reduction#

This notebook will guide you through the data reduction for the powder diffraction experiment that you simulated with McStas yesterday.

The following is a basic outline of what this notebook will cover:

  • Loading the NeXus files that contain the data

  • Inspect/visualize the data contents

  • How to convert the raw event time-of-arrival coordinate to something more useful (\(\lambda\), \(d\), …)

  • Improve the estimate of neutron time-of-flight

  • Save the reduced data to disk, in a state that is ready for analysis/fitting.

import numpy as np
import scipp as sc
import plopp as pp
import powder_utils as utils

Process the run with a reference sample: Si#

Load the NeXus file data#

By default, we will use the data we generated in yesterday’s simulation:

folder = "../3-mcstas/output_sample_Si"

We now load the data:

sample_si = utils.load_powder(folder)

The first way to inspect the data is to view the HTML representation of the loaded object.

Try to explore what is inside the data, and familiarize yourself with the different sections (Dimensions, Coordinates, Data).

sample_si
Show/Hide data repr Show/Hide attributes
scipp.DataArray (32.14 MB)
    • events: 350963
    • position
      (events)
      vector3
      m
      [ 4.97355937 0.2625 160.42201312], [ 3.10883934e+00 -3.75000000e-02 1.57522981e+02], ..., [ -1.97529876 0.4125 160.05783889], [-1.32519983e+00 -3.75000000e-02 1.58526302e+02]
      Values:
      array([[ 4.97355937e+00, 2.62500000e-01, 1.60422013e+02], [ 3.10883934e+00, -3.75000000e-02, 1.57522981e+02], [ 3.13580735e+00, 1.12500000e-01, 1.57537320e+02], ..., [-1.32519983e+00, 3.37500000e-01, 1.58526302e+02], [-1.97529876e+00, 4.12500000e-01, 1.60057839e+02], [-1.32519983e+00, -3.75000000e-02, 1.58526302e+02]])
    • sample_position
      ()
      vector3
      m
      [ 1.47918852 0. 160.62043787]
      Values:
      array([ 1.47918852, 0. , 160.62043787])
    • sim_source_time
      (events)
      float64
      s
      0.001, 0.003, ..., 0.001, 0.001
      Values:
      array([0.00135692, 0.00342941, 0.00342941, ..., 0.00051112, 0.00051112, 0.00051112])
    • sim_speed
      (events)
      float64
      m/s
      1444.839, 2607.873, ..., 1348.320, 1348.320
      Values:
      array([1444.83867569, 2607.87309718, 2607.87309718, ..., 1348.3203984 , 1348.3203984 , 1348.3203984 ])
    • sim_wavelength
      (events)
      float64
      Å
      2.738, 1.517, ..., 2.934, 2.934
      Values:
      array([2.73804549, 1.51695802, 1.51695802, ..., 2.93404596, 2.93404596, 2.93404596])
    • source_position
      ()
      vector3
      m
      [0. 0. 0.]
      Values:
      array([0., 0., 0.])
    • time_origin
      ()
      float64
      s
      0.0
      Values:
      array(0.)
    • toa
      (events)
      float64
      s
      0.115, 0.066, ..., 0.122, 0.122
      Values:
      array([0.11494571, 0.06636537, 0.06636959, ..., 0.12225075, 0.12225605, 0.1222465 ])
    • x
      (events)
      float64
      m
      4.974, 3.109, ..., -1.975, -1.325
      Values:
      array([ 4.97355937, 3.10883934, 3.13580735, ..., -1.32519983, -1.97529876, -1.32519983])
    • y
      (events)
      float64
      m
      0.262, -0.037, ..., 0.413, -0.037
      Values:
      array([ 0.2625, -0.0375, 0.1125, ..., 0.3375, 0.4125, -0.0375])
    • z
      (events)
      float64
      m
      160.422, 157.523, ..., 160.058, 158.526
      Values:
      array([160.42201312, 157.52298114, 157.53732029, ..., 158.52630177, 160.05783889, 158.52630177])
    • (events)
      float64
      counts
      148.534, 1.224, ..., 3.150e-15, 2.622e-15
      σ = 148.534, 1.224, ..., 3.150e-15, 2.622e-15
      Values:
      array([1.48534080e+02, 1.22398205e+00, 1.22398205e+00, ..., 2.62220306e-15, 3.14989726e-15, 2.62220306e-15])

      Variances (σ²):
      array([2.20623729e+04, 1.49813206e+00, 1.49813206e+00, ..., 6.87594887e-30, 9.92185272e-30, 6.87594887e-30])

Visualize the data#

Here is a 2D visualization of the neutron counts, histogrammed along the x and toadimensions:

sample_si.hist(x=200, toa=200).plot(norm="log", vmin=1.0e-2)
../_images/9e8b03737697c0797e0cf4195f7fbd84d3aa027218abea3ff7e0278947e00d4c.svg

We can also visualize the events in 3D, which show the shape of the detector panels:

pp.scatter3d(sample_si[::20], pos="position", cbar=True, vmin=0.01, pixel_size=0.07)

Coordinate transformations#

The first step of this data reduction workflow is to convert the raw event coordinates (position, time-of-flight) to something physically meaningful such as wavelength (\(\lambda\)) or d-spacing (\(d\)).

Scipp has a dedicated method for this called transform_coords (see docs here).

We begin with a standard graph which describes how to compute e.g. the wavelength from the other coordinates in the raw data.

from scippneutron.conversion.graph.beamline import beamline
from scippneutron.conversion.graph.tof import kinematic

graph = {**beamline(scatter=True), **kinematic("tof")}


def compute_tof(toa, time_origin):
    return toa - time_origin


graph["tof"] = compute_tof
sc.show_graph(graph, simplified=True)
../_images/f0b7aa3fe92fab51b4c6347b7d15204856da5f8c1d62a07e9bfdbec74b47b0ca.svg

To compute the wavelength of all the events, we simply call transform_coords on our loaded data, requesting the name of the coordinate we want in the output ("wavelength"), as well as providing it the graph to be used to compute it (i.e. the one we defined just above).

This yields

si_wav = sample_si.transform_coords(["wavelength", "two_theta"], graph=graph)
si_wav
Show/Hide data repr Show/Hide attributes
scipp.DataArray (53.56 MB)
    • events: 350963
    • L1
      ()
      float64
      m
      160.62724881363712
      Values:
      array(160.62724881)
    • L2
      (events)
      float64
      m
      3.510, 3.500, ..., 3.524, 3.500
      Values:
      array([3.50982995, 3.50020089, 3.50180757, ..., 3.51623467, 3.52422421, 3.50020089])
    • Ltotal
      (events)
      float64
      m
      164.137, 164.127, ..., 164.151, 164.127
      Values:
      array([164.13707876, 164.1274497 , 164.12905638, ..., 164.14348348, 164.15147302, 164.1274497 ])
    • incident_beam
      ()
      vector3
      m
      [ 1.47918852 0. 160.62043787]
      Values:
      array([ 1.47918852, 0. , 160.62043787])
    • position
      (events)
      vector3
      m
      [ 4.97355937 0.2625 160.42201312], [ 3.10883934e+00 -3.75000000e-02 1.57522981e+02], ..., [ -1.97529876 0.4125 160.05783889], [-1.32519983e+00 -3.75000000e-02 1.58526302e+02]
      Values:
      array([[ 4.97355937e+00, 2.62500000e-01, 1.60422013e+02], [ 3.10883934e+00, -3.75000000e-02, 1.57522981e+02], [ 3.13580735e+00, 1.12500000e-01, 1.57537320e+02], ..., [-1.32519983e+00, 3.37500000e-01, 1.58526302e+02], [-1.97529876e+00, 4.12500000e-01, 1.60057839e+02], [-1.32519983e+00, -3.75000000e-02, 1.58526302e+02]])
    • sample_position
      ()
      vector3
      m
      [ 1.47918852 0. 160.62043787]
      Values:
      array([ 1.47918852, 0. , 160.62043787])
    • scattered_beam
      (events)
      vector3
      m
      [ 3.49437085 0.2625 -0.19842476], [ 1.62965082 -0.0375 -3.09745673], ..., [-3.45448728 0.4125 -0.56259898], [-2.80438834 -0.0375 -2.0941361 ]
      Values:
      array([[ 3.49437085, 0.2625 , -0.19842476], [ 1.62965082, -0.0375 , -3.09745673], [ 1.65661884, 0.1125 , -3.08311758], ..., [-2.80438834, 0.3375 , -2.0941361 ], [-3.45448728, 0.4125 , -0.56259898], [-2.80438834, -0.0375 , -2.0941361 ]])
    • sim_source_time
      (events)
      float64
      s
      0.001, 0.003, ..., 0.001, 0.001
      Values:
      array([0.00135692, 0.00342941, 0.00342941, ..., 0.00051112, 0.00051112, 0.00051112])
    • sim_speed
      (events)
      float64
      m/s
      1444.839, 2607.873, ..., 1348.320, 1348.320
      Values:
      array([1444.83867569, 2607.87309718, 2607.87309718, ..., 1348.3203984 , 1348.3203984 , 1348.3203984 ])
    • sim_wavelength
      (events)
      float64
      Å
      2.738, 1.517, ..., 2.934, 2.934
      Values:
      array([2.73804549, 1.51695802, 1.51695802, ..., 2.93404596, 2.93404596, 2.93404596])
    • source_position
      ()
      vector3
      m
      [0. 0. 0.]
      Values:
      array([0., 0., 0.])
    • time_origin
      ()
      float64
      s
      0.0
      Values:
      array(0.)
    • toa
      (events)
      float64
      s
      0.115, 0.066, ..., 0.122, 0.122
      Values:
      array([0.11494571, 0.06636537, 0.06636959, ..., 0.12225075, 0.12225605, 0.1222465 ])
    • tof
      (events)
      float64
      s
      0.115, 0.066, ..., 0.122, 0.122
      Values:
      array([0.11494571, 0.06636537, 0.06636959, ..., 0.12225075, 0.12225605, 0.1222465 ])
    • two_theta
      (events)
      float64
      rad
      1.618, 2.648, ..., 1.740, 2.221
      Values:
      array([1.6181774 , 2.64794815, 2.63838919, ..., 2.21790405, 1.74026384, 2.2213701 ])
    • wavelength
      (events)
      float64
      Å
      2.770, 1.600, ..., 2.946, 2.947
      Values:
      array([2.7704229 , 1.59963287, 1.59971897, ..., 2.94637416, 2.94635841, 2.94655956])
    • x
      (events)
      float64
      m
      4.974, 3.109, ..., -1.975, -1.325
      Values:
      array([ 4.97355937, 3.10883934, 3.13580735, ..., -1.32519983, -1.97529876, -1.32519983])
    • y
      (events)
      float64
      m
      0.262, -0.037, ..., 0.413, -0.037
      Values:
      array([ 0.2625, -0.0375, 0.1125, ..., 0.3375, 0.4125, -0.0375])
    • z
      (events)
      float64
      m
      160.422, 157.523, ..., 160.058, 158.526
      Values:
      array([160.42201312, 157.52298114, 157.53732029, ..., 158.52630177, 160.05783889, 158.52630177])
    • (events)
      float64
      counts
      148.534, 1.224, ..., 3.150e-15, 2.622e-15
      σ = 148.534, 1.224, ..., 3.150e-15, 2.622e-15
      Values:
      array([1.48534080e+02, 1.22398205e+00, 1.22398205e+00, ..., 2.62220306e-15, 3.14989726e-15, 2.62220306e-15])

      Variances (σ²):
      array([2.20623729e+04, 1.49813206e+00, 1.49813206e+00, ..., 6.87594887e-30, 9.92185272e-30, 6.87594887e-30])

The result has a wavelength coordinate. We can also plot the result:

si_wav.hist(two_theta=128, wavelength=200).plot(norm="log", vmin=1.0e-3)
../_images/97f8bae947a108ec2123395e62113690ce76cde6b0d07df944adc4fa8c1936af.svg

Exercise 1: convert raw data to d-spacing#

Instead of wavelength as in the example above, the task is now to convert the raw data to interplanar lattice spacing \(d\).

The transformation graph is missing the computation for \(d\) so you will have to add it in yourself. As a reminder, \(d\) is computed as follows

\[d = \frac{\lambda}{2 \sin \theta}\]

You have to:

  • create a function that computes \(d\)

  • add it to the graph with name “dspacing”

  • call transform_coords using the new graph

Note that the graph already contains the necessary components to compute the scattering angle \(2 \theta\) (called two_theta in code).

Solution:

Hide code cell content

def compute_d(two_theta, wavelength):
    return wavelength / (2 * sc.sin(two_theta / 2))


graph["dspacing"] = compute_d

# Show the updated graph
display(sc.show_graph(graph, simplified=True))

# Run the coordinate transformation
si_dspacing = sample_si.transform_coords("dspacing", graph=graph)
si_dspacing
../_images/bbdbad49c62330c85cdb6a22b078125c7ac8c0cf35be48486f07d590a3d11e7f.svg
Show/Hide data repr Show/Hide attributes
scipp.DataArray (56.24 MB)
    • events: 350963
    • L1
      ()
      float64
      m
      160.62724881363712
      Values:
      array(160.62724881)
    • L2
      (events)
      float64
      m
      3.510, 3.500, ..., 3.524, 3.500
      Values:
      array([3.50982995, 3.50020089, 3.50180757, ..., 3.51623467, 3.52422421, 3.50020089])
    • Ltotal
      (events)
      float64
      m
      164.137, 164.127, ..., 164.151, 164.127
      Values:
      array([164.13707876, 164.1274497 , 164.12905638, ..., 164.14348348, 164.15147302, 164.1274497 ])
    • dspacing
      (events)
      float64
      Å
      1.914, 0.825, ..., 1.927, 1.644
      Values:
      array([1.91417829, 0.82481347, 0.82586183, ..., 1.64559215, 1.92720072, 1.6442798 ])
    • incident_beam
      ()
      vector3
      m
      [ 1.47918852 0. 160.62043787]
      Values:
      array([ 1.47918852, 0. , 160.62043787])
    • position
      (events)
      vector3
      m
      [ 4.97355937 0.2625 160.42201312], [ 3.10883934e+00 -3.75000000e-02 1.57522981e+02], ..., [ -1.97529876 0.4125 160.05783889], [-1.32519983e+00 -3.75000000e-02 1.58526302e+02]
      Values:
      array([[ 4.97355937e+00, 2.62500000e-01, 1.60422013e+02], [ 3.10883934e+00, -3.75000000e-02, 1.57522981e+02], [ 3.13580735e+00, 1.12500000e-01, 1.57537320e+02], ..., [-1.32519983e+00, 3.37500000e-01, 1.58526302e+02], [-1.97529876e+00, 4.12500000e-01, 1.60057839e+02], [-1.32519983e+00, -3.75000000e-02, 1.58526302e+02]])
    • sample_position
      ()
      vector3
      m
      [ 1.47918852 0. 160.62043787]
      Values:
      array([ 1.47918852, 0. , 160.62043787])
    • scattered_beam
      (events)
      vector3
      m
      [ 3.49437085 0.2625 -0.19842476], [ 1.62965082 -0.0375 -3.09745673], ..., [-3.45448728 0.4125 -0.56259898], [-2.80438834 -0.0375 -2.0941361 ]
      Values:
      array([[ 3.49437085, 0.2625 , -0.19842476], [ 1.62965082, -0.0375 , -3.09745673], [ 1.65661884, 0.1125 , -3.08311758], ..., [-2.80438834, 0.3375 , -2.0941361 ], [-3.45448728, 0.4125 , -0.56259898], [-2.80438834, -0.0375 , -2.0941361 ]])
    • sim_source_time
      (events)
      float64
      s
      0.001, 0.003, ..., 0.001, 0.001
      Values:
      array([0.00135692, 0.00342941, 0.00342941, ..., 0.00051112, 0.00051112, 0.00051112])
    • sim_speed
      (events)
      float64
      m/s
      1444.839, 2607.873, ..., 1348.320, 1348.320
      Values:
      array([1444.83867569, 2607.87309718, 2607.87309718, ..., 1348.3203984 , 1348.3203984 , 1348.3203984 ])
    • sim_wavelength
      (events)
      float64
      Å
      2.738, 1.517, ..., 2.934, 2.934
      Values:
      array([2.73804549, 1.51695802, 1.51695802, ..., 2.93404596, 2.93404596, 2.93404596])
    • source_position
      ()
      vector3
      m
      [0. 0. 0.]
      Values:
      array([0., 0., 0.])
    • time_origin
      ()
      float64
      s
      0.0
      Values:
      array(0.)
    • toa
      (events)
      float64
      s
      0.115, 0.066, ..., 0.122, 0.122
      Values:
      array([0.11494571, 0.06636537, 0.06636959, ..., 0.12225075, 0.12225605, 0.1222465 ])
    • tof
      (events)
      float64
      s
      0.115, 0.066, ..., 0.122, 0.122
      Values:
      array([0.11494571, 0.06636537, 0.06636959, ..., 0.12225075, 0.12225605, 0.1222465 ])
    • two_theta
      (events)
      float64
      rad
      1.618, 2.648, ..., 1.740, 2.221
      Values:
      array([1.6181774 , 2.64794815, 2.63838919, ..., 2.21790405, 1.74026384, 2.2213701 ])
    • wavelength
      (events)
      float64
      Å
      2.770, 1.600, ..., 2.946, 2.947
      Values:
      array([2.7704229 , 1.59963287, 1.59971897, ..., 2.94637416, 2.94635841, 2.94655956])
    • x
      (events)
      float64
      m
      4.974, 3.109, ..., -1.975, -1.325
      Values:
      array([ 4.97355937, 3.10883934, 3.13580735, ..., -1.32519983, -1.97529876, -1.32519983])
    • y
      (events)
      float64
      m
      0.262, -0.037, ..., 0.413, -0.037
      Values:
      array([ 0.2625, -0.0375, 0.1125, ..., 0.3375, 0.4125, -0.0375])
    • z
      (events)
      float64
      m
      160.422, 157.523, ..., 160.058, 158.526
      Values:
      array([160.42201312, 157.52298114, 157.53732029, ..., 158.52630177, 160.05783889, 158.52630177])
    • (events)
      float64
      counts
      148.534, 1.224, ..., 3.150e-15, 2.622e-15
      σ = 148.534, 1.224, ..., 3.150e-15, 2.622e-15
      Values:
      array([1.48534080e+02, 1.22398205e+00, 1.22398205e+00, ..., 2.62220306e-15, 3.14989726e-15, 2.62220306e-15])

      Variances (σ²):
      array([2.20623729e+04, 1.49813206e+00, 1.49813206e+00, ..., 6.87594887e-30, 9.92185272e-30, 6.87594887e-30])

Histogram the data in d-spacing#

The final step in processing the sample run is to histogram the data into \(d\) bins.

dbins = sc.linspace("dspacing", 0.7, 2.5, 901, unit="angstrom")
ntheta = 128

si_dspacing_hist = si_dspacing.hist(two_theta=ntheta, dspacing=dbins)
si_dspacing_hist.plot(norm="log", vmin=1.0e-3)
../_images/a5f46603b47763032eb1bfd3cb9dc6a4de0fa908404d89c8410e5dfd9066789f.svg
si_dspacing_hist.sum("two_theta").plot()
../_images/f5e61dee650b35712b327fa4fd610d24903412c99b7c919a9550fc076415cda2.svg

Exercise 2: Correct the time-of-flight#

Time-of-flight at a long-pulsed source#

The time-of-flight of a neutron is typically not known from experimental measurements. The only two parameters that we are able to record are usually the time at which the pulse was emitted (named time_origin in our dataset), and the timestamp when a neutron was detected by a detector pixel (named toa, standing for time-of-arrival).

The difference between these two times is commonly used to compute an approximate time-of-flight. This approximation performs well at neutron sources with a short pulse, where it is satisfactory to assume that all neutrons left the source at the same time.

At a long-pulsed source (lasting a few milliseconds) such as ESS, with high-precision instruments, this approximation is no longer valid.

This is in fact the reason why in the \(2\theta\) vs d-spacing plot, the bright lines appear curved while they should in reality be vertical.

In the following exercise, we will add a correction to our time-of-flight computation to improve our results.

Finding a new time-distance origin#

One way to look at the problem is to visualize the paths of each neutron that made it to the detector, on a time-distance diagram.

The neutrons begin at the source (bottom left), travel along the guide, go through the chopper, and finally arrive at the detectors (top).

utils.time_distance_diagram(si_wav)
../_images/587f8833e470180d1acf35e0fee1d898fe04fdd6cd97d050c7e1a0805a333a4b.png

As the figure above illustrates, the current time-of-flight computation (tof = toa - time_origin) assumes that all neutrons were born at \(t = 0\) (black dot).

This is clearly not the case from looking at the neutron tracks.

Exercise: find a new origin#

  • Inspect the time-distance diagram and try to find a point which would be a better representation of a common origin for all tracks.

  • Set the origin as coordinates on the original data (hint: you will need to update the source_position and time_origin coordinates).

  • Compute wavelength/d-spacing again.

Solution:

Hide code cell content

better = sample_si.copy(deep=False)

# Read off the plot that the lines cross as they go through the chopper:
# time=5.53, distance=6.5 (approximately)
better.coords["source_position"] = sc.vector([0, 0, 6.5], unit="m")
better.coords["time_origin"] = sc.scalar(5.53, unit="ms").to(unit="s")

better_dspacing = better.transform_coords("dspacing", graph=graph)
better_dspacing
Show/Hide data repr Show/Hide attributes
scipp.DataArray (56.24 MB)
    • events: 350963
    • L1
      ()
      float64
      m
      154.12753605076213
      Values:
      array(154.12753605)
    • L2
      (events)
      float64
      m
      3.510, 3.500, ..., 3.524, 3.500
      Values:
      array([3.50982995, 3.50020089, 3.50180757, ..., 3.51623467, 3.52422421, 3.50020089])
    • Ltotal
      (events)
      float64
      m
      157.637, 157.628, ..., 157.652, 157.628
      Values:
      array([157.637366 , 157.62773694, 157.62934362, ..., 157.64377072, 157.65176026, 157.62773694])
    • dspacing
      (events)
      float64
      Å
      1.898, 0.787, ..., 1.916, 1.634
      Values:
      array([1.89756669, 0.78729993, 0.78830554, ..., 1.63577631, 1.9155775 , 1.63447523])
    • incident_beam
      ()
      vector3
      m
      [ 1.47918852 0. 154.12043787]
      Values:
      array([ 1.47918852, 0. , 154.12043787])
    • position
      (events)
      vector3
      m
      [ 4.97355937 0.2625 160.42201312], [ 3.10883934e+00 -3.75000000e-02 1.57522981e+02], ..., [ -1.97529876 0.4125 160.05783889], [-1.32519983e+00 -3.75000000e-02 1.58526302e+02]
      Values:
      array([[ 4.97355937e+00, 2.62500000e-01, 1.60422013e+02], [ 3.10883934e+00, -3.75000000e-02, 1.57522981e+02], [ 3.13580735e+00, 1.12500000e-01, 1.57537320e+02], ..., [-1.32519983e+00, 3.37500000e-01, 1.58526302e+02], [-1.97529876e+00, 4.12500000e-01, 1.60057839e+02], [-1.32519983e+00, -3.75000000e-02, 1.58526302e+02]])
    • sample_position
      ()
      vector3
      m
      [ 1.47918852 0. 160.62043787]
      Values:
      array([ 1.47918852, 0. , 160.62043787])
    • scattered_beam
      (events)
      vector3
      m
      [ 3.49437085 0.2625 -0.19842476], [ 1.62965082 -0.0375 -3.09745673], ..., [-3.45448728 0.4125 -0.56259898], [-2.80438834 -0.0375 -2.0941361 ]
      Values:
      array([[ 3.49437085, 0.2625 , -0.19842476], [ 1.62965082, -0.0375 , -3.09745673], [ 1.65661884, 0.1125 , -3.08311758], ..., [-2.80438834, 0.3375 , -2.0941361 ], [-3.45448728, 0.4125 , -0.56259898], [-2.80438834, -0.0375 , -2.0941361 ]])
    • sim_source_time
      (events)
      float64
      s
      0.001, 0.003, ..., 0.001, 0.001
      Values:
      array([0.00135692, 0.00342941, 0.00342941, ..., 0.00051112, 0.00051112, 0.00051112])
    • sim_speed
      (events)
      float64
      m/s
      1444.839, 2607.873, ..., 1348.320, 1348.320
      Values:
      array([1444.83867569, 2607.87309718, 2607.87309718, ..., 1348.3203984 , 1348.3203984 , 1348.3203984 ])
    • sim_wavelength
      (events)
      float64
      Å
      2.738, 1.517, ..., 2.934, 2.934
      Values:
      array([2.73804549, 1.51695802, 1.51695802, ..., 2.93404596, 2.93404596, 2.93404596])
    • source_position
      ()
      vector3
      m
      [0. 0. 6.5]
      Values:
      array([0. , 0. , 6.5])
    • time_origin
      ()
      float64
      s
      0.00553
      Values:
      array(0.00553)
    • toa
      (events)
      float64
      s
      0.115, 0.066, ..., 0.122, 0.122
      Values:
      array([0.11494571, 0.06636537, 0.06636959, ..., 0.12225075, 0.12225605, 0.1222465 ])
    • tof
      (events)
      float64
      s
      0.109, 0.061, ..., 0.117, 0.117
      Values:
      array([0.10941571, 0.06083537, 0.06083959, ..., 0.11672075, 0.11672605, 0.1167165 ])
    • two_theta
      (events)
      float64
      rad
      1.618, 2.648, ..., 1.741, 2.222
      Values:
      array([1.61779013, 2.64755989, 2.63800169, ..., 2.2182896 , 1.74064946, 2.22175843])
    • wavelength
      (events)
      float64
      Å
      2.746, 1.527, ..., 2.929, 2.929
      Values:
      array([2.74587342, 1.52680487, 1.52689526, ..., 2.92908022, 2.9290647 , 2.92927151])
    • x
      (events)
      float64
      m
      4.974, 3.109, ..., -1.975, -1.325
      Values:
      array([ 4.97355937, 3.10883934, 3.13580735, ..., -1.32519983, -1.97529876, -1.32519983])
    • y
      (events)
      float64
      m
      0.262, -0.037, ..., 0.413, -0.037
      Values:
      array([ 0.2625, -0.0375, 0.1125, ..., 0.3375, 0.4125, -0.0375])
    • z
      (events)
      float64
      m
      160.422, 157.523, ..., 160.058, 158.526
      Values:
      array([160.42201312, 157.52298114, 157.53732029, ..., 158.52630177, 160.05783889, 158.52630177])
    • (events)
      float64
      counts
      148.534, 1.224, ..., 3.150e-15, 2.622e-15
      σ = 148.534, 1.224, ..., 3.150e-15, 2.622e-15
      Values:
      array([1.48534080e+02, 1.22398205e+00, 1.22398205e+00, ..., 2.62220306e-15, 3.14989726e-15, 2.62220306e-15])

      Variances (σ²):
      array([2.20623729e+04, 1.49813206e+00, 1.49813206e+00, ..., 6.87594887e-30, 9.92185272e-30, 6.87594887e-30])
better_dspacing.hist(two_theta=ntheta, dspacing=dbins).plot(norm="log", vmin=1.0e-3)
../_images/fec48785c8c384bcd9c56505b41570dda37f8536c7614984ffe3ab9bd36e53d6.svg
pp.plot(
    {
        "original": si_dspacing_hist.sum("two_theta"),
        "improved": better_dspacing.hist(two_theta=ntheta, dspacing=dbins).sum(
            "two_theta"
        ),
    }
)
../_images/83c0aa2e50927d927239814d33459672b79fa5e8d20f53cc1a53845126e22bfb.svg

Exercise 3: Process the Vanadium run#

We now need to repeat the process (load data, correct time-of-flight, convert to d-spacing) for the Vanadium run.

folder = "../3-mcstas/output_sample_vanadium"
sample_van = utils.load_powder(folder)

Hide code cell content

# Read off the plot that the lines cross as they go through the chopper:
# time=5.53, distance=6.5 (approximately)
sample_van.coords["source_position"] = sc.vector([0, 0, 6.5], unit="m")
sample_van.coords["time_origin"] = sc.scalar(5.53, unit="ms").to(unit="s")
# Convert to dspacing
van_dspacing = sample_van.transform_coords("dspacing", graph=graph)
van_dspacing.hist(two_theta=ntheta, dspacing=dbins).plot(norm="log", vmin=1.0e-3)
../_images/2e7c8568bbb9cb77f169f294f27f71bb9ab6e9b0392f66188f73ebfdc7ff7411.svg

Normalize by Vanadium#

The next step in our data reduction is to normalize our sample signal by the Vanadium counts.

Vanadium is an almost perfect incoherent scatterer (scatters equally in all directions) and the signal from that run can be used to correct for difference in detector exposure and sensitivity.

Performing accurate normalization is a complex subject, but in our simple idealised set-up, it basically boils down to a simple division operation.

We first histogram the sample and vanadium data using the same binning for both:

num = better_dspacing.hist(two_theta=ntheta, dspacing=dbins)
den = van_dspacing.hist(two_theta=num.coords["two_theta"], dspacing=dbins)

For best results, it is common practice to smooth the Vanadium signal before using it for normalization, to remove any noise that mostly comes from collection statistics rather than detector efficiency.

We thus smooth the Vandium histogram using a simple Gaussian filter. We will normalize the signal as a 1D histogram, so the data is first summed over the two_theta dimension. In Scipp, because of the physical units, we can define a physical kernel width as opposed to having to determine how many neighbours are necessary for good smoothing.

# Smooth the Vanadium run
from scipp.scipy.ndimage import gaussian_filter

smooth_vana = gaussian_filter(
    sc.values(den.sum("two_theta")), sigma=sc.scalar(0.03, unit="angstrom")
)

pp.plot(
    {"orig": den.sum("two_theta"), "smooth": smooth_vana}, title="Vanadium - smoothed"
)
../_images/619b0a2dc1ec707bb27ae82470753ec72efd58ea4966e4fc955a1c5b978a8a44.svg

Exercise 4: perform the normalization#

Finally, we divide the sample histogram num (summed over two_theta) by the smoothed Vanadium.

Hide code cell content

normed = num.sum("two_theta") / smooth_vana
normed.plot()
../_images/3ea99d4d6fc82c136cb8b08978e03580adbef42de8751baf9869f8597ced4c39.svg

You can compare to the plot above (before normalization) and see that the relative intensities of the Si peaks have changed.

Save to disk#

We now need to save our reduced normalized data to a file, that will then be used by the analysis software in the next chapter.

For this, we use a small helper function:

utils.save_xye(
    "reduced_Si.xye",
    event_data=better_dspacing,  # Needed for saving metadata required for analysis
    normalized_histogram=normed,
)

Exercise 5: Process and normalize main sample: LBCO#

Repeat the process for the LBCO sample:

  • load data

  • correct time-of-flight

  • convert to d-spacing

  • normalize by Vanadium

  • save to disk

folder = "../3-mcstas/output_sample_LBCO"

Hide code cell content

lbco = utils.load_powder(folder)

# Read off the plot that the lines cross as they go through the chopper:
# time=5.53, distance=6.5 (approximately)
lbco.coords["source_position"] = sc.vector([0, 0, 6.5], unit="m")
lbco.coords["time_origin"] = sc.scalar(5.53, unit="ms").to(unit="s")

lbco_dspacing = lbco.transform_coords("dspacing", graph=graph)
lbco_dspacing.hist(two_theta=ntheta, dspacing=dbins).plot(norm="log", vmin=1.0e-3)

# Normalize
lbco_num = lbco_dspacing.hist(two_theta=den.coords["two_theta"], dspacing=dbins)

lbco_normed = lbco_num.sum("two_theta") / smooth_vana

lbco_normed.plot()
../_images/f9e27c06ddc4efd296bdf088b0d7fb35aa240702bd083793f0e02264896b69c4.svg

Hide code cell content

# Save to disk
utils.save_xye(
    "reduced_LBCO.xye",
    event_data=lbco_dspacing,  # Needed for saving metadata required for analysis
    normalized_histogram=lbco_normed,
)

Sciline workflow#

Pause here if you have not yet been introduced to Sciline!

Below we build a re-usable Sciline workflow for reducing the powder diffraction data. We wrap the operations that were carried out throughout this notebook into function that have the correct type-annotations, building a pipeline of connected steps.

The custom types have been created for you in the powder_utils module, and are imported from there.

Take some time to read through the code and understand the different parts. Ask questions if anything is unclear.

import sciline as sl
from powder_utils import *


def load(folder: Foldername[RunType]) -> RawData[RunType]:
    """Load raw data from file"""
    return RawData[RunType](utils.load_powder(folder))


def correct_beamline_params(
    data: RawData[RunType], source_position: SourcePosition, time_origin: TimeOrigin
) -> CorrectedData[RunType]:
    """Correct the source position and time origin for a better time-of-flight estimate"""
    corrected = data.copy(deep=False)
    corrected.coords["source_position"] = source_position
    corrected.coords["time_origin"] = time_origin
    return CorrectedData[RunType](corrected)


def to_dspacing(
    data: CorrectedData[RunType], graph: CoordTransformGraph
) -> DspacingData[RunType]:
    """Compute dspacing for events"""
    return DspacingData[RunType](data.transform_coords("dspacing", graph=graph))


def to_histogram(
    data: DspacingData[RunType],
    two_theta_bins: TwoThetaBins,
    dspacing_bins: DspacingBins,
) -> HistogramTwothetaDspacing[RunType]:
    """Histogram event data using two_theta and dspacing binning"""
    return HistogramTwothetaDspacing[RunType](
        data.hist(two_theta=two_theta_bins, dspacing=dspacing_bins)
    )


def smooth_vanadium(
    van: HistogramTwothetaDspacing[VanadiumRun], sigma: GaussianSmoothingSigma
) -> SmoothedVanadium:
    """Smooth the Vanadium data using a Gaussian kernel"""
    return SmoothedVanadium(
        gaussian_filter(sc.values(van.sum("two_theta")), sigma=sigma)
    )


def sum_over_two_theta(
    sample: HistogramTwothetaDspacing[SampleRun],
) -> SampleDspacing:
    """Sum the sample data over the two_theta dimension so we are left with dspacing"""
    return SampleDspacing(sample.sum("two_theta"))


def normalize(sample: SampleDspacing, van: SmoothedVanadium) -> NormalizedDspacing:
    """Normalize sample by vanadium"""
    return NormalizedDspacing(sample / van)


wf = sl.Pipeline(
    (
        load,
        correct_beamline_params,
        to_dspacing,
        to_histogram,
        smooth_vanadium,
        sum_over_two_theta,
        normalize,
    ),
    constraints={RunType: [SampleRun, VanadiumRun]},
)

wf.visualize()
../_images/4b3c7eea48792e98bb84dcd4ca7f50302fe0b33c1e8f76c1a54b81b0dbd029a0.svg

Exercise 6: Set the workflow parameters#

In the visualization above, the red boxes indicate missing values; parameters required by the computation that have yet to be set on the workflow.

Using the wf[TYPE] = VALUE syntax, set the missing parameters on the workflow.

Once you have done so, use wf.compute(NormalizedDspacing) to compute the final normalized result for the Si and LBCO samples, remembering to update the Foldername[SampleRun] for each case.

Solution:

Hide code cell content

# Parameters common to both samples
wf[SourcePosition] = sc.vector([0, 0, 6.5], unit="m")
wf[TimeOrigin] = sc.scalar(5.53, unit="ms").to(unit="s")

wf[CoordTransformGraph] = graph
wf[TwoThetaBins] = sc.linspace("two_theta", 0, 3, 129, unit="rad")
wf[DspacingBins] = dbins
wf[GaussianSmoothingSigma] = sc.scalar(0.03, unit="angstrom")

wf[Foldername[VanadiumRun]] = "../3-mcstas/output_sample_vanadium"

# Compute for Si sample
wf[Foldername[SampleRun]] = "../3-mcstas/output_sample_Si"
result_si = wf.compute(NormalizedDspacing)

# Compute for LBCO sample
wf[Foldername[SampleRun]] = "../3-mcstas/output_sample_LBCO"
result_lbco = wf.compute(NormalizedDspacing)

result_si.plot(title="Si sample") + result_lbco.plot(title="LBCO sample")
../_images/0515b49929469b1a7efb6332080b672708e2c5eefc661401cd2fb3e6434b9d96.svg

Exercise 7 (optional): Modify the dspacing binning#

Change the number of bins used in the dspacing dimension on the workflow, and recompute the result.

Hint: use wf[DspacingBins] = ...

Solution:

Hide code cell content

wf[DspacingBins] = sc.linspace("dspacing", 0.7, 2.5, 64, unit="angstrom")
new = wf.compute(NormalizedDspacing)

pp.plot({"old": result_lbco, "new": new})
../_images/22e09f07fbc847e32303aa0ccd4c1cc4e961b5f41e5d0f7fc58f057acffdd25f.svg

Exercise 8: Debugging detector banks#

The detector team suspects that one of the two cylindrical banks is faulty (recording too many counts), and you have to help them find out which one (if any!).

  • Identify an intermediate result in the workflow graph that contains the necessary information.

  • Separate the events between left and right detector banks (hint: you can either use masks or smart indexing: da[da.coords['x'] < x0])

  • Histogram both banks using the dspacing bins

  • Plot both histograms on the same axes (hint: send a dict to the plotting function pp.plot({"left": left_hist, "right": right_hist}))

Solution:

Hide code cell content

da = wf.compute(DspacingData[SampleRun])

x0 = da.coords["sample_position"].fields.x
left = da[da.coords["x"] < x0].hist(dspacing=dbins)
right = da[da.coords["x"] > x0].hist(dspacing=dbins)

pp.plot({"left": left, "right": right})
../_images/f6ce3049d1d986d72bca95877e03103341cfc26ebca9ef1b8ab28dae223f70a1.svg