def theta(gravity: sc.Variable, wavelength: sc.Variable, incident_beam: sc.Variable, scattered_beam: sc.Variable, sample_rotation: sc.Variable) -> sc.Variable: """ Compute the theta angle, including gravity correction, This is similar to the theta calculation in SANS (see https://docs.mantidproject.org/nightly/algorithms/Q1D-v2.html#q-unit-conversion), but we ignore the horizontal `x` component. See the schematic in Fig 5 of doi: 10.1016/j.nima.2016.03.007. """ grav = sc.norm(gravity) L2 = sc.norm(scattered_beam) y = sc.dot(scattered_beam, gravity) / grav y_correction = sc.to_unit(wavelength, _elem_unit(L2), copy=True) y_correction *= y_correction drop = L2**2 drop *= grav * (m_n**2 / (2 * h**2)) # Optimization when handling either the dense or the event coord of binned data: # - For the event coord, both operands have same dims, and we can multiply in place # - For the dense coord, we need to broadcast using non in-place operation if set(drop.dims).issubset(set(y_correction.dims)): y_correction *= drop else: y_correction = y_correction * drop y_correction += y out = sc.abs(y_correction, out=y_correction) out /= L2 out = sc.asin(out, out=out) out -= sc.to_unit(sample_rotation, 'rad') return out
def _furthest_component(det_center, scipp_obj, additional): distances = [ sc.norm(settings["center"] - det_center).value for settings in list(additional.values()) ] max_displacement = sorted(distances)[-1] return max_displacement
def test_norm(): var = sp.Variable(dims=[Dim.X], values=[[1, 0, 0], [3, 4, 0]], unit=sp.units.m) expected = sp.Variable([Dim.X], values=np.array([1.0, 5.0]), unit=sp.units.m) assert sp.norm(var) == expected
def test_convert_beam_length_no_scatter(): original = make_test_data(coords=('position', 'source_position')) converted = scn.convert(original, origin='position', target='Ltotal', scatter=False) expected = sc.norm(make_position() - make_source_position()) assert sc.identical(converted.coords['Ltotal'], expected)
def test_norm(): var = sc.Variable(dims=[Dim.X], values=[[1, 0, 0], [3, 4, 0]], unit=sc.units.m, dtype=sc.dtype.vector_3_float64) expected = sc.Variable([Dim.X], values=np.array([1.0, 5.0]), unit=sc.units.m) assert sc.norm(var) == expected
def _frames_from_slopes(data): detector_pos_norm = sc.norm(data.meta["position"]) # Get the number of WFM frames choppers = { key: data.meta[key].value for key in ch.find_chopper_keys(data) } nframes = ch.cutout_angles_begin(choppers["chopper_wfm_1"]).sizes["frame"] # Now find frame boundaries frames = sc.Dataset() frames["time_min"] = sc.zeros(dims=["frame"], shape=[nframes], unit=sc.units.us) frames["time_max"] = sc.zeros_like(frames["time_min"]) frames["delta_time_min"] = sc.zeros_like(frames["time_min"]) frames["delta_time_max"] = sc.zeros_like(frames["time_min"]) frames["wavelength_min"] = sc.zeros(dims=["frame"], shape=[nframes], unit=sc.units.angstrom) frames["wavelength_max"] = sc.zeros_like(frames["wavelength_min"]) frames["delta_wavelength_min"] = sc.zeros_like(frames["wavelength_min"]) frames["delta_wavelength_max"] = sc.zeros_like(frames["wavelength_min"]) frames["time_correction"] = sc.zeros(dims=["frame"], shape=[nframes], unit=sc.units.us) near_wfm_chopper = choppers["chopper_wfm_1"] far_wfm_chopper = choppers["chopper_wfm_2"] # Distance between WFM choppers dz_wfm = sc.norm(far_wfm_chopper["position"].data - near_wfm_chopper["position"].data) # Mid-point between WFM choppers z_wfm = 0.5 * (near_wfm_chopper["position"].data + far_wfm_chopper["position"].data) # Distance between detector positions and wfm chopper mid-point zdet_minus_zwfm = sc.norm(data.meta["position"] - z_wfm) # Neutron mass to Planck constant ratio alpha = sc.to_unit(constants.m_n / constants.h, 'us/m/angstrom') near_t_open = ch.time_open(near_wfm_chopper) near_t_close = ch.time_closed(near_wfm_chopper) far_t_open = ch.time_open(far_wfm_chopper) for i in range(nframes): dt_lambda_max = near_t_close["frame", i] - near_t_open["frame", i] slope_lambda_max = dz_wfm / dt_lambda_max intercept_lambda_max = sc.norm( near_wfm_chopper["position"].data ) - slope_lambda_max * near_t_close["frame", i] t_lambda_max = (detector_pos_norm - intercept_lambda_max) / slope_lambda_max slope_lambda_min = sc.norm(near_wfm_chopper["position"].data) / ( near_t_close["frame", i] - (data.meta["source_pulse_length"] + data.meta["source_pulse_t_0"])) intercept_lambda_min = sc.norm( far_wfm_chopper["position"].data ) - slope_lambda_min * far_t_open["frame", i] t_lambda_min = (detector_pos_norm - intercept_lambda_min) / slope_lambda_min t_lambda_min_plus_dt = ( detector_pos_norm - (sc.norm(near_wfm_chopper["position"].data) - slope_lambda_min * near_t_close["frame", i])) / slope_lambda_min dt_lambda_min = t_lambda_min_plus_dt - t_lambda_min # Compute wavelength information lambda_min = (t_lambda_min + 0.5 * dt_lambda_min - far_t_open["frame", i]) / (alpha * zdet_minus_zwfm) lambda_max = (t_lambda_max - 0.5 * dt_lambda_max - far_t_open["frame", i]) / (alpha * zdet_minus_zwfm) dlambda_min = dz_wfm * lambda_min / zdet_minus_zwfm dlambda_max = dz_wfm * lambda_max / zdet_minus_zwfm frames["time_min"]["frame", i] = t_lambda_min frames["delta_time_min"]["frame", i] = dt_lambda_min frames["time_max"]["frame", i] = t_lambda_max frames["delta_time_max"]["frame", i] = dt_lambda_max frames["wavelength_min"]["frame", i] = lambda_min frames["wavelength_max"]["frame", i] = lambda_max frames["delta_wavelength_min"]["frame", i] = dlambda_min frames["delta_wavelength_max"]["frame", i] = dlambda_max frames["time_correction"]["frame", i] = far_t_open["frame", i] frames["wfm_chopper_mid_point"] = z_wfm return frames
def time_distance_diagram(data: sc.DataArray, **kwargs) -> plt.Figure: """ Plot the time-distance diagram for a WFM beamline. The expected input is a Dataset or DataArray containing the chopper cascade information as well as the description of the source pulse. This internally calls the `get_frames` method which is used to compute the frame properties for stitching. """ # Get the frame properties frames = get_frames(data, **kwargs) # Find detector pixel furthest away from source source_pos = data.meta["source_position"] furthest_detector_pos = sc.max(sc.norm(data.meta["position"] - source_pos)).value pulse_rectangle_height = furthest_detector_pos / 50.0 tmax_glob = sc.max(frames["time_max"].data).value # Create figure and axes fig, ax = plt.subplots(1, 1, figsize=(9, 7)) ax.grid(True, color='lightgray', linestyle="dotted") ax.set_axisbelow(True) # Draw a light grey rectangle from the origin to t_0 + pulse_length + t_0 # The second t_0 should in fact be the end of the pulse tail, but since this # information is not needed for computing the frame properties, it may # not be present in the description of the beamline. # So we fake this by simply using t_0 again at the end of the pulse. ax.add_patch( Rectangle((0, 0), (2.0 * data.meta["source_pulse_t_0"] + data.meta["source_pulse_length"]).value, -pulse_rectangle_height, lw=1, fc='lightgrey', ec='k', zorder=10)) # Draw a dark grey rectangle from t_0 to t_0 + pulse_length to represent the usable # pulse. ax.add_patch( Rectangle((data.meta["source_pulse_t_0"].value, 0), data.meta["source_pulse_length"].value, -pulse_rectangle_height, lw=1, fc='grey', ec='k', zorder=11)) # Indicate source pulse and add the duration. ax.text(data.meta["source_pulse_t_0"].value, -pulse_rectangle_height, "Source pulse ({} {})".format( data.meta["source_pulse_length"].value, data.meta["source_pulse_length"].unit), ha="left", va="top", fontsize=6) # Plot the chopper openings as segments # for name, chopper in data.meta["choppers"].value.items(): for name in ch.find_chopper_keys(data): chopper = data.meta[name].value yframe = sc.norm(chopper["position"].data - source_pos).value time_open = ch.time_open(chopper).values time_close = ch.time_closed(chopper).values tmin = 0.0 for fnum in range(len(time_open)): tmax = time_open[fnum] ax.plot([tmin, tmax], [yframe] * 2, color='k') tmin = time_close[fnum] ax.plot([tmin, tmax_glob], [yframe] * 2, color='k') ax.text(2.0 * time_close[-1] - time_open[-1], yframe, name, ha="left", va="bottom") # Plot the shades of possible neutron paths for i in range(frames.sizes["frame"]): col = "C{}".format(i) frame = frames["frame", i] for dim in data.meta["position"].dims: frame = frame[dim, 0] # Minimum wavelength lambda_min = np.array( [[(data.meta["source_pulse_t_0"] + data.meta["source_pulse_length"] - frame['delta_time_min']).value, 0], [(data.meta["source_pulse_t_0"] + data.meta["source_pulse_length"]).value, 0], [(frame["time_min"] + frame["delta_time_min"]).value, furthest_detector_pos], [frame["time_min"].value, furthest_detector_pos]]) # Maximum wavelength lambda_max = np.array( [[data.meta["source_pulse_t_0"].value, 0], [(data.meta["source_pulse_t_0"] + frame['delta_time_max']).value, 0], [frame["time_max"].value, furthest_detector_pos], [(frame["time_max"] - frame["delta_time_max"]).value, furthest_detector_pos]]) ax.plot(np.concatenate((lambda_min[:, 0], lambda_min[0:1, 0])), np.concatenate((lambda_min[:, 1], lambda_min[0:1, 1])), color=col, lw=1) ax.plot(np.concatenate((lambda_max[:, 0], lambda_max[0:1, 0])), np.concatenate((lambda_max[:, 1], lambda_max[0:1, 1])), color=col, lw=1) ax.fill([ lambda_max[0, 0], lambda_max[-1, 0], lambda_min[2, 0], lambda_min[1, 0] ], [ lambda_max[0, 1], lambda_max[-1, 1], lambda_min[2, 1], lambda_min[1, 1] ], alpha=0.3, color=col, zorder=-5) ax.fill(lambda_min[:, 0], lambda_min[:, 1], color='w', zorder=-4) ax.fill(lambda_max[:, 0], lambda_max[:, 1], color='w', zorder=-4) ax.text(0.5 * (frame["time_min"] + frame["delta_time_min"] + frame["time_max"] - frame["delta_time_max"]).value, furthest_detector_pos, "Frame {}".format(i + 1), ha="center", va="top") # Add thick solid line for the detector position, spanning the entire width ax.plot([0, tmax_glob], [furthest_detector_pos] * 2, lw=3, color='grey') ax.text(0.0, furthest_detector_pos, "Detector", va="bottom", ha="left") # Set axis labels ax.set_xlabel("Time [microseconds]") ax.set_ylabel("Distance [m]") return fig
def frames_analytical(data: Union[sc.DataArray, sc.Dataset]) -> sc.Dataset: """ Compute analytical frame boundaries and shifts based on chopper parameters and detector pixel positions. A set of frame boundaries is returned for each pixel. The frame shifts are the same for all pixels. See Schmakat et al. (2020); https://www.sciencedirect.com/science/article/pii/S0168900220308640 for a description of the procedure. TODO: This currently ignores scattering paths, only the distance from source to pixel. For imaging, this is what we want, but for scattering techniques, we should use l1 + l2 in the future. """ # Identify the WFM choppers based on their `kind` property wfm_choppers = {} for name in ch.find_chopper_keys(data): chopper = data.meta[name].value if chopper["kind"].value == "wfm": wfm_choppers[name] = chopper if len(wfm_choppers) != 2: raise RuntimeError("The number of WFM choppers is expected to be 2, " "found {}".format(len(wfm_choppers))) # Find the near and far WFM choppers based on their positions relative to the source wfm_chopper_names = list(wfm_choppers.keys()) if (sc.norm(wfm_choppers[wfm_chopper_names[0]]["position"].data - data.meta["source_position"]) < sc.norm(wfm_choppers[wfm_chopper_names[1]]["position"].data - data.meta["source_position"])).value: near_index = 0 far_index = 1 else: near_index = 1 far_index = 0 near_wfm_chopper = wfm_choppers[wfm_chopper_names[near_index]] far_wfm_chopper = wfm_choppers[wfm_chopper_names[far_index]] # Compute distances for each detector pixel detector_positions = data.meta["position"] - data.meta["source_position"] # Container for frames information frames = sc.Dataset() # Distance between WFM choppers dz_wfm = sc.norm(far_wfm_chopper["position"].data - near_wfm_chopper["position"].data) # Mid-point between WFM choppers z_wfm = 0.5 * ( near_wfm_chopper["position"].data + far_wfm_chopper["position"].data) - data.meta["source_position"] # Ratio of WFM chopper distances z_ratio_wfm = (sc.norm(far_wfm_chopper["position"].data - data.meta["source_position"]) / sc.norm(near_wfm_chopper["position"].data - data.meta["source_position"])) # Distance between detector positions and wfm chopper mid-point zdet_minus_zwfm = sc.norm(detector_positions - z_wfm) # Neutron mass to Planck constant ratio alpha = sc.to_unit(constants.m_n / constants.h, 'us/m/angstrom') # Frame time corrections: these are the mid-time point between the WFM choppers, # which is the same as the opening edge of the second WFM chopper in the case # of optically blind choppers. frames["time_correction"] = ch.time_open(far_wfm_chopper) # Find delta_t for the min and max wavelengths: # dt_lambda_max is equal to the time width of the WFM choppers windows dt_lambda_max = ch.time_closed(near_wfm_chopper) - ch.time_open( near_wfm_chopper) # t_lambda_max is found from the relation between t and delta_t: equation (2) in # Schmakat et al. (2020). t_lambda_max = (dt_lambda_max / dz_wfm) * zdet_minus_zwfm # t_lambda_min is found from the relation between lambda_N and lambda_N+1, # equation (3) in Schmakat et al. (2020). t_lambda_min = t_lambda_max * z_ratio_wfm - data.meta[ "source_pulse_length"] * (zdet_minus_zwfm / sc.norm( near_wfm_chopper["position"].data - data.meta["source_position"])) # dt_lambda_min is found from the relation between t and delta_t: equation (2) # in Schmakat et al. (2020), and using the expression for t_lambda_max. dt_lambda_min = dt_lambda_max * z_ratio_wfm - data.meta[ "source_pulse_length"] * dz_wfm / sc.norm( near_wfm_chopper["position"].data - data.meta["source_position"]) # Compute wavelength information lambda_min = t_lambda_min / (alpha * zdet_minus_zwfm) lambda_max = t_lambda_max / (alpha * zdet_minus_zwfm) dlambda_min = dz_wfm * lambda_min / zdet_minus_zwfm dlambda_max = dz_wfm * lambda_max / zdet_minus_zwfm # Frame edges and resolutions for each pixel. # The frames do not stop at t_lambda_min and t_lambda_max, they also include the # fuzzy areas (delta_t) at the edges. frames["time_min"] = t_lambda_min - ( 0.5 * dt_lambda_min) + frames["time_correction"] frames["delta_time_min"] = dt_lambda_min frames["time_max"] = t_lambda_max + ( 0.5 * dt_lambda_max) + frames["time_correction"] frames["delta_time_max"] = dt_lambda_max frames["wavelength_min"] = lambda_min frames["wavelength_max"] = lambda_max frames["delta_wavelength_min"] = dlambda_min frames["delta_wavelength_max"] = dlambda_max frames["wfm_chopper_mid_point"] = 0.5 * ( near_wfm_chopper["position"].data + far_wfm_chopper["position"].data) return frames
def make_fake_beamline( chopper_wfm_1_position=sc.vector(value=[0.0, 0.0, 6.775], unit='m'), chopper_wfm_2_position=sc.vector(value=[0.0, 0.0, 7.225], unit='m'), frequency=sc.scalar(56.0, unit=sc.units.one / sc.units.s), lambda_min=sc.scalar(1.0, unit='angstrom'), pulse_length=sc.scalar(2.86e-03, unit='s'), pulse_t_0=sc.scalar(1.3e-4, unit='s'), nframes=2): """ Fake chopper cascade with 2 optically blind WFM choppers. Based on mathematical description in Schmakat et al. (2020); https://www.sciencedirect.com/science/article/pii/S0168900220308640 """ dim = 'frame' # Neutron mass to Planck constant ratio alpha = sc.to_unit(m_n / h, 's/m/angstrom') omega = (2.0 * np.pi * sc.units.rad) * frequency cutout_angles_center_wfm_1 = sc.empty(dims=[dim], shape=[nframes], unit='rad') cutout_angles_center_wfm_2 = sc.empty_like(cutout_angles_center_wfm_1) cutout_angles_width = sc.empty_like(cutout_angles_center_wfm_1) for i in range(nframes): # Equation (3) in Schmakat et al. (2020) lambda_max = (pulse_length + alpha * lambda_min * sc.norm(chopper_wfm_1_position)) / ( alpha * sc.norm(chopper_wfm_2_position)) # Equation (4) in Schmakat et al. (2020) theta = omega * (pulse_length + alpha * (lambda_min - lambda_max) * sc.norm(chopper_wfm_1_position)) # Equation (5) in Schmakat et al. (2020) phi_wfm_1 = omega * ( pulse_t_0 + 0.5 * pulse_length + 0.5 * alpha * (lambda_min + lambda_max) * sc.norm(chopper_wfm_1_position)) # Equation (6) in Schmakat et al. (2020) phi_wfm_2 = omega * (pulse_t_0 + 1.5 * pulse_length + 0.5 * alpha * ( (3.0 * lambda_min) - lambda_max) * sc.norm(chopper_wfm_1_position)) cutout_angles_width[dim, i] = theta cutout_angles_center_wfm_1[dim, i] = phi_wfm_1 cutout_angles_center_wfm_2[dim, i] = phi_wfm_2 lambda_min = lambda_max return { "chopper_wfm_1": sc.scalar( make_chopper(frequency=frequency, phase=sc.scalar(0.0, unit='deg'), position=chopper_wfm_1_position, cutout_angles_center=cutout_angles_center_wfm_1, cutout_angles_width=cutout_angles_width, kind=sc.scalar('wfm'))), "chopper_wfm_2": sc.scalar( make_chopper(frequency=frequency, phase=sc.scalar(0.0, unit='deg'), position=chopper_wfm_2_position, cutout_angles_center=cutout_angles_center_wfm_2, cutout_angles_width=cutout_angles_width, kind=sc.scalar('wfm'))), 'position': sc.vector(value=[0., 0., 60.], unit='m'), "source_pulse_length": sc.to_unit(pulse_length, 'us'), "source_pulse_t_0": sc.to_unit(pulse_t_0, 'us'), "source_position": sc.vector(value=[0.0, 0.0, 0.0], unit='m') }
def _do_stitching_on_beamline(wavelengths, dim, event_mode=False): # Make beamline parameters for 6 frames coords = wfm.make_fake_beamline(nframes=6) # They are all created half-way through the pulse. # Compute their arrival time at the detector. alpha = sc.to_unit(constants.m_n / constants.h, 's/m/angstrom') dz = sc.norm(coords['position'] - coords['source_position']) arrival_times = sc.to_unit(alpha * dz * wavelengths, 'us') + coords['source_pulse_t_0'] + ( 0.5 * coords['source_pulse_length']) coords[dim] = arrival_times # Make a data array that contains the beamline and the time coordinate tmin = sc.min(arrival_times) tmax = sc.max(arrival_times) dt = 0.1 * (tmax - tmin) if event_mode: num = 2 else: num = 2001 time_binning = sc.linspace(dim=dim, start=(tmin - dt).value, stop=(tmax + dt).value, num=num, unit=dt.unit) events = sc.DataArray(data=sc.ones(dims=['event'], shape=arrival_times.shape, unit=sc.units.counts, with_variances=True), coords=coords) if event_mode: da = sc.bin(events, edges=[time_binning]) else: da = sc.histogram(events, bins=time_binning) # Find location of frames frames = wfm.get_frames(da) stitched = wfm.stitch(frames=frames, data=da, dim=dim, bins=2001) wav = scn.convert(stitched, origin='tof', target='wavelength', scatter=False) if event_mode: out = wav else: out = sc.rebin(wav, dim='wavelength', bins=sc.linspace(dim='wavelength', start=1.0, stop=10.0, num=1001, unit='angstrom')) choppers = {key: da.meta[key].value for key in ch.find_chopper_keys(da)} # Distance between WFM choppers dz_wfm = sc.norm(choppers["chopper_wfm_2"]["position"].data - choppers["chopper_wfm_1"]["position"].data) # Delta_lambda / lambda dlambda_over_lambda = dz_wfm / sc.norm( coords['position'] - frames['wfm_chopper_mid_point'].data) return out, dlambda_over_lambda
def make_L2(): return sc.norm(make_scattered_beam())
def make_L1(): return sc.norm(make_incident_beam())