Fluorescence Correlation Spectroscopy
Fluorescence correlation spectroscopy (FCS) is a technique that analyzes fluorescence fluctuations at the single-molecule level
to infer fluorophore concentration, diffusion, and kinetic parameters underlying the signal.
A laser excites a small number of fluorophores within a tiny observation volume. As molecules diffuse in and out,
the intensity fluctuates primarily due to Brownian motion and, in some cases, photophysical processes such as triplet blinking.
In the single-photon regime, the detector produces individual photon events, and the Time Tagger
precisely time-stamps their arrival times.
Using these timestamps, the Time Tagger software and API enable computation of the correlation function
, from which particle number, diffusion coefficients, and reaction or conformational rates can be estimated.
This tutorial shows how to perform FCS and advanced variants like fluorescence cross-correlation spectroscopy (FCCS),
pulsed interleaved excitation (PIE), and raster image correlation spectroscopy (RICS), with the Swabian Instruments Time Tagger
for data acquisition and live analysis. After a brief recap, the tutorial shows how to configure channels and triggers, align delays,
acquire single-photon timestamps, and compute the correlation function (auto-/cross-correlation)
using multiple-
binning.
The tutorial covers:
FCS: set up a single-detector, continuous-wave (CW) workflow, run autocorrelation on the detector channel, monitor
, and visualize traces.
FCCS: acquire dual-color streams on two detectors/lasers, compute cross-correlation, and visualize traces for downstream interaction analysis.
PIE: drive interleaved excitation pulses (via Pulse Streamer), define time-gating windows, and suppress spectral cross-talk while retaining lifetime information.
RICS: integrate a scanning stage, map timestamps to pixels/lines, and compute spatially resolved correlation maps.
Microscope Setup
FCS can be performed on microscope setups such as confocal or two-photon excitation, which create small,
well defined observation volumes (typically in the order of 1 ) to capture low fluorophore numbers (0.1-100).
The observation volume is typically modeled as a 3D Gaussian ellipsoid.
The simplest setup contains one laser, one single-photon detector, the optical elements and the Time Tagger for data acquisition,
as well as the fluorescent sample (A).
The system can be expanded to include multiple lasers and detectors (B), a Pulse Streamer
as the laser driver for pulsed measurements, including PIE (C), and a scanning stage for RICS (D).
Single Photon Correlation
In FCS, relevant dynamics, such as particle diffusion and reaction kinetics, span timescales from microseconds to seconds. Building a correlation function from time-stamped single-photon data, therefore, requires an algorithm that is both memory-wise and computationally efficient across several lag times decades.
A naive approach, using a Histogram with uniform time bins, becomes impractical when trying to cover
a broad range of lag times while preserving high temporal resolution.
This is where the multiple-tau approach comes in. Instead of using equally spaced bins, this algorithm uses logarithmic
binning: the bin width increases with
, allowing coverage of large timescales without excessive memory or CPU usage.
The sketch above illustrates a typical autocorrelation (CH 1 = CH 2) algorithm.
On the left is the intensity-trace view, where the signal is shifted by and multiplied by itself.
On the right is the event-based (single-photon) view.
In the event-based autocorrelation, each photon arrival time serves as a reference (starting with 1 in step 1 out of N).
From this reference point, the lag times are discretized according to the logarithmic binning.
For each lag interval, the number of photons is counted and stored in the corresponding bin. By accumulating data from all N reference points
(Step 1, Step 2, …, Step N) and normalizing for bin width and the number of available photon pairs (not shown) at each
, one obtains the final autocorrelation curve. For a detailed description of the algorithm,
please refer to the publication (Laurence, 2005).
The correlation curve shows three characteristic regions.
Region I and II capture antibunching and triplet state dynamics at very short time scales
and are commonly omitted in FCS plots (gray region).
Region III reflects the diffusion of fluorophores through the observation volume and is the region
of interest for further FCS analyses.
The data from this region is typically fitted with a model that corresponds to the experimental expectation
(3D/2D/anomalous diffusion, triplet correction, etc.).
The model then reveals the sample’s parameters.
In practice, the overall curve shape and the value of
at short-
(e.g.,
)
are informative about diffusion and particle number.
All regions can be computed simultaneously using the Time Tagger library, as will be shown next.
Time Tagger configuration
The Time Tagger library provides several measurement classes designed for microscopy. We start by defining channel assignments and then add the necessary configuration.
DETECTOR_1_CH = 1 # Rising edge on input 1
DETECTOR_2_CH = 2 # Optional: FCCS
LASER_CH = 3 # Optional: Lifetime
Connect to the Time Tagger:
from Swabian import TimeTagger
tt = TimeTagger.createTimeTagger()
The Time Tagger hardware allows you to specify an individual trigger level voltage for each input channel. This trigger level applies to both rising and falling edges of an input pulse. Whenever the signal crosses this threshold, the Time Tagger registers an event and stores its timestamp. It is often convenient to set the trigger level to half the signal amplitude. For example, if your laser sync output provides pulses of 0.2 V amplitude, set the trigger level to 0.1 V on this channel. The default trigger level is 0.5 V.
tt.setTriggerLevel(DETECTOR_1_CH, 0.5)
tt.setTriggerLevel(DETECTOR_2_CH, 0.4)
tt.setTriggerLevel(LASER_CH, 0.1)
The basic hardware setup is complete; next we define the measurements.
Continuous Wave Laser
Building the autocorrelation function with the multiple- approach is straightforward in the API.
First, define your experiment parameters and create the HistogramLogBins measurement.
exp_start = -7 # 100 nanoseconds
exp_stop = 0 # 1 second
n_bins = 150
hist_log_bins = TimeTagger.HistogramLogBins(tagger=tt,
click_channel=DETECTOR_1_CH,
start_channel= DETECTOR_1_CH,
exp_start=exp_start,
exp_stop=exp_stop,
n_bins=n_bins)
By default, the measurement starts immediately after creation,
begins acquiring time tags and computing the autocorrelation on channel DETECTOR_1_CH.
Acquisition can be controlled via the common methods shared by all measurement classes.
To run the measurement for a fixed duration, use startFor():
DURATION = 60e12 # one minute
hist_log_bins.startFor(capture_duration=DURATION)
While the measurement is running, you can poll isRunning()
and update a live plot of :
import matplotlib.pyplot as plt
while hist_log_bins.isRunning():
g2 = hist_log_bins.getDataObject().getG2()
tau = hist_log_bins.getBinEdges() # shape: (n_bins+1)
plt.plot(tau[1:], g2)
plt.pause(0.1)
You can fit the measured on the fly with a model suited to your sample and optical parameters.
Fluorescence Cross-Correlation Spectroscopy
When studying molecular interactions between two or more molecules, fluorescence cross-correlation spectroscopy (FCCS)
becomes a powerful extension of FCS.
In practice, it is useful to acquire the two autocorrelations alongside the cross trace:
they provide per-channel quality checks and a reference when interpreting the cross-correlation.
With the Time Tagger API, create three HistogramLogBins measurements:
autocorrelation on detector 1 (start_channel = click_channel = DETECTOR_1_CH),
autocorrelation on detector 2 (start_channel = click_channel = DETECTOR_2_CH),
and the FCCS cross-correlation (e.g., start_channel = DETECTOR_2_CH, click_channel = DETECTOR_1_CH).
Start them together with the SynchronizedMeasurements helper class so they share the same acquisition window
and can be directly compared.
sm = TimeTagger.SynchronizedMeasurements(tt)
sync_proxy = sm.getTagger()
hist_log_bins_11 = TimeTagger.HistogramLogBins(tagger=sync_proxy,
click_channel=DETECTOR_1_CH,
start_channel= DETECTOR_1_CH,
exp_start=exp_start,
exp_stop=exp_stop,
n_bins=n_bins)
hist_log_bins_22 = TimeTagger.HistogramLogBins(tagger=sync_proxy,
click_channel=DETECTOR_2_CH,
start_channel= DETECTOR_2_CH,
exp_start=exp_start,
exp_stop=exp_stop,
n_bins=n_bins)
hist_log_bins_12 = TimeTagger.HistogramLogBins(tagger=sync_proxy,
click_channel=DETECTOR_1_CH,
start_channel= DETECTOR_2_CH,
exp_start=exp_start,
exp_stop=exp_stop,
n_bins=n_bins)
sm.startFor(DURATION)
sm.waitUntilFinished()
CW excitation can photo-bleach fluorophores or drive triplet-state saturation, both of which degrade signal quality. Background from scattered light also reduces the signal-to-noise ratio (SNR). Pulsed excitation helps mitigate these effects and enables timing-based strategies (e.g., lifetime, gating, PIE).
Pulsed Laser
Pulsed lasers offer several important advantages for FCS. The first one is that well-defined laser pulses provide access to fluorescence lifetime information of the sample. More details on this topic can be found in the FLIM application page or in the Confocal Fluorescence Microscope tutorial.
A second advantage is control over excitation timing: the pulse sequence can be configured to minimize bleaching and to allow recovery of fluorophores from non-radiative triplet states. To achieve this, the pulse period is typically chosen longer than the characteristic relaxation times, which in practice corresponds to repetition rates of about 80-100 MHz (12.5-10 ns) or lower. The pulse duration is adjusted so that, on average, approximately one emission event per pulse is expected. In this regime, the detected click rate is usually about 1% of the laser repetition rate, (e.g., 1 MHz on the detector for a 100 MHz laser). Under such conditions, the laser trigger generates roughly 80-100 MTags/s, while each detector records 0.8-1 MTags/s; the overall stream can meet or exceed the sustained transfer limit from the Time Tagger to the PC (90 MTags/s).
To reduce the bandwidth without losing physical information, the Conditional Filter can be used.
With this hardware setting enabled, the Time Tagger transmits only the next tag on a high-rate channel
after a tag on a low-rate channel. For pulsed excitation, it is useful to delay the laser channel by one laser period
so that the photon-originating laser event is forwarded.
This can be achieved with the setDelayHardware() feature.
After applying the Conditional Filter, the laser can be
“pushed back” in software with setDelaySoftware().
See the in-depth guide Conditional Filter for details.
laser_frequency = 100e6 # 100 MHz
laser_period = 1/laser_frequency * 1e12 # picoseconds
tt.setDelayHardware(LASER_CH, int(laser_period)) # Delay is specified in picoseconds
tt.setDelayHardware(DETECTOR_1_CH, 0) # Default value is 0
tt.setConditionalFilter(trigger=[DETECTOR_1_CH], filtered=[LASER_CH])
tt.setDelaySoftware(LASER_CH, -int(laser_period))
This approach extends to multiple lasers and detectors.
Note
If fixed delays exist between laser and detector (e.g., cable length or device latency),
compensate them first with per-channel hardware delays via setDelayHardware().
On pulsed-laser setups, autocorrelation often benefits from aligning bin edges to integer multiples of the laser period. This is supported by HistogramCustomBins and not by HistogramLogBins. In the following example, we run these two measurements in parallel by using SynchronizedMeasurements and compare the results. The only difference between the two analyses is the time binning: the custom bin edges are aligned precisely to integer multiples of the laser period, while HistogramLogBins uses a purely logarithmic grid.
hist_log_bins = TimeTagger.HistogramLogBins(sync_proxy, DETECTOR_1_CH, DETECTOR_1_CH,
exp_start, exp_stop, n_bins)
# Example of custom bin edges starting from logarithmic grid
log_bin_edges = np.logspace(exp_start, exp_stop, num=n_bins + 1, base=10.0, dtype=np.float64)
custom_bin_edges = np.round(log_bin_edges / laser_period) * laser_period
custom_bin_edges = np.unique(custom_bin_edges) # remove duplicates
hist_cust_bins = TimeTagger.HistogramCustomBins(tagger=sync_proxy,
click_channel=DETECTOR_1_CH,
start_channel=DETECTOR_1_CH,
binedges=custom_bin_edges)
The effect of aligning bin edges to the laser period is most evident at short lag times. When a purely logarithmic grid is slightly misaligned with the laser pulse train, especially below 10 microseconds, HistogramLogBins can produce a spiky correlation trace. By contrast, HistogramCustomBins uses period-aligned edges, yielding a smoother correlation curve at short timescales.
As a reminder, for extended runs, the laser stability can affect the analysis.
A practical mitigation is to lock the Time Tagger to the laser via setReferenceClock(),
keeping detector timestamps phase-aligned to the pulse train.
When operated with the Reference Clock, the system also supports the Conditional Filter functionality,
as discussed in the in-depth guide Software-Defined Reference Clock.
Spectral Overlap and Pulsed Interleaved Excitation
FCCS with two or more lasers and detectors can be set up as described in the section Fluorescence Cross-Correlation Spectroscopy. Additionally, you can measure fluorescence lifetime on the same time tags stream in parallel. This can be achieved by using the SynchronizedMeasurements class together with the Histogram measurement.
lifetime_binwidth = 100 # ps
n_bins = 250
hist_cust_bins = TimeTagger.HistogramCustomBins(tagger=sync_proxy,
click_channel=DETECTOR_1_CH,
start_channel=DETECTOR_2_CH,
binedges=custom_bin_edges)
hist_1 = TimeTagger.Histogram(tagger=sync_proxy,
click_channel=DETECTOR_1_CH,
start_channel=LASER_CH,
binwidth=lifetime_binwidth,
n_bins=n_bins)
hist_2 = TimeTagger.Histogram(tagger=sync_proxy,
click_channel=DETECTOR_2_CH,
start_channel=LASER_CH,
binwidth=lifetime_binwidth,
n_bins=n_bins)
Dual-color FCCS setups may introduce spectral cross-talk between fluorophores. This can be identified by exciting one laser at a time and inspecting the intensity trace or the lifetime histogram of both detectors. In Step 1 of the sketch below, the blue laser excites both the blue and the red fluorophore (bleed-through). In the next step, the red laser excites the red fluorophore in the red detector channel as expected.
A practical way to suppress spectral overlap is pulsed interleaved excitation (PIE). PIE is typically performed in two steps:
Interleave the lasers in time: laser 1 is triggered, a defined delay (often comparable to the fluorescence lifetime) elapses, and laser 2 is triggered. This temporal separation allows each detected photon to be associated with its excitation laser.
Gate the detector channels: short time windows are applied so that each detector accepts photons only in the interval that follows its own laser pulse. For example, the red detector channel is open immediately after the red laser pulse; the blue detector channel is open immediately after the blue laser pulse. This gating ensures that each detected photon is attributed to the correct excitation source.
An example implementation, using the Pulse Streamer to drive the lasers and the Time Tagger to define the gates in software, is shown below. The laser period and pulse duration are experiment-dependent. Detector channels are gated with DelayedChannel (to define the stop edge) and GatedChannel.
from pulsestreamer import PulseStreamer
ps = PulseStreamer('pulsestreamer')
LASER_blue = 1
LASER_red = 2
DET_blue = 3
DET_red = 4
... # configure trigger levels, hardware delays, conditional filters
pulse_pattern_blue = [(2, 1),(24,0)] # 2 ns HIGH, 24 ns LOW
pulse_pattern_red = [(13,0),(2, 1),(11,0)] # 13 ns low (pulse period), 2 ns HIGH, 11 ns LOW
seq = ps.createSequence()
seq.setDigital(0, pulse_pattern_blue) # connect PS channel 0 to blue laser
seq.setDigital(1, pulse_pattern_red) # connect PS channel 1 to red laser
ps.stream(seq) # start the pulse stream
# define gates using delayed channels
pulse_period = 13_000 # ps
blueStop = TimeTagger.DelayedChannel(tt, LASER_blue, pulse_period)
redStop = TimeTagger.DelayedChannel(tt, LASER_red, pulse_period)
blueGate = TimeTagger.GatedChannel(tt, DET_blue, LASER_blue, blueStop.getChannel())
redGate = TimeTagger.GatedChannel(tt, DET_red, LASER_red, redStop.getChannel())
red_channel = redGate.getChannel()
blue_channel = blueGate.getChannel()
... # use red_channel and blue_channel for FCS, FCCS, lifetime analysis, etc.
# instead of the original DET_blue and DET_red channels
PIE separates the blue and red fluorescence signals with minimal spectral cross-talk, producing per-excitation FCS/FCCS traces. Gating operates in real time, so acquisition and analysis remain live. The same approach can be combined with scanning to obtain spatially resolved FCS maps across multiple pixels.
Raster Image Correlation Spectroscopy
In the Confocal Fluorescence Microscope tutorial, we describe how to set up a fluorescence lifetime imaging microscope (FLIM) using the Time Tagger together with a scanning system. Although a dedicated measurement class for RICS is not yet available, it can still be implemented straightforwardly by following a simple strategy.
The simplest approach is to implement RICS with a basic Python loop. This requires a scanner that can either be triggered directly via an API command, for example, by using the Pulse Streamer to control the scanning position, or a scanner that provides position feedback available in Python. The feedback may consist of the absolute position or relative changes, allowing the software to track the scan reliably.
Since a typical FCS measurement takes several seconds, RICS does not require high-speed scanning systems. Even a relatively slow scanner is sufficient. A minimal example of handling FCS data at each position is shown below:
hist_cust_bins = TimeTagger.HistogramCustomBins(sync_proxy, DETECTOR_1_CH, DETECTOR_1_CH, custom_bin_edges)
measurement_XYZ = ... # set up all measurements
positions = [...] # list of positions to scan
for pos in positions:
scanner.moveTo(pos) # move scanner to position
sm.startFor(DURATION) # start all synchronized measurements
sm.waitUntilFinished()
data_custombins = hist_cust_bins.getDataObject().getG2()
data_XYZ = ... # collect additional synchronized data
analyze_data(data_custombins) # user function for data analysis
save_data(pos, data_custombins, data_XYZ) # user function to save results per position
The code above assumes control of the scanner through a generic scanner API. At each scan position, the defined SynchronizedMeasurements are started. Data can be analyzed while the measurement is running or after completion. The scanner then advances to the next position, and the measurement cycle repeats.