Skip to content
Snippets Groups Projects
lib2828.py 5.34 KiB
Newer Older
Lars Funke's avatar
Lars Funke committed
from euxfel_bunch_pattern import indices_at_sase  # Installable from PyPI
import numpy as np
import string
from scipy.interpolate import interp1d
from pathlib import Path

Lars Funke's avatar
Lars Funke committed

# List of detector infos from external txt file:
# name, digitizer, channel, angle, sample_rate
detectors = np.genfromtxt(Path(__file__).parent / 'detectors.txt',
Lars Funke's avatar
Lars Funke committed
                          names=True, dtype=('|U5', '|U4', '|U3', '<f8', '<i4'))
Lars Funke's avatar
Lars Funke committed

Lars Funke's avatar
Lars Funke committed
def correct_adq_common_mode(trace, region, sym):
Lars Funke's avatar
Lars Funke committed
    """Baseline subtraction based on common mode.

Lars Funke's avatar
Lars Funke committed
    Since ADQ digitizers always interleave multiple ADCs per channel to sample
Lars Funke's avatar
Lars Funke committed
    a trace, regular baseline subtraction will cause an ADC-dependent common
Lars Funke's avatar
Lars Funke committed
    mode to appear. This correction directly uses a per-ADC baseline instead
Lars Funke's avatar
Lars Funke committed
    to perform include this in the substraction.

Lars Funke's avatar
Lars Funke committed
    Empirical testing has shown that a symmetry of 8 (should be 4 for
    non-interleaved) regularly shows better results in suppressing high
    frequency signals from the common mode. Going 16 or higher is not recommend
Lars Funke's avatar
Lars Funke committed
    in most cases, as additional artifacts appear.

Lars Funke's avatar
Lars Funke committed
    Parameters
    ----------
Lars Funke's avatar
Lars Funke committed
    trace : array_like
        Vector to correct, will be modified in-place.
    region : slice
        Region to use for computing baseline.
    sym : int
        Periodic symmetry of ADC common mode.

Lars Funke's avatar
Lars Funke committed
    Returns
    -------
Lars Funke's avatar
Lars Funke committed
    trace : array_like
        Corrected vector, same shape as trace.
Lars Funke's avatar
Lars Funke committed
    """
Lars Funke's avatar
Lars Funke committed
    trace = trace.astype(np.float32)
Lars Funke's avatar
Lars Funke committed

Lars Funke's avatar
Lars Funke committed
    for x in range(sym):
Lars Funke's avatar
Lars Funke committed
        trace[..., x::sym] -= trace[..., region][x::sym].mean()
Lars Funke's avatar
Lars Funke committed

Lars Funke's avatar
Lars Funke committed
    return trace

Lars Funke's avatar
Lars Funke committed

Lars Funke's avatar
Lars Funke committed
def separate_pulses(traces, ppt, adq_train_region, adq_pulse_region, adq_sample_rate=4, sase=3):
Lars Funke's avatar
Lars Funke committed
    """Separate train into seperate pulses using the pulse pattern table.

Lars Funke's avatar
Lars Funke committed
    Parameters
    ----------
Lars Funke's avatar
Lars Funke committed
    traces : array_like
        Array of traces to be split up
    ppt : array_like
        Pulse pattern table from time server device.
    adq_train_region : slice, optional
        Region containing actual signal for the entire train.
    adq_pulse_region : slice, optional
        Region after pulse separation containing actual signal for the pulse.
    adq_pulse_region : slice, optional
        Region after pulse separation containing actual signal for the pulse.
    adq_sample_rate : array_like or int, optional
        Sample rate for all digitizer channels in GS/s.
    sase : int, optional
        Which SASE the experiments run at, 3 by default.

Lars Funke's avatar
Lars Funke committed
    Returns
    -------
Lars Funke's avatar
Lars Funke committed
    traces : array_like
        Separated vector of trains.
Lars Funke's avatar
Lars Funke committed
    """
    traces = traces[..., adq_train_region]
Lars Funke's avatar
Lars Funke committed
    pulse_ids = indices_at_sase(ppt, sase=sase)
    num_pulses = len(pulse_ids)
Lars Funke's avatar
Lars Funke committed
    if num_pulses < 2:
        return traces[..., adq_pulse_region].reshape(traces.shape[0], 1, -1)
Lars Funke's avatar
Lars Funke committed
    else:
Lars Funke's avatar
Lars Funke committed
        # ADQ412s run at a clock factor of 440 relative to the PPT un-interleaved.
Lars Funke's avatar
Lars Funke committed
        pulse_spacing = (pulse_ids[1] - pulse_ids[0]) * 220 * adq_sample_rate
Lars Funke's avatar
Lars Funke committed
        traces_aligned = traces[..., :(num_pulses * pulse_spacing)]
        assert num_pulses * pulse_spacing == traces_aligned.shape[-1] # digitizer traces are too short for pulse pattern?
Lars Funke's avatar
Lars Funke committed
        return traces_aligned.reshape(*(traces_aligned.shape[:-1]), num_pulses, pulse_spacing)[..., adq_pulse_region]
Lars Funke's avatar
Lars Funke committed
def _f(t, a, b, c, d):
Lars Funke's avatar
Lars Funke committed
    return a / t**3 + b / t**2 + c / t + d
Lars Funke's avatar
Lars Funke committed

def _df(t, a, b, c, d):
Lars Funke's avatar
Lars Funke committed
    return -3 * a / t**4 - 2 * b / t**3 - c / t**2
Lars Funke's avatar
Lars Funke committed
def energy_calibration(traces, calib_params, sample_rate, valid_energies,
Lars Funke's avatar
Lars Funke committed
                       energy_nodes=None, model=_f, model_derivative=_df):
Lars Funke's avatar
Lars Funke committed
    """Calibrate time of flight traces to energy spectra.

Lars Funke's avatar
Lars Funke committed
    Parameters
    ----------
Lars Funke's avatar
Lars Funke committed
    traces : array_like
        Traces to be calibrated, shape (detectors, ..., samples).
    calib_params : array_like
        Calibration parameters passed to the model, first dimension needs
        to be number of function arguments.
    sample_rate : array_like or int, optional
        Sample rate for all digitizer channels in GS/s.
    valid_energies : slice
        Slice applied to input allowing to discard divergent values.
    energy_nodes : array_like, optional
        Energy values to be evaluated in interpolation, if you want.
    model : function, optional
        Mapping from time to energy, arguments (t, *args). Defaults to
        a / t**3 + b / t**2 + c / t + d.
    model_derivative : function, optional
        Derivative of `model` function.

Lars Funke's avatar
Lars Funke committed
    Returns
    -------
Lars Funke's avatar
Lars Funke committed
    energy : ndarray
        Energy values corresponding to time of flight.
    spectra : ndarray
        Reweighted spectra.
    resampled_spectra : ndarray
        Spectra resampled according to `energy_nodes`.
    time : ndarray
        Times of flight corresponding to samples.
Lars Funke's avatar
Lars Funke committed
    """
Lars Funke's avatar
Lars Funke committed
    # Convert digitizer channels into ToF [ns]
    time = np.arange(traces.shape[-1])[:, np.newaxis] / sample_rate * 1e-9
    # Calibrate ToF to energy
    energy = model(time, *calib_params)[valid_energies].T
    dE = model_derivative(time[:, None], *calib_params)[valid_energies].T
    spectra = traces[..., valid_energies] / dE
Lars Funke's avatar
Lars Funke committed

    if energy_nodes is not None:
        try:
Lars Funke's avatar
Lars Funke committed
            resampled_spectra = np.asarray([interp1d(e, t)(energy_nodes) for e, t in
                                            zip(energy, spectra)])
        except ValueError as e:
            print(e)
            resampled_spectra = np.full((*traces.shape[:-1], len(energy_nodes)), np.nan)
    else:
        resampled_spectra = None
Lars Funke's avatar
Lars Funke committed
    return energy, spectra, resampled_spectra, time[valid_energies]