# Transient detection ## Why transient detection? The indicator brightness rises and falls with whichever underlying biological signal the indicator is engineered to report (intracellular calcium for GCaMP, extracellular dopamine for sensors like dLight, and so on for serotonin, acetylcholine, voltage, and the rest of the indicator catalog). A brief, large upward deflection in the trace therefore corresponds to a brief, large excursion in that local signal: a bout of population activity, a transmitter release event, or a shift in release-uptake balance, depending on which indicator is in use. Transient detection (also called peak detection) is the analysis that extracts the times of those deflections. The resulting event list is useful for relating activity to other observables (behavioral timestamps, drug administration, sensory stimuli, signals recorded simultaneously from other regions), for aggregating into a session-level event rate or mean amplitude that compares across conditions, and for cross-correlating event streams between regions to extract timing relationships independent of slow shared drift. ## Detecting transients The basic detector is conceptually simple. To flag samples that are anomalous relative to the rest of the trace, we characterize that trace by two summary numbers: a typical level and a noise scale (how far a typical sample sits from the typical level). Each peak more than K noise scales above the typical level is counted as a transient, where K is a sensitivity multiplier the user chooses (larger K means fewer detections of any kind, real or noise; values around 3 are typical in photometry). The output is a discrete list of *(time, amplitude)* events extracted from a continuous recording. Importantly, the detector runs on the *preprocessed* fluorescence trace, not on the raw recording. Two upstream cleanup steps are typically applied first: an [isosbestic correction](isosbestic_correction.md) that removes optical artifacts shared between the indicator-sensitive and isosbestic channels, and [z-score normalization](zscore.md) that puts the trace on a noise-relative scale where detection thresholds are interpretable across recordings. What survives is, in principle, a trace whose deflections are predominantly indicator-driven; the detector takes that on faith and operates on whatever comes out of preprocessing. ![A 60 s synthetic z-scored fiber photometry trace (top) with detected events marked as red circles. The bottom strip shows the same time axis with the extracted event list rendered as vertical ticks. The figure makes the discretization explicit: a long continuous trace becomes a short list of timestamps that downstream analyses can operate on.](../_static/images/transient_detection_explainer/fig1_transient_extraction.svg) The following sections cover how to improve this basic approach to deal with complications that arise out of the characteristics of the signal. ### Addressing drift The mean of the signal is non-stationary across the session. Slow processes that survive preprocessing (residual photobleaching, slow patchcord motion, hemodynamic effects, and genuine biological state changes) create a long-term drift that breaks any single threshold applied across the session. A naive fixed threshold therefore catches events on stretches where the baseline is high and misses events on low-baseline stretches, simply because the threshold doesn't move with the drift. One way of addressing the session drift is to split the signal into contiguous chunks of fixed duration and recompute the typical level and noise scale from the samples inside each chunk. The detection threshold within a chunk is then tailored to that chunk's local conditions. The robust pair to use here is the median for the typical level and MAD (the median absolute deviation) for the noise scale, the same pair [introduced in the z-score explainer](zscore.md). ![A 60 s synthetic z-scored trace with eight identical-amplitude calcium events, against a slowly wandering baseline. Dotted light-gray vertical lines mark the chunk boundaries; the dashed gray line is the detection threshold. Each event is marked with a dot at its peak: red = detected, navy = missed. Top: a fixed session-wide threshold computed from session_median + 3 × session_MAD. The drift inflates the session-wide noise scale enough that the threshold lands above every event, and all eight are missed (navy dots). Bottom: the same trace with a chunk-local threshold drawn as a step function that follows chunk_median + 3 × chunk_MAD. The threshold now moves with the baseline, and most of the events the fixed threshold missed are recovered (red dots), with a few still missed (navy dots) where within-chunk drift contamination still inflates the local noise scale. The fix is partial: chunking improves on the session-wide failure but does not eliminate the underlying tension between drift and noise estimation.](../_static/images/transient_detection_explainer/fig2_drift_failure_and_fix.svg) ### Addressing outlier-induced bias Even with a reasonable window, the chunk's typical level and noise scale are still sensitive to what samples happen to land in it. A chunk that catches a large event plus its slow decay tail, or two events arriving close together, sees those high-amplitude samples shift the typical level slightly upward and inflate the noise scale substantially (their distance from the typical level is large, so they enter the spread tally at full distance). Both shifts push the detection threshold upward, and smaller real events in the same chunk fall below it. A one-sided trim of the per-chunk samples addresses this. Before computing the noise reference for a chunk, blank the upward outliers, presumed to be event-driven rather than noise-driven, and recompute the noise reference on what remains. The trimmed samples still participate in detection (they are not removed from the chunk, only from the noise reference). The remaining samples produce a cleaner estimate of the underlying noise scale, and the threshold set against that estimate catches smaller events that the contaminated single-pass version would miss. ![Three-panel walkthrough, stacked vertically, on a single 15 s chunk with two events of very different sizes. Each event is marked with a dot at its peak: red = detected, navy = missed. Top: the raw chunk with the naive single-stage threshold (median + K₂ × raw_MAD) drawn. The big event sits well above this line and is detected (red dot), but the smaller event sits below it and is missed (navy dot) because the big event has inflated the raw MAD. Middle: the cleanup step. The trace is drawn continuously, with samples whose z-score exceeds the first-pass cutoff T₁ drawn in faded green to mark them as excluded from the noise reference, while samples below the cutoff remain in saturated green to mark them as kept; the cleaned median and MAD are computed on the saturated-green samples only. Bottom: the original chunk restored with the two-stage threshold T₂ (median' + K₂ × cleaned_MAD) drawn in dark, and the naive threshold drawn faintly for comparison. The two-stage threshold sits lower because the cleaned MAD is smaller, and the smaller event now sits above it and is detected (red dot). The gap between the dark and faint lines is the advantage of the two-stage scheme: it recovers events that the contaminated single-stage threshold would miss.](../_static/images/transient_detection_explainer/fig4_two_stage_walkthrough.svg) In compact form the procedure is: 1. Compute the median and MAD of the raw chunk. Form the first-pass cutoff *T₁ = median + K₁ × MAD*. 2. Set aside samples above T₁. These are presumed extreme upward outliers. The filter is one-sided: downward excursions (e.g. a brief loss of coupling) are not removed. 3. Recompute the median and MAD on the remaining samples. Form the detection threshold *T₂ = median′ + K₂ × MAD′*. 4. In the original chunk (including the samples trimmed for the noise estimate), mark every sample exceeding T₂. These are the detected transients. - **K₁, K₂** are the multipliers: how many noise scales above the median each threshold sits. - **T₁ = median + K₁ × MAD** is the first-pass cutoff; raw-chunk samples above it are excluded from the noise reference. - **T₂ = median′ + K₂ × MAD′** is the detection threshold, applied against the cleaned median and MAD. Conceptually, K₁ (e.g. 2) places the cutoff T₁ a couple of noise-scales above the median: high enough to clear the noise, low enough that real events fall above it and get trimmed. K₂ (e.g. 3) then sets the detection threshold T₂ on the cleaned estimate. A higher K₂ is a stricter "what counts as a real event" criterion (fewer detections, real or noise), a lower one more permissive. ## Detecting transients by their rise The detector above judges each peak by its height: how far it sits above the typical level of the samples around it. A peak can instead be judged by its rise: how far the trace climbed to reach it, starting from the lowest point just before it. GuPPy offers this as the `minimum rise` detection method. It pairs every local maximum in the trace with the nearest local minimum before it, and counts the maximum as a transient when the climb between the two exceeds a fixed threshold. That climb is also the transient's amplitude. ![A 20 s synthetic ΔF/F trace with every local maximum whose rise exceeds 0.5 % ΔF/F marked, together with the local minimum just before it. A dotted line runs from each minimum to the time of its peak and an arrow climbs from the minimum's level to the peak, labeled with the size of the rise. Red filled markers are rises above the 2 % minimum and count as transients; navy hollow markers are rises below it. The bump on the decay of the second transient climbs only 0.5 from the dip just before it and is not counted, even though it sits well above baseline. The third transient starts from a dip below baseline and counts because its climb of 3.3 exceeds the minimum, although its peak is lower than the bump. The small event at 18 s rises 0.9 and is not counted.](../_static/images/transient_detection_explainer/fig6_rise_walkthrough.svg) Because the rise is measured from the trace's own preceding minimum, the baseline level never enters the comparison. A transient that starts from a dip below baseline counts as long as it climbs far enough, and a stretch where the whole baseline sits higher, such as after a drug raises tonic activity, adds no detections from noise riding on the raised level. For the same reason each transient is counted once: the smaller bumps on its decay climb only a little from the dip just before each of them, so they fall short of the threshold unless they are large in their own right. The threshold is fixed for the whole recording and expressed in the units of the metric the detector runs on: percent ΔF/F on the `dff` trace, z-score units on the `z_score` trace. On ΔF/F it is a fixed physical size, independent of the recording's noise; on a z-score it scales with the session-wide spread the z-score normalizes by. Either way nothing is re-estimated along the recording, so a change in how often transients occur, over minutes or hours, shows up directly in the event rate. The rise criterion has no noise floor of its own. Every small wiggle in the trace is a local maximum with a minimum before it, so the threshold has to sit well above the rises that noise produces, and the size of those noise rises depends on how heavily the trace was smoothed. A smaller **Window for Moving Average filter** leaves more and larger noise rises and the detected rate climbs with them, and a threshold that suits one recording's noise can be too permissive for a noisier one. ## Choosing a detection method The two methods behave differently in two common situations: a recording whose event rate changes partway through, and a set of recordings whose noise levels differ. The figures below run both detectors on synthetic ΔF/F traces whose true events are known, with GuPPy's default settings: 15 s windows, K₁ = 2 and K₂ = 3 for `MAD threshold`, and a minimum rise of 2 % ΔF/F. In the first situation the true event rate rises from about 2 to about 12 per minute halfway through a 20-minute recording. The MAD threshold is re-estimated in every 15 s window, and once events become frequent their decay tails make up a large share of each window's samples. The one-sided trim removes the highest samples but not the tails below the first-pass cutoff, so the noise estimate and the threshold rise with the activity, and events in the busiest windows fall below the threshold. At the same time the method counts every local maximum above the threshold, so a transient with a few noise ripples near its peak is counted several times, and noise crossings add detections in windows that hold no events. The detected rate therefore sits between about 15 and 40 per minute throughout, against a true rate of 2 and then 12, and the rise barely shows. The rise criterion is the same everywhere in the recording and counts each transient once, so its rate follows the true rate. ![Top two rows: two 30 s excerpts of a synthetic ΔF/F recording, one from the low-rate first half and one from the high-rate second half, with detections marked. The top row uses the MAD threshold method and draws its per-window threshold as a dashed step. In the high-rate excerpt the threshold jumps in the window that holds four events, and the method marks several points on single transients in the window before it. The middle row uses the minimum rise method on the same excerpts and marks one point per transient. Bottom: detections per minute over the whole recording. The dashed black line is the true event rate, which rises from about 2 to about 12 per minute at 10 min. The minimum rise rate (orange) follows it; the MAD threshold rate (purple) stays between about 15 and 40 per minute and shows little of the change.](../_static/images/transient_detection_explainer/fig7_rate_change.svg) In the second situation the same events, at a true rate of 6.5 per minute, sit in noise of increasing size, as when recordings differ in signal quality. The MAD threshold scales with each recording's noise, so the number of detections from noise alone stays roughly level, between 7 and 9 per minute here, while the real events, whose size does not change, fall below the rising threshold more often. The two effects partly offset, and the detected rate stays between 28 and 38 per minute while the noise quadruples. That rate sits several times above the true rate, because of the noise detections and the repeat counts of single transients, but it moves little with the noise. The minimum rise is a fixed size in ΔF/F, so it keeps finding the same events, but once the noise produces rises of comparable size it counts them too. Its noise detections climb from none at the lowest noise level to about 14 per minute at the highest, and its detected rate climbs with them, from 5.7 per minute, close to the true rate, to 20 per minute. ![Top: the same 30 s of a synthetic ΔF/F recording at the lowest and highest of four noise levels (standard deviation 0.3 and 1.2 % ΔF/F). Bottom left: the detected event rate at each noise level, against the true rate of 6.5 per minute (dashed). The MAD threshold method (purple) stays between about 28 and 38 per minute, several times the true rate; the minimum rise method (orange) starts near the true rate and climbs to about 20 per minute. Bottom middle: the fraction of the true events each method finds. The minimum rise method finds about 85 to 95 % of them throughout; the MAD threshold method falls from about 88 % to 66 % as the noise grows. Bottom right: detections that fall near no true event, per minute. The MAD threshold method stays between about 7 and 9 per minute; the minimum rise method climbs from 0 to about 14 per minute.](../_static/images/transient_detection_explainer/fig8_noise_level.svg) These results suggest when each method fits. `minimum rise` suits analyses that follow the event rate across time within a recording, such as before and after a drug, and indicators for which a size in ΔF/F has a meaning, such as a lab's criterion of a 2 % rise. Its threshold has to sit well above the rises the noise produces, which depends on the indicator's brightness, the recording's noise, and the moving-average filter, so it needs to be chosen with the noisiest recordings in mind. `MAD threshold` suits sets of recordings whose noise levels differ, because its threshold adapts to each recording without a size being chosen in advance, and its detected rate changes little with the noise level. Its rate includes noise detections and repeat counts of single transients, and the threshold shifts with the level of activity, so it is better suited to comparing recordings with similar activity than to following a change in rate. ## Reading the output The detector returns a per-event list of *(time, amplitude)* pairs plus two per-recording summary scalars: - **Event rate** (events per minute): how often the detector flags a transient. An approximation of the underlying biological event rate, dependent on the noise floor (which sets where the threshold lands) and contaminated by some non-zero rate of noise crossings. - **Mean amplitude** (in noise-relative units, i.e. multiples of the chunk's noise scale): the average above-threshold excursion among detected events. An approximation of the average biological event size, expressed in units that are local to the recording's noise floor. With `minimum rise`, the amplitude is each transient's rise instead, in the units of the metric the detector ran on. Rate is the headline statistic in most photometry papers that quantify transients: it is the single number that gets compared across conditions (control versus drug, baseline versus learning, etc.). The two dependencies above (noise floor across recordings; noise-crossing contribution within a recording) are unpacked next. ![Two-panel figure illustrating the two summary scalars on a synthetic 3-minute photometry recording with calcium-realistic event shape (rise ~100 ms, decay ~1 s) and ~7 events/min. Both panels share the same units (× MAD). Left: the trace with detected events marked as red dots; the dashed line is the median + 3 × MAD threshold; the panel title names the scalar (`event rate = X / min`). Right: a histogram of the per-event amplitudes, with the mean amplitude drawn as a dashed vertical reference line; the panel title names the scalar (`mean amplitude = M × MAD`). The figure makes both metrics visible: the rate is the count of dots in the left panel divided by the duration; the mean amplitude is the centroid of the amplitude distribution in the right panel. The two panels show two distributions of the same event list: events distributed over time (left, summarized by rate) and events distributed over amplitude (right, summarized by mean amplitude).](../_static/images/transient_detection_explainer/fig5_summary_statistics.svg) Mean amplitude is reported in noise-relative units, so cross-recording comparisons silently assume that the recordings have matched noise floors: the same physical event reads as a smaller noise-scale multiple when the noise floor is higher. The [z-score explainer](zscore.md) covers this fragility at session scale; the same logic applies here at chunk scale, and the same workarounds apply (re-express amplitudes in dF/F, or argue that SNR is matched). Within a single recording, some fraction of the detected rate is noise rather than biology. Any finite threshold is crossed by chance, so even a recording with no biological transients sits at a non-zero noise floor of detections, and a low rate should be read against that floor rather than as biology.