import mcstasscript as ms
import make_powder_instrument
from mcstasutils import plot
import quizlib
quiz = quizlib.Powder_Quiz()

Powder diffraction exercise#

Diffraction#

Diffraction instruments aim to measure the elastic scattering from the sample, meaning the neutron does not change energy. Since elastic scattering is much more probable than inelastic scattering, diffraction instruments typically assume all neutrons have the same energy before and after interacting with the sample. The elastic signal provides information on the crystal structure of the sample through Bragg’s law, \(n\lambda = 2d \text{sin}(\theta)\), where \(n\) is the scattering order, \(\lambda\) is the neutron wavelength, \(d\) is a distance between two planes of atoms in the sample and \(\theta\) is the scattering angle. There will be many different planes of atoms in a crystal, and by measuring the distance between many of these the crystal structure can be ascertained through analysis. Thus a diffractometer needs to measure scattering intensity as a function of neutron wavelength \(\lambda\) and scattering angle \(\theta\).

Powder diffraction#

This instrument is a powder diffractometer, meaning the measurements are performed on a powder containing all crystal orientations. For this reason there is no need to rotate the sample. It is however relevant to measure a large range of scattering angles, as several can be in the Bragg condition simultaneously.

This exercise#

In this notebook you will work with a McStas model of a simplified powder diffraction instrument. You will have to answer questions in the notebook by working with this model, both by running simulations and expanding the model. We will use the Python McStas API McStasScript to work with the instrument, you can find documentation here.

Get the instrument object#

First we need the McStas instrument object. Here it is retrieved from a local python function that generates it.

instrument = make_powder_instrument.make()

Investigate instrument#

The first task is to investigate the instrument object instrument using some of the available methods available on that object. Each method that show something about the instrument starts with the word show, so you can use tab to autocomplete in the cell to see the relevant methods.

In particular, look at what parameters are available and take a look at the instrument geometry.

Hide code cell content

instrument.show_parameters()
       l_min            = 0.5          // Minimum simulated wavelength [AA]
       l_max            = 4.0          // Maximum simulated wavelength [AA]
int    n_pulses         = 1            // Number of simulated pulses
       guide_curve_deg  = 1.0          // total curvature of guide [deg]
       detector_height  = 2.5          // Height of detector banks [m]
       sample_radius    = 0.005        // Radius of simulated sample [m]
       sample_height    = 0.02         // Height of simulated sample [m]
string sample_choice    = "no_sample"  // Choice of sample, given as string

Hide code cell content

instrument.show_diagram()
../_images/f3c5feefcdc7d1b12227161ef548982c3863531dc53c4dedc50a05a974aeb836.png ../_images/b714ef588f0994e5b466e923cf9fd51a7d62e635d9469ab7f20eb1615cb82e86.png

Select sample size#

Select appropriate sample size given Vanadium has a macroscopic scattering cross section \(\Sigma\) of around 0.35 cm\(^{-1}\). A neutron beam with intensity \(I_0\) that travel in a media a distance of \(z\) will be attenuated as the neutrons scatter in the material, and the remaining intensity \(I\) can be calculated with the Beer-Lambert law:

\[ I = I_0 e^{-z\Sigma} \]

For our experiment we want to observe neutrons that scattered once, as neutrons that scatter more than once would be considered background.

What sample depth would be appropriate?

  • A: 10 cm

  • B: 1 cm

  • C: 1 mm

import numpy as np
fraction_left = np.exp(-0.35*1.0) # 1 cm
fraction_scattered = 1 - fraction_left 
print(f"scattering probability {100*fraction_scattered:.1f} %")
# Assume same distance before and after each scattering (rough approximation)
print(f"single scattering probability {100*fraction_scattered*fraction_left:.1f} %")
print(f"double scattering probability {100*fraction_scattered**2*fraction_left:.1f} %")
print(f"fraction of single scattering to multiple scattering {100*fraction_scattered*fraction_left/fraction_scattered:.1f} %")
scattering probability 29.5 %
single scattering probability 20.8 %
double scattering probability 6.1 %
fraction of single scattering to multiple scattering 70.5 %
quiz.question_1()

Hide code cell content

quiz.question_1("B")
Correct!
Yes, this is a reasonable mix, here there is a 30% probability for a neutron to
scatter, so about 20% scatters exactly once.

Work with instrument simulation#

Set sample size#

Use the set_parameters method on the instrument object to set the sample thickness, here using the parameter sample_radius. At first we will work with a narrow wavelength range of 2.5 - 2.501 Å, this should be specified using the l_min and l_max parameters.

Hide code cell content

wavelength = 2.5
instrument.set_parameters(sample_radius=0.005, l_min=wavelength, l_max=wavelength + 0.001)
# Test your instrument by giving it to the question_2 function
quiz.question_2(instrument)

Hide code cell output

Correct!
The parameters of the instrument were correctly set!

Instrument settings#

Before running the simulation, a few settings pertaining to computing options need to be specified. This is done with a different method to clearly distinguish these from the instrument parameters. One important setting is called output_path which sets the name of the generated folder with simulation output.

instrument.settings(ncount=4.0e6, mpi=4, suppress_output=True, NeXus=True, output_path="first_run")

Run simulation#

Now the simulation can be executed with the backengine method. This method returns a list of data objects. Store this returned data in a python variable named data.

Hide code cell content

data = instrument.backengine()

Visualize the data#

The data objects in the returned list can be plotted with the McStasScript function make_sub_plot.

Plot overview#

The function should plot three graphs:

PSD monitor#

Shows spatial distribution of intensity at the sample position.

Time of flight monitor#

Shows intensity as a function of neutron arrival time at the sample position.

Wavelength monitor#

Shows intensity as a function of neutron wavelength at the sample position. Since a tiny wavelength interval was used, this simulation is almost for a constant wavelength.

Hide code cell content

ms.make_sub_plot(data)
../_images/69e31b8c59f2d03656811f7f650271f31e3cc9d8ccafb0823e889145c2ff14cf.png

Time / wavelength resolution#

In order to analyze data from this instrument, the neutron energy is needed. On a pulsed source, this can be calculated from the arrival time of the neutrons and the known distance between the source and sample. Since the simulation was performed with a constant wavelength, the width of the time distribution can be used to deduce the uncertainty in the computed energy.

Whats the relative uncertainty on the time observed at the sample position, FWHM?

Insert value as a percentage.

Hide code cell content

print("delta t/t", (1.0453E5 - 1.0173E5)/(0.5*(1.0453E5 + 1.0173E5))*100, "%")
delta t/t 2.7150198778241057 %
quiz.question_3()

Hide code cell content

quiz.question_3(2.715)
Correct!
Yes!

This will correspond to the wavelength resolution at this wavelength, generally we need less than 1% for powder diffraction, is this sufficient?

  • A: yes

  • B: no

quiz.question_4()

Hide code cell content

quiz.question_4("B")
Correct!
Yes, this would result in insufficient wavelength resolution for powder
diffraction.

How could we improve this?

  • A: monochromator close to the sample

  • B: chopper close to the source

  • C: velocity selector close to the sample

  • D: shorter guide

quiz.question_5()

Hide code cell content

quiz.question_5("B")
Correct!
Yes, that would ensure we knew that all neutrons passed through that point in
the guide at the same time, improving the time uncertainty.

Improve the instrument#

In order to improve the performance of the instrument, we will add a McStas component. The first aspect to consider when doing so is where to place it, both in the component sequence and its physical location. We start by investigating where to put the component in the component sequence, as that needs to be specified when adding the component, and the physical location can be updated later.

McStas sequence#

Use either the show_diagram or show_components method on the instrument object to get an overview of the component sequence in the instrument. Where would you place the new component? Keep in mind that the shielding around the source extends to a radius of 6 m, and it would not be possible to place it closer to the source than that distance.

  • A: before feeder guide section

  • B: after feeder guide section

  • C: before expanding guide section

  • D: after expanding guide section

Hide code cell content

instrument.show_components()
Source  ESS_butterfly 
  AT      (0, 0, 0) ABSOLUTE
feeder  Elliptic_guide_gravity 
  AT      (0, 0, 2.0) RELATIVE Source
expanding  Elliptic_guide_gravity 
  AT      (0, 0, 6.55) RELATIVE Source
guide_0  Guide_gravity 
  AT      (0, 0, 10.003) RELATIVE expanding 
  ROTATED (0, guide_curve_deg/12.0, 0) RELATIVE expanding
guide_1  Guide_gravity 
  AT      (0, 0, 11.123833333333332) RELATIVE guide_0 
  ROTATED (0, guide_curve_deg/12.0, 0) RELATIVE guide_0
guide_2  Guide_gravity 
  AT      (0, 0, 11.123833333333332) RELATIVE guide_1 
  ROTATED (0, guide_curve_deg/12.0, 0) RELATIVE guide_1
guide_3  Guide_gravity 
  AT      (0, 0, 11.123833333333332) RELATIVE guide_2 
  ROTATED (0, guide_curve_deg/12.0, 0) RELATIVE guide_2
guide_4  Guide_gravity 
  AT      (0, 0, 11.123833333333332) RELATIVE guide_3 
  ROTATED (0, guide_curve_deg/12.0, 0) RELATIVE guide_3
guide_5  Guide_gravity 
  AT      (0, 0, 11.123833333333332) RELATIVE guide_4 
  ROTATED (0, guide_curve_deg/12.0, 0) RELATIVE guide_4
guide_6  Guide_gravity 
  AT      (0, 0, 11.123833333333332) RELATIVE guide_5 
  ROTATED (0, guide_curve_deg/12.0, 0) RELATIVE guide_5
guide_7  Guide_gravity 
  AT      (0, 0, 11.123833333333332) RELATIVE guide_6 
  ROTATED (0, guide_curve_deg/12.0, 0) RELATIVE guide_6
guide_8  Guide_gravity 
  AT      (0, 0, 11.123833333333332) RELATIVE guide_7 
  ROTATED (0, guide_curve_deg/12.0, 0) RELATIVE guide_7
guide_9  Guide_gravity 
  AT      (0, 0, 11.123833333333332) RELATIVE guide_8 
  ROTATED (0, guide_curve_deg/12.0, 0) RELATIVE guide_8
guide_10  Guide_gravity 
  AT      (0, 0, 11.123833333333332) RELATIVE guide_9 
  ROTATED (0, guide_curve_deg/12.0, 0) RELATIVE guide_9
guide_11  Guide_gravity 
  AT      (0, 0, 11.123833333333332) RELATIVE guide_10 
  ROTATED (0, guide_curve_deg/12.0, 0) RELATIVE guide_10
focusing  Elliptic_guide_gravity 
  AT      (0, 0, 11.123833333333332) RELATIVE guide_11
guide_end  Arm 
  AT      (0, 0, 10) RELATIVE focusing
PSD_sample  PSD_monitor 
  AT      (0, 0, 0.6) RELATIVE guide_end
wavelength  L_monitor 
  AT      (0, 0, 0.6) RELATIVE guide_end
arrival_time  TOF_monitor 
  AT      (0, 0, 0.6) RELATIVE guide_end
sample_position  Arm 
  AT      (0, 0, 0.6) RELATIVE guide_end
fixed_source  Arm 
  AT      (0, 0, -160.6) RELATIVE sample_position
init  Union_init 
  AT      (0, 0, 0) ABSOLUTE
sample_Si_Si_inc  Incoherent_process 
  AT      (0, 0, 0) ABSOLUTE
sample_Si_Si_pow  Powder_process 
  AT      (0, 0, 0) ABSOLUTE
sample_Si  Union_make_material 
  AT      (0, 0, 0) ABSOLUTE
sample_LBCO_La0_5Ba0_5CoO3_inc  Incoherent_process 
  AT      (0, 0, 0) ABSOLUTE
sample_LBCO_La0_5Ba0_5CoO3_pow  Powder_process 
  AT      (0, 0, 0) ABSOLUTE
sample_LBCO_Si_inc  Incoherent_process 
  AT      (0, 0, 0) ABSOLUTE
sample_LBCO_Si_pow  Powder_process 
  AT      (0, 0, 0) ABSOLUTE
sample_LBCO  Union_make_material 
  AT      (0, 0, 0) ABSOLUTE
sample_Vanadium_Vanadium_inc  Incoherent_process 
  AT      (0, 0, 0) ABSOLUTE
sample_Vanadium  Union_make_material 
  AT      (0, 0, 0) ABSOLUTE
start_union_geometries  Arm 
  AT      (0, 0, 0) ABSOLUTE
Sample_sample_Vanadium  Union_cylinder 
  AT      (0, 0, 0) RELATIVE sample_position
Sample_sample_LBCO  Union_cylinder 
  AT      (0, 0, 0) RELATIVE sample_position
Sample_sample_Si  Union_cylinder 
  AT      (0, 0, 0) RELATIVE sample_position
master  Union_master 
  AT      (0, 0, 0) ABSOLUTE
stop  Union_stop 
  AT      (0, 0, 0) ABSOLUTE
Banana_large_0  Monitor_nD 
  AT      (0, 0, 0.0) RELATIVE sample_position
Banana_large_1  Monitor_nD 
  AT      (0, 0, 0.0) RELATIVE sample_position
quiz.question_6()

Hide code cell content

quiz.question_6("B")
Correct!
Yes, the feeder ends just outside the monolith, so this is a great spot

Which component#

Now we need to select what type of component to add to the instrument, here we will need the DiskChopper component. Use the component_help method on the instrument object to learn more about this component.

Hide code cell content

instrument.available_components("optics")
instrument.component_help("DiskChopper")
Here are all components in the optics category.
 Absorber                 Guide_channeled        Pol_FieldBox
 Arm                      Guide_gravity          Pol_SF_ideal
 Beamstop                 Guide_simple           Pol_bender
 Bender                   Guide_tapering         Pol_constBfield
 Collimator_linear        Guide_wavy             Pol_guide_mirror
 Collimator_radial        He3_cell               Pol_guide_vmirror
 Derotator                Mask                   Pol_mirror
 Diaphragm                Mirror                 Pol_tabled_field
 DiskChopper              Monochromator_curved   Refractor
 Elliptic_guide_gravity   Monochromator_flat     Rotator
 FZP_simple               Monochromator_pol      Selector
 FermiChopper             Place                  Set_pol
 Filter_gen               PolAnalyser_ideal      Slit
 Guide                    Pol_Bfield             V_selector
 Guide_anyshape           Pol_Bfield_stop        Vitess_ChopperFermi
 ___ Help DiskChopper _______________________________________________________________
|optional parameter|required parameter|default value|user specified value|
theta_0 = 0.0 [deg] // Angular width of the slits.
radius = 0.5 [m] // Radius of the disc
yheight [m] // Slit height (if = 0, equal to radius). Auto centering of beam at 
               half height. 
nu [Hz] // Frequency of the Chopper, omega=2*PI*nu (algebraic sign defines the 
           direction of rotation) 
nslit = 3.0 [1] // Number of slits, regularly arranged around the disk
jitter = 0.0 [s] // Jitter in the time phase
delay = 0.0 [s] // Time 'delay'
isfirst = 0.0 [0/1] // Set it to 1 for the first chopper position in a cw 
                       source (it then spreads the neutron time distribution) 
n_pulse = 1.0 [1] // Number of pulses (Only if isfirst)
abs_out = 1.0 [0/1] // Absorb neutrons hitting outside of chopper radius?
phase = 0.0 [deg] // Angular 'delay' (overrides delay)
xwidth = 0.0 [m] // Horizontal slit width opening at beam center
verbose = 0.0 [1] // Set to 1 to display Disk chopper configuration
-------------------------------------------------------------------------------------

Chopper calculations#

When adding a chopper, one needs to perform calculations to obtain the appropriate rotation frequency and phase. For this exercise, those calculations can be added to the instrument using a function included in the given Python file.

make_powder_instrument.add_chopper_calculations(instrument)

# If low on time, its possible to add the calculations and choppper component, solving question 7 and 8. Only run one of the two functions.
#make_powder_instrument.add_chopper(instrument)

To see what variables are used in the instrument, one can use the show_variables method like below. This will include the ones added by the above function.

Hide code cell content

instrument.show_variables()
DECLARE VARIABLES 
type    variable name           array length  value  
---------------------------------------------------
double  min_time                                     
double  max_time                                     
int     sample_Si_active                             
int     sample_LBCO_active                           
int     sample_Vanadium_active                       
double  chopper_position                      6.5    
double  speed                                        
double  delay                                        

USER VARIABLES (per neutron, only use in EXTEND)
type    variable name           array length  value  
---------------------------------------------------
double  source_time                                  
double  n_scattering_sample                          

Add chopper component and set parameters#

Use the add_component method on the instrument object to add a chopper. Place it in the component sequence according to your answer in question 6 by using either the before or after keyword argument.

The add_component method returns a component object, store that in a python variable.

Set the following parameters on the chopper component object:

  • yheight: 0.05 m

  • radius: 0.35 m

  • nslit: 1.0

  • theta_0: 7.0 deg

  • delay: To the variable calculated in the instrument (use quotation marks)

  • nu: A calculation using the frequency_multiplier variable, “frequency_multiplier*14.0”

Hide code cell content

chopper = instrument.add_component("chopper", "DiskChopper", after="feeder")
chopper.theta_0 = 7.0
chopper.nslit = 1
chopper.radius = 0.35
chopper.yheight = 0.05
chopper.nu = "frequency_multiplier*14.0"
chopper.delay = "delay"  # Variable with calculated delay
quiz.question_7(instrument)

Hide code cell output

Correct!
The DiskChopper was added at the right point in the component sequence and with
correct parameters!

Placing the component in space#

The next task is to specify the physical location of the component, this is done using the set_AT method on the component object. This method takes a list of 3 numbers, corresponding to the x, y and z coordinates of the component. If just a single number is specified instead of a list, that is assumed to be the z coordinate.

One can also specify in what coordinate system one wants to work, which can be that of any preceding component. Use the RELATIVE keyword to work in the Source coordinate system. The position of the chopper is needed for calculating the phase, so it is available as a variable in the instrument, use this variable to set the position.

Set the chopper position, a variable is already created with the name “chopper_position” and value of 6.5 meters, this is the distance relative to the neutron Source.

Hide code cell content

chopper.set_AT("chopper_position", RELATIVE="Source")

To check if the component was added correctly, provide your instrument object to the question_8 function below.

quiz.question_8(instrument)

Hide code cell output

Correct!
The DiskChopper was added at the right point in the space!

Verify new component#

Now that the chopper has been added to the instrument, let’s show the component sequence again to verify it was added correctly.

Hide code cell content

instrument.show_diagram()
../_images/26f1f365ff4844710c791c879ff4b128cf75b7ae72f04f55d7204c5e8dbaba7a.png ../_images/657bd819c7313ce49508397f0d87e97df6e6f7582ae91b96c1886b0b18936ce3.png

Run improved instrument#

Run the improved instrument with the parameter frequency_multiplier set to 3 and plot the data. The frequency_multiplier parameter controls the ratio of the frequency of the chopper and source, this should be a natural number to keep the two in sync.

Use the settings method to set a reasonable name for this simulation run.

Hide code cell content

instrument.set_parameters(frequency_multiplier=3)
instrument.settings(output_path="improved_instrument")
data = instrument.backengine()

Plot the data using the make_sub_plot function.

Hide code cell content

ms.make_sub_plot(data)
../_images/9f981a3cdbae259e87c8149d7288aca7cc3507911b301ad42d1f673e963a559d.png

Whats the relative time uncertainty with this setup? Insert the answer as a percentage.

Hide code cell content

print("delta t/t", (1.0315e5 - 1.0267e5) / 1.0289e5 * 100, "%")
delta t/t 0.46651764019826997 %
quiz.question_9()

Hide code cell content

quiz.question_9(0.4665)
Correct!
Yes!

Set parameters for run with Si sample#

Now we can run a simulation with a sample, set these parameters on the instrument object.

  • l_min: 0.5 Å

  • l_max: 4.0 Å

  • sample_choice: ‘“sample_Si”’ (Need single and double quotes to send a literal string to McStas)

  • detector_height: 1.5 m

Again set a descriptive name using the output_path keyword argument in the settings method on the instrument object.

Hide code cell content

instrument.set_parameters(sample_choice='"sample_Si"', l_min=0.5, l_max=4.0, detector_height=1.5)
instrument.settings(output_path="sample_Si_quick")
quiz.question_10(instrument)
Correct!
The parameters of the instrument were correctly set!

Now we are ready to run the simulation with the backengine method.

Hide code cell content

data = instrument.backengine()

The resulting data list now contains event data from the large detector banks. This can be plotted in many ways, but to simplify this exercise a plot function is provided called plot.

plot(data)
../_images/e3e3f522f7456670e92d1ae9bbf15ffd6255e0f40e79292831623f22fafa0701.png

Describe the data#

What do we see in the plots from event data?

  • A: Inelastic peaks

  • B: Magnetic scattering

  • C: Bragg peaks

quiz.question_11()

Hide code cell content

quiz.question_11("C")
Correct!
Yes, the visible curves follow Braggs law, the incoming wavelength is correlated
with the time and thus the different bragg peaks follow their own curve in
wavelength and theta.

Producing final data sets#

Over the next days you will work with data from this simulation. These simulations need to be performed with a larger number of neutron rays to have sufficient information, and we thus recommend you use the settings below.

Be aware that if these simulations are repeated, new data folders will be created with an appended number. Tomorrow you will need to know which of these contain the data you wish to proceed with.

instrument.settings(ncount=4.0e8, mpi=4, suppress_output=True, NeXus=True)

Run reference sample: Si#

The first dataset use a simple Si sample.

instrument.set_parameters(sample_choice='"sample_Si"', frequency_multiplier=3, detector_height=1.5)
instrument.settings(output_path="output_sample_Si")
data_si = instrument.backengine()
plot(data_si, orders_of_mag=5)
../_images/b462187aeed628c18980268a8e51331bdcf0d45f3b0c94b73aba0cf44c00cbaa.png

Run calibration sample: Vanadium#

Next a simulation is performed with vanadium, this data will be used for normalization.

instrument.set_parameters(sample_choice='"sample_Vanadium"')
instrument.settings(output_path="output_sample_vanadium")

data_vanadium = instrument.backengine()
plot(data_vanadium, orders_of_mag=5)
../_images/20437f8a91881829299a36acfc0c1b3b1a4e57c34803b64cbcb4e6d17e734861.png

Run main sample: LBCO#

The last simulation uses a more complex sample that will be more difficult to analyze.

instrument.set_parameters(sample_choice='"sample_LBCO"')
instrument.settings(output_path="output_sample_LBCO")

data_sample_lbco = instrument.backengine()
plot(data_sample_lbco, orders_of_mag=5)
../_images/ab6f42ae8ae9b22d9d81a26f9e40e796e8df8ee6569d2e2bb9093bfdf0544029.png