Source code for camera.trace_analysis

"""Trace analysis."""

import numpy as np
from scipy import signal

PEDESTAL_WINDOW_START = -10
INTEGRATION_WINDOW_START = 4
DEFAULT_SUM_THRESHOLD = 10.0


[docs] def compute_integration_window(peak_idx, n_samp, half_width, mode, offset=None): """ Compute integration window bounds for signal or pedestal modes. Parameters ---------- peak_idx : int Index of the peak in the waveform. n_samp : int Total number of samples in the waveform. half_width : int Half-width of the integration window. mode : str Either "signal" or "pedestal". offset : int, optional Offset for pedestal window from peak. Default is 16. Returns ------- tuple of int Window bounds (start, end) for integration [start:end). """ hw = int(half_width) win_len = 2 * hw + 1 if mode == "signal": a = max(0, peak_idx - hw) b = min(n_samp, peak_idx + hw + 1) return a, b g = int(offset) if offset is not None else 16 start = peak_idx + g if start + win_len <= n_samp: return start, start + win_len start = max(0, peak_idx - g - win_len) a = start b = min(n_samp, start + win_len) if a >= b: return 0, min(n_samp, win_len) return a, b
[docs] def calculate_pedestals(trace, start=None, end=None, half_width=2, offset=16): """ Calculate pedestal per pixel. If start/end are provided, uses fixed window. Otherwise, computes pedestal window dynamically based on detected peak position. Parameters ---------- trace : array-like Trace, shape (n_pixels, n_samples) or (1, n_pixels, n_samples). start : int, optional Start index for fixed window. If None, uses dynamic windowing. end : int, optional End index for fixed window. half_width : int, optional Half-width for dynamic pedestal window. Default is 2. offset : int, optional Offset from peak for dynamic pedestal window. Default is 16. Returns ------- np.ndarray Mean pedestal value per pixel. """ w = np.atleast_2d(trace) if w.ndim == 3: w = w[0] if start is not None and end is not None: return np.mean(w[:, start:end], axis=-1) # Dynamic windowing based on peak detection n_pix, n_samp = w.shape pedestals = np.zeros(n_pix) for i in range(n_pix): peak_idx = int(np.argmax(w[i])) a, b = compute_integration_window(peak_idx, n_samp, half_width, "pedestal", offset) pedestals[i] = np.mean(w[i, a:b]) return pedestals
[docs] def trace_integration(trace, pedestals, window): """ Integrate traces over specific window. Parameters ---------- trace : array-like Trace, shape (n_pixels, n_samples) or (1, n_pixels, n_samples). pedestals : array-like or None Pedestal values per pixel. If None, pedestals are calculated. window : tuple of int Integration window (start, end) indices. Returns ------- np.ndarray Integrated trace values per pixel. """ if pedestals is None: pedestals = calculate_pedestals(trace) w = np.squeeze(trace) if w.ndim == 1: w = w[np.newaxis, :] calibrated = w - np.atleast_1d(pedestals)[:, None] return np.sum(calibrated[:, int(window[0]) : int(window[1])], axis=1)
[docs] def get_time_axis(time_slice, n_samples): """ Generate a time axis from the sampling period. Parameters ---------- time_slice : astropy.units.Quantity Length of one sampling time slice (e.g., ns). n_samples : int Number of samples. Returns ------- np.ndarray Time axis array. """ dt = time_slice.to("ns").value if dt <= 0: dt = 1.0 return np.linspace(0, (n_samples - 1) * dt, n_samples)
[docs] def get_adc_samples_per_gain(adc_samples, low_gain=False): """ Get ADC samples for low gain channel if specified. Parameters ---------- adc_samples : array-like ADC samples array, shape (n_gains, n_pixels, n_samples) or (1, n_pixels, n_samples). where n_gains is typically 2 for dual-gain readout. low_gain : bool If True, return low gain channel samples. Returns ------- np.ndarray ADC samples for the specified gain channel (all pixels). """ if adc_samples.ndim == 3: return np.asarray(adc_samples[1 if low_gain else 0]) return np.asarray(adc_samples)
[docs] def get_trace_data(adc_samples): """Prepare gain-selected samples, pedestals, and integrated signal values. Parameters ---------- adc_samples : array-like ADC samples with shape ``(n_gains, n_pixels, n_samples)`` or ``(n_pixels, n_samples)``. Returns ------- tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray] Gain-selected ADC samples, pedestal values per pixel, and integrated pedestal-subtracted signal values per pixel. """ samples = get_adc_samples_per_gain(adc_samples) n_samples = samples.shape[-1] pedestals = calculate_pedestals( samples, start=n_samples + PEDESTAL_WINDOW_START, end=n_samples, ) integrated_signal = trace_integration( samples, pedestals=pedestals, window=(INTEGRATION_WINDOW_START, n_samples), ) return samples, pedestals, integrated_signal
[docs] def trace_maxima(trace, sum_threshold): """ Compute trace maxima sample per pixel from waveform matrix. Parameters ---------- trace : numpy.ndarray Trace array of shape ``(n_pix, n_samples)`` (or ``(1, n_pix, n_samples)``). sum_threshold : float Minimum sum over samples for a pixel to be considered. Returns ------- tuple[numpy.ndarray | None, numpy.ndarray | None, int] ``(peak_samples, pix_ids, found_count)`` where ``peak_samples`` are the selected peak indices per considered pixel, ``pix_ids`` are the pixel indices that passed ``sum_threshold``, and ``found_count`` is the number of pixels with at least one detected peak. Returns ``(None, None, 0)`` if no pixels passed the threshold. """ sums = trace.sum(axis=1) pix_ids = np.flatnonzero(sums > float(sum_threshold)) if pix_ids.size == 0: return None, None, 0 peak_samples = np.empty(pix_ids.size, dtype=int) found_count = 0 for i, pid in enumerate(pix_ids): pixel_trace = trace[pid] pks = trace_maximum(pixel_trace) if pks.size: found_count += 1 peak_samples[i] = pks[np.argmax(pixel_trace[pks])] else: peak_samples[i] = np.argmax(pixel_trace) return peak_samples, pix_ids, found_count
[docs] def trace_maximum(trace): """ Detect trace maximum. Parameters ---------- trace : numpy.ndarray One-dimensional trace for a single pixel. Returns ------- numpy.ndarray Array of integer indices of detected peaks (possibly empty). """ try: peaks, _ = signal.find_peaks(trace, prominence=0.1 * np.max(trace)) return peaks.astype(int) except ValueError, TypeError: return np.empty(0, dtype=int)