# SPDX-License-Identifier: BSD-3-Clause
# Copyright (c) 2023 Scipp contributors (https://github.com/scipp)
# @author Simon Heybrock
import scipp as sc
from matplotlib.patches import Rectangle
from scipp.constants import h, m_n
from .._utils import as_float_type, elem_unit
def _tof_from_wavelength(
*, wavelength: sc.Variable, Ltotal: sc.Variable
) -> sc.Variable:
scale = (m_n / h).to(unit=sc.units.us / elem_unit(Ltotal) / elem_unit(wavelength))
return as_float_type(Ltotal * scale, wavelength) * wavelength
[docs]
class TimeDistanceDiagram:
[docs]
def __init__(self, ax, *, tmax, frame_rate=None):
self._ax = ax
self._time_unit = sc.Unit('ms')
self._distance_unit = sc.Unit('m')
frame_rate = 14.0 * sc.Unit('Hz') if frame_rate is None else frame_rate
self._frame_length = (1.0 / frame_rate).to(unit=self._time_unit)
self._tmax = tmax.to(unit=self._time_unit)
self._ax.set_xlabel(f"time [{self._time_unit}]")
self._ax.set_ylabel("distance [m]")
@property
def frame_length(self):
return self._frame_length
def to_time(self, time):
return time.to(unit=self._time_unit)
def to_distance(self, distance):
return distance.to(unit=self._distance_unit)
def annotate(self, text, *, xy, xytext, **kwargs):
def to_mpl(point):
x, y = point
return self.to_time(x).value, self.to_distance(y).value
self._ax.annotate(text, xy=to_mpl(xy), xytext=to_mpl(xytext), **kwargs)
def add_source_pulse(self, pulse_length=None, ls='dotted'):
pulse_length = 3.0 * sc.Unit('ms') if pulse_length is None else pulse_length
t0 = 0.0
t1 = self.to_time(pulse_length).value
self._ax.text(
t0,
-2,
f"Source pulse ({pulse_length.value} {pulse_length.unit})",
ha="left",
va="top",
fontsize=6,
)
while t0 < self._tmax.value:
rect = Rectangle((t0, 0), t1, -1, lw=1, fc='orange', ec='k')
self._ax.add_patch(rect)
self._ax.axvline(x=t0, ls=ls)
t0 += self._frame_length.value
def add_neutron(
self,
*,
time_offset: sc.Variable,
wavelength: sc.Variable,
L: sc.Variable,
label=None,
color='black',
ls='solid',
lw=0.7,
):
tof = self.to_time(_tof_from_wavelength(wavelength=wavelength, Ltotal=L))
t0 = self.to_time(time_offset).value
self._ax.plot(
[t0, t0 + tof.value], [0, L.value], marker='', color=color, ls=ls, lw=lw
)
if label is not None:
self._ax.text(tof.value, L.value, label, ha="center", va="bottom")
[docs]
def add_neutrons(
self,
*,
lambda_min: sc.Variable,
lambda_max: sc.Variable = None,
Lmin: sc.Variable = 0.0 * sc.units.m,
Lmax: sc.Variable,
time_offset: sc.Variable,
stride=1,
frames=2,
):
"""
Draw a wavelength band to depict propagation of neutrons. Neutrons are assumed
to be emitted from a single point, i.e., no resolution effects are taken into
account.
Parameters
----------
Lmin:
Distance where neutrons are "emitted", such as the source pulse or
a chopper. The default is at 0.0, i.e., the source position.
Lmax:
Distance where propagation of neutrons stops. This is typically set
to the last detector (or after), but could be set to a chopper distance if
a chopper extracts a smaller wavelength band.
lambda_min:
Minimum wavelength, defining fastest neutrons.
lambda_max:
Maximum wavelength, defining slowest neutrons. If lambda_max
is None (the default) it is set such that there is no frame overlap at Lmax.
time_offset:
Offset time at which neutrons are emitted.
frames:
The number of frames that should be drawn.
"""
tof_min = self.to_time(
_tof_from_wavelength(wavelength=lambda_min, Ltotal=Lmax - Lmin)
)
if lambda_max is None:
tof_max = tof_min + (stride - 1 + 0.95) * self._frame_length # small 5% gap
else:
tof_max = self.to_time(
_tof_from_wavelength(wavelength=lambda_max, Ltotal=Lmax - Lmin)
)
time_offset = self.to_time(time_offset)
t0 = time_offset
tmin = t0 + tof_min
tmax = t0 + tof_max
t = sc.concat([t0, t0, tmax, tmin], 't')
L = sc.concat([Lmin, Lmin, Lmax, Lmax], 'L')
for _ in range(frames):
self._ax.fill(t.values, L.values, alpha=0.3)
t += stride * self._frame_length
def add_detector(self, *, distance, name='detector'):
# TODO This could accept a list of positions and plot a rectangle from min to
# max detector distance
self._ax.plot(
[0, self._tmax.max().value],
[distance.value, distance.value],
lw=3,
color='grey',
)
self._ax.text(0.0, distance.value, name, va="bottom", ha="left")
def add_sample(self, *, distance):
self._ax.plot(
[0, self._tmax.max().value],
[distance.value, distance.value],
lw=3,
color='green',
)
self._ax.text(0.0, distance.value, 'sample', va="bottom", ha="left")