Sources#

This notebook will illustrate how to create different kinds of neutron sources for the beamline.

[1]:
import numpy as np
import scipp as sc
import tof

Hz = sc.Unit('Hz')

The ESS source#

Sources are characterized by five aspects:

  • the number of neutrons in a pulse

  • the time at which each neutron is born inside the pulse

  • the wavelength of each neutron inside the pulse

  • the pulse frequency (or repetition rate)

  • the number of pulses

The default way of creating a source is to choose a facility name and a number of neutrons. Each facility defines time and wavelength probability distributions for the neutrons, as well as a pulse frequency. By default, a single pulse is generated.

[2]:
source = tof.Source(facility='ess', neutrons=1_000_000)
source
[2]:
Source:
  pulses=1, neutrons per pulse=1000000
  frequency=14.0 Hz
  facility='ess'

To inspect the data in the pulse, we can either look at the source’s data property

[3]:
source.data
[3]:
Show/Hide data repr Show/Hide attributes
scipp.DataArray (38.15 MB)
    • pulse: 1
    • event: 1000000
    • birth_time
      (pulse, event)
      float64
      µs
      2547.896, 658.950, ..., 1868.216, 2317.192
      Values:
      array([[2547.89649508, 658.94968702, 165.06005351, ..., 3274.20705361, 1868.2155258 , 2317.19154366]], shape=(1, 1000000))
    • id
      (pulse, event)
      int64
      0, 1, ..., 999998, 999999
      Values:
      array([[ 0, 1, 2, ..., 999997, 999998, 999999]], shape=(1, 1000000))
    • speed
      (pulse, event)
      float64
      m/s
      1040.987, 3267.623, ..., 1282.376, 1559.688
      Values:
      array([[1040.98722095, 3267.6227637 , 9635.59354332, ..., 7256.04837532, 1282.37644009, 1559.68767617]], shape=(1, 1000000))
    • wavelength
      (pulse, event)
      float64
      Å
      3.800, 1.211, ..., 3.085, 2.536
      Values:
      array([[3.80027144, 1.21067647, 0.41056464, ..., 0.54520502, 3.08492412, 2.53642705]], shape=(1, 1000000))
    • (pulse, event)
      float64
      counts
      1.0, 1.0, ..., 1.0, 1.0
      Values:
      array([[1., 1., 1., ..., 1., 1., 1.]], shape=(1, 1000000))

or we can plot it with

[4]:
source.plot()
[4]:
_images/sources_7_0.svg

Specifying time and wavelength distributions#

It is also possible to create sources with custom time and wavelength distributions. For this, we need to use the Source.from_distribution() method.

Flat distributions#

The from_distribution method require two arrays that define the time (p_time) and wavelength (p_wav) distributions for the neutrons in the pulse. The array values represent the probabilities, while the associated coordinates represent the values to be sampled from.

We first show how to create flat time and wavelength distributions.

To create a pulse with 1 million neutrons, uniformly distributed in the ranges of 1-3 ms for birth times, and 1-10 Å for wavelengths, we write:

[5]:
birth_time = sc.array(dims=['birth_time'], values=[1.0, 3.0], unit='ms')
p_time = sc.DataArray(
    data=sc.ones(sizes=birth_time.sizes),
    coords={'birth_time': birth_time},
)

wavelength = sc.array(dims=['wavelength'], values=[1.0, 10.0], unit='angstrom')
p_wav = sc.DataArray(
    data=sc.ones(sizes=wavelength.sizes),
    coords={'wavelength': wavelength},
)

source = tof.Source.from_distribution(
    neutrons=1_000_000,
    p_time=p_time,
    p_wav=p_wav,
)
source.plot()
[5]:
_images/sources_9_0.svg

Custom distributions#

Pulses at neutron facilities are rarely flat, and it is thus useful to be able to supply custom distributions as arrays of probabilities. The array values represent the probabilities, while the associated coordinates represent the values to be sampled from.

As an example, we create a triangular distribution for the neutron birth times, and a linearly increasing distribution for the neutron wavelengths (note that internally a linear interpolation is performed on the original data).

[6]:
v = np.arange(30.0)
p_time = sc.DataArray(
    data=sc.array(dims=['birth_time'], values=np.concatenate([v, v[::-1]])),
    coords={'birth_time': sc.linspace('birth_time', 0.1, 6.0, len(v) * 2, unit='ms')},
)
p_wav = sc.DataArray(
    data=sc.array(dims=['wavelength'], values=[1.0, 4.0]),
    coords={
        'wavelength': sc.array(dims=['wavelength'], values=[1.0, 4.0], unit='angstrom')
    },
)

source = tof.Source.from_distribution(neutrons=200_000, p_time=p_time, p_wav=p_wav)
source.plot()
[6]:
_images/sources_11_0.svg

Note that the time and wavelength distributions are independent; a neutron with a randomly selected birth time from p_time can adopt any wavelength in p_wav (in other words, the two distributions are simply broadcast into a square 2D parameter space).

Specifying neutrons manually#

Finally, it is possible to simply specify a list of birth times and wavelengths manually to create a pulse via the from_neutrons method.

[7]:
birth_times = sc.array(
    dims=['event'],
    values=[0.0, 0.1, 0.2, 0.56],
    unit='ms',
)
wavelengths = sc.array(dims=['event'], values=[5.0, 8.0, 11.0, 7.1], unit='angstrom')

source = tof.Source.from_neutrons(birth_times=birth_times, wavelengths=wavelengths)
source.plot()
[7]:
_images/sources_14_0.svg

Multiple pulses#

To make more than one pulse, use the pulses parameter:

[8]:
tof.Source(facility='ess', neutrons=100_000, pulses=3).plot()
[8]:
_images/sources_16_0.svg

If a custom distribution is supplied, a frequency for the pulse repetition rate must be supplied:

[9]:
tof.Source.from_distribution(
    neutrons=200_000, p_time=p_time, p_wav=p_wav, pulses=2, frequency=100.0 * Hz
).plot()
[9]:
_images/sources_18_0.svg

If a source was created from individual neutrons, the same neutrons will be repeated in all the pulses:

[10]:
tof.Source.from_neutrons(
    birth_times=birth_times, wavelengths=wavelengths, pulses=3, frequency=500.0 * Hz
).plot()
[10]:
_images/sources_20_0.svg