Spike inference from Calcium imaging data with CASCADE
CASCADE, short for Calibrated spike inference from calcium imaging data using deep networks, is an open-source, deep learning-based pipeline for inferring neuronal spiking activity from calcium imaging data. It provides robust and calibrated spike inference by leveraging ground-truth datasets and convolutional neural networks, enabling the reconstruction of continuous spike estimates from noisy and temporally blurred calcium fluorescence signals. These continuous estimates can subsequently be expressed as firing rates or, optionally, converted into inferred discrete spike events.
CASCADE translates calcium imaging $\Delta F/F$ traces into continuous inferred spike estimates and, optionally, reconstructed discrete spikes. Source: CASCADE GitHub repositoryꜛ (license: MIT License)
CASCADE operates by training deep neural networks on ground-truth datasets containing simultaneously recorded calcium fluorescence and electrophysiological spike times. A pre-trained or custom model can then be applied to new calcium traces to generate continuous estimates of the underlying spiking activity. These estimates represent inferred spike counts associated with individual imaging time bins and can optionally be transformed into firing rates or reconstructed into discrete spike events.
The approach is particularly valuable for neuroscience experiments where direct electrophysiological recordings are impractical, and offers a standardized, reproducible method for analyzing large calcium imaging datasets.
Acknowledgment
This script is based on the official demoꜛ provided by the CASCADE teamꜛ, with modifications for use in the present course. Original code by Peter Rupprechtꜛ and Adrian Hoffmannꜛ, Helmchen Labꜛ, in collaboration with the Friedrich Labꜛ. All credit for the original development and maintenance of CASCADE belongs to the authors. For scientific use, please cite the following publication:
Rupprecht et al., A database and deep learning toolbox for noise-optimized, generalized spike inference from calcium imaging, 2021, Nature Neuroscience, doi: 10.1038/s41593-021-00895-5ꜛ.
Please refer to the CASCADE GitHub repositoryꜛ for the latest updates, documentation, and contact information. Feedback regarding this adaptation may be directed to the course instructor; questions about CASCADE itself should be addressed to Peter Rupprechtꜛ.
Installation
Before you begin, ensure that that you have installed the latest version of Miniforgeꜛ on your computer. Miniforge is a minimal installer for conda, which is a package manager that allows you to install Python packages and their dependencies easily. The latest versions come with the mamba package manager, which is a faster alternative to conda. If you are using an older version of Miniforge, you can use conda instead of mamba and install mamba in your new environment separately. Follow similar steps as described in the previous tutorial.
Create a conda environment with CASCADE
First, we need to create a conda environment with the required packages. CASCADE offers instructions for different platforms, including Windows, macOS, and Linux, with and without GPU support:
PC (Windows/Linux) with GPU support
For a GPU installation (faster, recommended if you will train networks):
conda create -n cascade python=3.7 tensorflow-gpu keras h5py numpy scipy matplotlib seaborn ruamel.yaml ipykernel ipython -y
conda activate cascade
PC (Windows/Linux) without GPU support
For a CPU installation (slower, recommended if you will not train a network):
conda create -n cascade python=3.7 tensorflow keras h5py numpy scipy matplotlib seaborn ruamel.yaml ipykernel ipython -y
conda activate cascade
macOS with GPU support
I’ve recently tested this on a MacBook Pro with an M1 chip, using macOS Sequoya 15.5, and it worked perfectly:
conda create -n cascade_2025 -y python=3.8 tensorflow keras h5py numpy scipy matplotlib seaborn ruamel.yaml ipykernel ipython -y
conda activate cascade_2025
conda install -c apple tensorflow-deps
pip install tensorflow-macos tensorflow-metal
Troubleshooting on macOS: If you receive an error message after executing conda install -c apple tensorflow-deps, skip this step and proceed with the next one. The installation of tensorflow-macos and tensorflow-metal will still work.
Troubleshooting “SSL errors”: Depending on your institutional environment, you may encounter https- and/or SSL-certificate-related errors. In this case, please follow the troubleshooting instructions in this blog post, in particular the deactivation of the SSL verification (just perform step 1) and the reconfiguration of the conda channels. It may also be necessary to execute the commands above without the -c anaconda in case your institutional environment blocks access to the Anaconda channel.
Download the CASCADE repository
CASCADE is not available on PyPI, so you need to download the repository from GitHub. The Github repository contains all custom functions, the ground truth datasets and the pre-trained models. To do so, you have two options:
Option 1: Download the repository as a ZIP file
You can download the repository as a ZIP file from the CASCADE GitHub page. After downloading, extract the ZIP file to the tutorial folder 02 Cascade tutorial in the course directory.
Option 2: Clone the repository using Git
If you have Git installed, you can clone the repository directly into the tutorial folder using the following Python command:
# download Cascade if not present (this may take a while):
import os
if "02 Cascade tutorial" in os.getcwd():
if not os.path.exists('Cascade'):
print("Cascade directory not found. Cloning the repository...")
!git clone https://github.com/HelmchenLabSoftware/Cascade
print("Cascade repository cloned successfully.")
else:
print("Cascade directory already exists.")
os.chdir('Cascade')
print(f"Changed directory to {os.getcwd()}")
Relation between action potentials and calcium transients
The action potential is the fundamental electrical event by which neurons communicate. It is a brief, stereotyped change in membrane potential, initiated when synaptic input or external stimulation depolarizes the membrane beyond a threshold.
Stages of an action potential. The trace of an action potential can also be simulated numerically, e.g., using the Hodgkin-Huxley or Fitzhugh-Nagumo model. Source: Wikimedia Commons (license: CC BY-SA 3.0; modified)
Phases of the action potential:
- Depolarization: Voltage-gated sodium (Na⁺) channels open, allowing Na⁺ influx and a rapid rise of the membrane potential (from typically −70 mV to +30 mV).
- Repolarization: Sodium channels inactivate; voltage-gated potassium (K⁺) channels open, K⁺ exits the cell, and the membrane potential falls back.
- Hyperpolarization: As K⁺ channels close slowly, the membrane potential briefly undershoots its resting value.
The entire electrical event lasts ~1–2 milliseconds. However, a key link to functional imaging comes from voltage-gated calcium (Ca²⁺) channels, which open during the depolarization (and sometimes early repolarization) phase — most notably at axon terminals and in certain somatic/dendritic compartments. This results in a brief but significant influx of Ca²⁺ ions into the neuron.
Calcium transient:
The influx of Ca²⁺ produces a transient rise in intracellular calcium concentration. The amplitude and time course of this transient depend on:
- The number and timing of action potentials.
- The local density and subtype of Ca²⁺ channels.
- Cellular buffering, extrusion mechanisms, and compartment geometry.
Temporal relationship:
- Action potential: Rapid (1–2 ms, all-or-none, digital signal).
- Ca²⁺ transient: Slower (10–100 ms or more), reflecting biological Ca²⁺ handling.
- Measured fluorescence: Even slower, because the optical signal reflects both the underlying Ca²⁺ dynamics and the kinetics of the chosen indicator (e.g., GCaMP variants, OGB-1, Fluo-4).
Why do indicator kinetics matter?
- Fast indicators (e.g., GCaMP6f) have rapid binding/unbinding and better temporal fidelity, but may have lower signal amplitude.
- Slow indicators (e.g., GCaMP6s) bind Ca²⁺ for longer, producing larger, longer-lasting signals but greater temporal blurring.
- The recorded fluorescence trace is thus a convolution of the cell’s true Ca²⁺ dynamics and the indicator’s own response, resulting in a signal that rises and decays much more slowly than the action potential that triggered it.
Does the fluorescence decay match the true Ca²⁺ decay?
- Not exactly. The decay time of the recorded signal is generally longer than the underlying Ca²⁺ transient because it includes both biological clearance and indicator off-kinetics.
- For this reason, fast and slow indicators exist: to balance sensitivity against temporal resolution, depending on experimental goals.
Summary table:
| Event | Typical time course | Notes |
|---|---|---|
| Action potential | 1–2 ms | Electrical event (Na⁺/K⁺ channels) |
| Ca²⁺ influx & clearance | 10–100 ms (rise/decay) | Biological, varies with cell/compartment |
| Indicator fluorescence response | 50 ms – 1 s (rise/decay) | Convolution of Ca²⁺ + indicator kinetics |
Analytical implication:
Calcium imaging does not measure action potentials directly, but a filtered, noisy, and temporally blurred proxy—the fluorescence signal of the indicator bound to Ca²⁺. The lack of strict one-to-one correspondence means:
- Not every spike produces a clearly separable calcium transient (especially at high firing rates or with slow indicators).
- Some calcium transients represent the cumulative effect of multiple spikes.
- Noise, baseline drift, and kinetics can obscure single events.
This biophysical chain underlies the need for careful spike inference algorithms: reconstructing the underlying spike train from the slow, convolved, and noisy optical signal, given known indicator properties and the basic principles of cellular calcium dynamics.
Import required python packages
After this brief background overview, we now begin our analysis and start with importing some standard python packages:
import warnings
import glob
import numpy as np
import scipy.io as sio
import matplotlib.pyplot as plt
import ruamel.yaml as yaml
yaml = yaml.YAML(typ='rt')
warnings.filterwarnings('ignore')
Next, we import modules from the CASCADE folder you just downloaded:
from cascade2p import checks
checks.check_packages()
from cascade2p import cascade # local folder
from cascade2p.utils import plot_dFF_traces, plot_noise_level_distribution, plot_noise_matched_ground_truth
Load a sample data set of $\Delta F/F$ traces
We begin this tutorial with some sample data provided by CASCADE. CASCADE generally expects the traces to be saved as a single large 2D NumPy array, where each row corresponds to a neuron and each column corresponds to a time point (neurons $\times$ time, thus, (neurons, time)). The $\Delta F/F$ values stored therein should be numeric, not in percent (e.g. 0.5 instead of 50%).
To load the sample data, we need to define a function which handles .npy and .mat files (the format used by the sample datasets distributed with CASCADE). This function will read the data and return it as a 2D NumPy array.
Info: If your own data is already in the correct array format, you can skip the usage of this function and use your array directly.
def load_neurons_x_time(file_path):
"""Custom method to load data as 2d array with shape (neurons, nr_timepoints)"""
if file_path.endswith('.mat'):
traces = sio.loadmat(file_path)['dF_traces']
# PLEASE NOTE: If you use mat73 to load large *.mat-file, be aware of potential numerical errors, see issue #67 (https://github.com/HelmchenLabSoftware/Cascade/issues/67)
elif file_path.endswith('.npy'):
traces = np.load(file_path, allow_pickle=True)
# if saved data was a dictionary packed into a numpy array (MATLAB style): unpack
if traces.shape == ():
traces = traces.item()['dF_traces']
else:
raise Exception('This function only supports .mat or .npy files.')
print('Traces standard deviation:', np.nanmean(np.nanstd(traces,axis=1)))
if np.nanmedian(np.nanstd(traces,axis=1)) > 2:
print('Fluctuations in dF/F are very large, probably dF/F is given in percent. Traces are divided by 100.')
return traces/100
else:
return traces
Now, we load a sample data set provided by CASCADE. The dataset contains calcium imaging data from a mouse visual cortex, with ground truth spike trains available for comparison. The data is stored in a .mat file, which we will load using the function defined above. We also need to specify the frame rate of the calcium imaging data, which is required for the spike inference process.
example_file = "Example_datasets/Allen-Brain-Observatory-Visual-Coding-30Hz/Experiment_552195520_excerpt.mat"
frame_rate = 30 # fps
# check and load the example file:
try:
traces = load_neurons_x_time( example_file )
print('Number of neurons in dataset:', traces.shape[0])
print('Number of timepoints in dataset:', traces.shape[1])
except Exception as e:
print('\nSomething went wrong!\nEither the target file is missing, in this case please provide the correct location.\nOr your file is not yet completely uploaded, in this case wait until the upload is completed.\n')
print('Error message: '+str(e))
# THIS CELL/STEP IS RESERVED FOR THE EXERCISE
# overwrite traces by your own data:
# traces = ...
Let’s investigate the shape of the loaded data to ensure it is in the correct format:
print(f"shape of dF/F traces: {traces.shape}")
The shape (74, 6001) = (neurons $\times$ time) indicates that we have 74 neurons and 6001 time points. So, everything is as expected. Let’s plot the trace of the first neuron to see what it looks like:
# define the neuron index to plot:
neuron_i = 0
# define a time array based on the frame_rate:
time_vector = np.arange(traces.shape[1]) / frame_rate
# plot the calcium trace for the selected neuron:
plt.figure(figsize=(12, 5))
plt.plot(time_vector, traces[neuron_i, :])
plt.xlabel('Time (seconds)')
plt.ylabel(f'\Delta F/F')
plt.xlim(0, time_vector[-1])
plt.title(f'Calcium trace of neuron {neuron_i + 1}')
plt.show()
Trace of the first neuron from the sample dataset. The trace shows the fluorescence signal over time, which is typical for calcium imaging data. The y-axis represents the $\Delta F/F$ values, which indicate changes in fluorescence relative to a baseline level. The x-axis represents time in seconds, based on the specified frame rate of 30 Hz.
Exercise: Plot some other traces to see how they look like. What do you notice?
Instead of plotting a single trace on “our own”, we can use CASCADE’s plotting functions to visualize the traces. This is useful for quickly checking the data quality and understanding the overall structure of the dataset. Here is how you can plot randomly some selected calcium traces:
plt.rcParams['figure.figsize'] = [13, 13]
np.random.seed(0) # for reproducibility
neuron_indices = np.random.randint(traces.shape[0], size=16)
time_axis = plot_dFF_traces(traces,neuron_indices,frame_rate)
Random sample of calcium traces from the sample dataset. The plot shows the fluorescence signals of 16 randomly selected neurons over time, with each trace representing a different neuron. The y-axis represents the $\Delta F/F$ values, indicating changes in fluorescence relative to a baseline level, while the x-axis represents time in seconds based on the specified frame rate of 30 Hz.
Plotting random traces helps to check whether the data have been loaded correctly. If you want to plot specific instead of randomly selected neurons, modify the variable neuron_indices accordingly.
Plot distribution of noise levels
The traces are quite noisy, which is typical for calcium imaging data.
In calcium imaging analysis, the noise level is a quantitative measure of the baseline variability (fluctuations not caused by neural activity) in each neuron’s fluorescence trace. It is usually given as a standard deviation (or a related metric) of the trace’s background fluctuations, normalized by the signal amplitude and frame rate. CASCADE offers a method to estimate the noise level of the traces:
plt.rcParams['figure.figsize'] = [12, 5]
noise_levels = plot_noise_level_distribution(traces, frame_rate)
Histogram of noise levels across all neurons in the dataset. The x-axis represents the noise level (standard deviation of the fluorescence trace), while the y-axis shows the number of neurons with that noise level. The histogram provides a visual representation of how noise is distributed across the dataset, indicating the variability in signal quality among different neurons.
What does the noise level mean?
- Low noise level: The fluorescence trace has little baseline fluctuation, meaning genuine calcium transients (spikes) stand out more clearly.
- High noise level: The trace is “noisier”, i.e., there are larger random fluctuations not related to neuronal activity, making it harder to reliably detect true events.
Why is this important?
- Quality control: The distribution of noise levels across neurons allows you to assess the overall quality of your dataset. A narrow, low-centered distribution means your data is generally “clean”; a wide or high-centered distribution suggests variable or poor signal quality.
- Spike inference performance: The performance of any spike inference algorithm (including CASCADE) is fundamentally limited by the noise level:
- Low-noise traces allow more accurate spike detection.
- High-noise traces may lead to missed spikes (false negatives) or false positives.
- Network training and calibration: CASCADE uses the noise level to select or train an appropriate deep learning model for spike inference, since the ground truth datasets and models are stratified by noise level. For optimal performance, the noise distribution of your data should match that of the ground truth set used to train the model.
What does our noise level histogram tell us?
The histogram above shows how the noise levels are distributed across all neurons in our dataset.
- Peak location: Indicates the typical noise level in our experiment.
- Spread: Reflects neuron-to-neuron variability (possibly due to depth, indicator loading, or other biological/technical factors).
- Outliers: Very high or low noise neurons may warrant further inspection, as they may represent artefacts, poorly segmented cells, or technical errors.
Summary
Computing and visualizing the noise level distribution is a crucial diagnostic step before spike inference. It informs you about data quality, guides model selection, and ultimately sets the fundamental limit on the accuracy of spike train reconstruction from your calcium imaging data.
Prepare CASCADE: Select a pre-trained model
Next, we need to select a pre-trained model for spike inference. CASCADE provides several pre-trained models that are optimized for different noise levels and, of course, different Calcium indicators. Let’s first get a list of available models:
cascade.download_model( 'update_models', verbose = 1)
yaml_file = open('Pretrained_models/available_models.yaml')
X = yaml.load(yaml_file)
list_of_models = list(X.keys())
print('\n List of available models: \n')
for model in list_of_models:
print(model)
Note: You can view all currently available pre-trained models in the CASCADE GitHub repositoryꜛ.
As you can see, there is already a bunch of pre-trained models available. Instead of scrolling through the list, you can also search for a specific model by its name. For example, if you want to find a model for the GCaMP6 indicator, you can use the following code:
# define some helper function:
import re
def find_models(search_string, model_list):
pattern = re.compile(rf'{search_string}[a-z]*', re.IGNORECASE)
matching_models = [model for model in model_list if pattern.search(model)]
if matching_models:
print(f'\nFound {len(matching_models)} models matching "{search_string}":')
for model in matching_models:
print(model)
else:
print(f'\nNo models found matching "{search_string}". Please check the spelling or try a different search term.')
return matching_models
search_string = 'GCaMP6' # search for GCaMP6 models; if you indicator is not listed, try alternative spellings like GC6
# Example usage:
_ = find_models(search_string, list_of_models)
Exercise: Play around with the search function above and search for, e.g., all 15Hz framerate models.
After that, proceed with the next cell. To continue, we select a model that suits best to our sample data set: Global_EXC_30Hz_smoothing25ms. The following code will select and download it:
model_name = "Global_EXC_30Hz_smoothing25ms"
cascade.download_model(model_name, verbose=1)
Infer spiking activity from $\Delta F/F$ traces
The next step is the core of the CASCADE pipeline: inferring neuronal spiking activity from the $\Delta F/F$ traces using the selected pre-trained model. The calcium fluorescence data are passed through a deep convolutional neural network that has been trained on simultaneously recorded calcium traces and electrophysiological spike data. The primary output is a continuous estimate of the expected spike count associated with each imaging time bin.
In the core step of spike inference, CASCADE takes the measured calcium fluorescence trace ($\Delta F/F$) of each neuron and estimates the underlying spiking activity as a function of time. This is achieved by passing temporal windows of the fluorescence signal through a deep convolutional neural network trained on ground-truth datasets in which calcium fluorescence and electrophysiological spike times were recorded simultaneously.
For every imaging time point, CASCADE evaluates a temporal window of fluorescence values surrounding that time point. In simplified notation, the model computes
\[\hat{s}[k] = f(\mathbf{F}_{k-w:k+w}),\]where $\hat{s}[k]$ is the continuous inferred spike estimate associated with imaging frame $k$, $w$ defines the temporal context used by the network, and $\mathbf{F}_{k-w:k+w}$ contains the corresponding fluorescence samples. The function $f$ represents the nonlinear mapping learned by the neural network.
Importantly, $\hat{s}[k]$ is not a binary spike indicator and should not be interpreted as the probability that exactly one spike occurred at frame $k$. It represents a continuous estimate of the expected spike count associated with that imaging time bin. The resulting array therefore has the same temporal sampling as the input calcium trace but contains continuous inferred spiking activity rather than fluorescence values.
Computationally, the network performs a nonlinear transformation that learns the statistical relationship between calcium fluorescence and electrophysiologically measured spiking activity. Rather than attempting a purely analytical inversion of calcium-indicator kinetics, CASCADE learns this mapping from experimental ground-truth data and can therefore account for indicator dynamics, nonlinearities and noise characteristics represented in the training datasets.
The resulting continuous time series should be interpreted as an inferred spike-count density on the temporal grid of the calcium recording: each value estimates how much spiking activity is associated with the corresponding imaging time bin. Because the ground-truth spike trains used during training are represented with finite temporal precision and are temporally smoothed, the CASCADE output is itself a continuous rather than binary representation of spiking activity. It can subsequently be converted into an estimated firing rate in Hz or, if required by a downstream analysis, into an inferred discrete spike train.
total_array_size = traces.itemsize*traces.size*64/1e9
# If the expected array size is too large for the Colab Notebook, split up for processing
if total_array_size < 1:
spike_prob = cascade.predict( model_name, traces, verbosity=1 )
# Will only be use for large input arrays (long recordings or many neurons)
else:
print("Split analysis into chunks in order to fit into Colab memory.")
# pre-allocate array for results
spike_prob = np.zeros((traces.shape))
# nb of neurons and nb of chunks
nb_neurons = traces.shape[0]
nb_chunks = int(np.ceil(total_array_size/1))
chunks = np.array_split(range(nb_neurons), nb_chunks)
# infer spike rates independently for each chunk
for part_array in range(nb_chunks):
spike_prob[chunks[part_array],:] = cascade.predict( model_name, traces[chunks[part_array],:] )
After running the above code, your machine will start processing the calcium traces and predicting the spiking activity for each neuron. This may take some time, depending on the size of your dataset and the computational resources available.
Once the processing is complete, you will have a 2D NumPy array spike_prob containing the continuous CASCADE spike estimates for each neuron over time. The shape of this array should match that of the input traces, i.e., (neurons, time):
print(f"Shape of CASCADE spike estimate array: {spike_prob.shape}")
Next, we again plot the calcium trace (traces) of the first neuron, this time together with the continuous CASCADE spike estimate stored in spike_prob:
neuron_i = 0
# define a time array based on the frame_rate:
time_vector = np.arange(traces.shape[1]) / frame_rate
# plot the calcium trace and the continuous CASCADE spike estimate:
plt.figure(figsize=(12, 5))
plt.plot(time_vector, traces[neuron_i, :], c="blue", alpha=0.75, label='Calcium trace')
plt.plot(time_vector, spike_prob[neuron_i, :], alpha=0.5, c="orange", label='CASCADE-inferred spikes per frame')
plt.xlabel('Time (seconds)')
plt.ylabel(r'$\Delta F/F$ / inferred spikes per frame')
plt.xlim(0, time_vector[-1])
plt.ylim(-0.25, 2)
plt.title(f'Calcium trace and CASCADE-inferred spiking activity of neuron {neuron_i + 1}')
plt.legend()
plt.show()
Calcium trace and continuous CASCADE spike estimate of the first neuron from the sample dataset. The blue line represents the $\Delta F/F$ calcium trace, while the orange line represents the CASCADE-inferred spike count associated with each imaging frame. The continuous CASCADE output should not be interpreted as the probability of exactly one spike occurring at a given frame. The plot allows a qualitative comparison between the measured calcium signal and the inferred neuronal spiking activity.
Exercise: Again, play around with the neuron index variable neuron_i to plot different neurons and assess your results. What do you notice? How does the inferred spiking activity relate temporally to the calcium trace? Are there calcium transients with little inferred spiking activity, or inferred spike events that are difficult to identify directly from the fluorescence trace?
Of course, we can also use CASCADE’s plotting functions to visualize several calcium traces together with their continuous inferred spiking activity:
N_neurons = 16
neuron_indices = np.random.randint(traces.shape[0], size=N_neurons)
time_axis = plot_dFF_traces(traces,neuron_indices,frame_rate,spike_prob,y_range=(-1.5, 3))
Random sample of calcium traces and continuous CASCADE spike estimates from the sample dataset. The plot shows the fluorescence signals of 16 randomly selected neurons over time together with the corresponding inferred spiking activity. The blue lines represent the $\Delta F/F$ traces, while the orange lines represent the continuous CASCADE estimates of spikes associated with individual imaging frames.
Save predictions to output file
We now have the option, to save our predicted spike probabilities to an output file. Below are a few examples of how to do this:
# create a folder path, that is two levels above the current working directory:
folder_path = os.path.dirname(os.getcwd())
file_name = 'predictions_' + os.path.splitext( os.path.basename(example_file))[0]
save_path = os.path.join(folder_path, file_name)
# save as csv file:
np.savetxt(save_path+'.csv', spike_prob, delimiter=',', fmt='%.6f')
# save as mat file:
#sio.savemat(save_path+'.mat', {'spike_prob':spike_prob})
# save as numpy file:
# np.save(save_path, spike_prob)
What does CASCADE actually return?
The variable name spike_prob used by CASCADE can easily lead to a wrong interpretation. Despite the name, CASCADE does not return the probability that exactly one spike occurred at a given imaging frameꜛ. The output is also not a binary spike train. Instead, CASCADE returns a continuous estimate of the expected number of spikes associated with each imaging time bin. In the CASCADE documentation, this quantity is sometimes called spike probability for brevity, but strictly speaking it is not a probability and is therefore not restricted to values between 0 and 1.
To understand where this representation comes from, it is useful to start from the ground-truth data used to train CASCADE.
From electrophysiological spikes to the CASCADE training target
The ground-truth datasets used to train CASCADE contain simultaneously recorded calcium fluorescence and electrophysiological action potentials. The electrophysiological recording provides a discrete sequence of spike events. After binning the spike times to the temporal resolution of the calcium recording, a simplified example could look like
frame: 497 498 499 500 501 502 503
spikes: 0 0 0 1 0 0 0
Here, one action potential has been assigned to imaging frame 500. Keep in mind that there could also be 2 or more spikes assigned to a single frame, depending on the underlying spike train and the imaging frame rate (e.g., if one time-bin covers 100 ms, several spikes could occur within that interval).
At an imaging rate of 10 Hz, one frame corresponds to
\[\Delta t = \frac{1}{10\ \mathrm{Hz}} = 0.1\ \mathrm{s}.\]The discrete spike sequence itself is not used directly as the regression target of the CASCADE network. Before training, the ground-truth spikes are temporally smoothed, typically with a Gaussian kernel. CASCADE explicitly defines the smoothing model parameter as the standard deviation $\sigma$ of this Gaussian kernel. For example, a model with ...10Hz_smoothing150ms... uses
At a model sampling rate of 10 Hz this corresponds to
\[\sigma_{\mathrm{frames}} = 0.15\,\mathrm{s} \times 10\,\mathrm{frames/s} = 1.5\,\mathrm{frames}.\]Thus, the single discrete ground-truth spike
frame: 497 498 499 500 501 502 503
spikes: 0 0 0 1 0 0 0
is transformed into a smooth profile distributed across neighboring frames:
frame: 497 498 499 500 501 502 503
target: small ... larger peak larger ... small
The exact numerical values depend on the Gaussian kernel and sampling rate, but the important property is that the sum of the smoothed profile still represents approximately one spike. In mathematical form, if the discrete ground-truth spike train is
\[s[k] = \sum_j n_j,\delta_{k,k_j},\]where $n_j$ is the number of spikes assigned to frame $k_j$, then CASCADE uses a smoothed target
\[\tilde{s}[k]= (G_\sigma * s)[k].\]The neural network is trained to predict this continuous quantity $\tilde{s}[k]$ from a temporal window of the calcium fluorescence trace. Therefore, CASCADE does not first infer exact spike times and subsequently smooth them. The network directly predicts the continuous, temporally smoothed spike representation on which it was trained.
The reason for this smoothing is both practical and conceptual. Calcium imaging does not provide millisecond-scale electrophysiological spike timing. The measured fluorescence signal is already temporally blurred by intracellular calcium dynamics, indicator kinetics, imaging frame rate and noise. Training against a perfectly discrete target such as
0 0 0 1 0 0 0
would therefore demand a temporal precision that the calcium signal often cannot support. A slightly smoothed target makes the learning problem more robust and avoids penalizing a prediction extremely strongly merely because an inferred event is shifted by one imaging frame.
CASCADE’s authors therefore describe the smoothing parameter as a trade-off between temporal precision and reliability of spike inference.
What does one CASCADE output value mean?
Suppose CASCADE returns
frame: 497 498 499 500 501 502 503
CASCADE: 0.03 0.10 0.22 0.31 0.22 0.09 0.02
These numbers should not be interpreted as
“There is a 31% probability that exactly one spike occurred at frame 500.”
Instead, each number represents the continuous expected spike count associated with that imaging time bin. The quantity can therefore be interpreted as
\[p[k]=\text{estimated spikes per imaging frame}.\]Because this is not a Bernoulli probability, values larger than 1 are allowed. For example,
\[p[k]=1.7\]means that CASCADE assigns an expected spike count of approximately 1.7 spikes to that imaging bin (in other words, 1.7 spikes per frame). This is biologically possible because one calcium imaging frame can span much more time than one action potential. At 10 Hz, one imaging bin spans approximately 100 ms as we have calculated above. Several action potentials can easily occur within that interval. For example, the underlying discrete spike sequence could in principle look like
frame: 497 498 499 500 501 502 503
spikes: 0 0 0 2 0 0 0
or, over a more active interval,
frame: 497 498 499 500 501 502 503
spikes: 0 3 0 2 0 0 1
The values 2 or 3 do not mean that CASCADE knows the precise millisecond times of these spikes. They only indicate that multiple spikes are assigned to the corresponding imaging bin.
Why can the CASCADE output be summed?
An important consequence of this representation is that the continuous CASCADE output approximately preserves spike count.
If $p[k]$ is the inferred spike count associated with frame $k$, then
\[N_\mathrm{spikes} \approx \sum_k p[k]\]gives the estimated total number of spikes over the analyzed interval.
This property is used explicitly by CASCADE’s own discrete-spike reconstruction algorithm. For example, if the inferred values over a time interval sum to
\[\sum_k p[k] = 7.4,\]CASCADE estimates that this interval contains approximately 7.4 spikes in expectation.
The fractional value does not imply that fractional action potentials physically occurred. It reflects uncertainty in the continuous regression estimate.
From inferred spikes per frame to estimated firing rate
The raw CASCADE output is naturally expressed as
\[\mathrm{spikes/frame}.\]For biological interpretation, however, firing rate in
\[\mathrm{spikes/s} = \mathrm{Hz}\]is usually more familiar.
If the experimental frame rate is $f_s$, then one frame has duration
\[\Delta t = \frac{1}{f_s}.\]For a CASCADE value $p[k]$, the corresponding estimated firing rate is therefore
\[\hat{r}[k]= \frac{p[k]}{\Delta t} = p[k] f_s.\]For example, with $f_s = 10\ \mathrm{Hz}$, and $p[k] = 0.3$, the corresponding estimated firing rate is
\[\hat r[k] = 0.3 \times 10 = 3\ \mathrm{Hz}.\]In Python:
firing_rate = spike_prob * frame_rate
This conversion introduces no additional temporal averaging, binning or smoothing. It is only a linear change of units from inferred spikes per imaging frame to inferred spikes per second.
Consequently, a raster plot of spike_prob and a raster plot of spike_prob * frame_rate have exactly the same spatial and temporal pattern. Only the numerical scale and physical units differ.
Also note that converting the x-axis from frame number to seconds is a completely separate operation:
\[t[k] = \frac{k}{f_s}.\]The x-axis tells us when an imaging bin occurred. The color value or y-value tells us how much inferred spiking activity is associated with that bin. Thus, it is perfectly valid to plot inferred spikes per frame against time in seconds.
Interpreting firing rate conversion
It is important to interpret this conversion correctly. A value of $3\ \mathrm{Hz}$ at a particular 100-ms imaging bin does not mean that three spikes occurred within that bin, nor does it mean that three spikes occurred exactly at the corresponding time point. It is the frame-resolved estimate expressed as a rate normalized to one second.
For the example above, the imaging bin has a duration of $\Delta t = 0.1\ \mathrm{s}$, so a firing-rate estimate of $3\ \mathrm{spikes/s}$ corresponds within this particular bin to
\[3\ \frac{\mathrm{spikes}}{\mathrm{s}} \times 0.1\ \mathrm{s} = 0.3\ \mathrm{spikes}.\]This is exactly the original CASCADE value of $0.3\ \mathrm{spikes/frame}$. Thus,
\[0.3\ \mathrm{spikes/frame} \;\Longleftrightarrow\; 3\ \mathrm{spikes/s}\]at a frame rate of 10 Hz. These are two different units for the same inferred quantity, not two different estimates.
The fractional value $0.3$ should again not be interpreted as 0.3 physical action potentials. CASCADE provides a continuous, temporally smoothed estimate. The value represents the amount of inferred spike count associated with that imaging bin. Likewise, the corresponding value of $3\ \mathrm{Hz}$ represents the local estimated firing rate around that time, not a statement that three discrete action potentials occurred at that exact time.
For example, a sequence such as
time: 9.8 9.9 10.0 10.1 10.2 s
spikes/frame: 0.05 0.20 0.30 0.25 0.10
rate: 0.5 2.0 3.0 2.5 1.0 Hz
contains the same information in the last two rows. Each firing-rate value is simply the corresponding spikes-per-frame value multiplied by 10 Hz. The smooth distribution across neighboring frames reflects the temporally smoothed representation learned by CASCADE. It should not be interpreted as a sequence of fractional physical action potentials.
Estimated firing rate is still frame-resolved
The term instantaneous firing rate is sometimes used for
\[\hat r[k]=p[k]f_s.\]Here, instantaneous should not be interpreted in the mathematical sense of an infinitely precise rate at an exact time point. The estimate is still limited to the temporal resolution of the calcium recording and to the temporal smoothing of the CASCADE model. A more conservative description is therefore frame-resolved estimated firing rate or simply estimated firing rate.
At 10 Hz, the rate estimate is still sampled only every 100 ms.
Recovering discrete spikes
CASCADE also provides an optional procedure via its utility function infer_discrete_spikes()ꜛ to reconstruct a discrete spike train from the continuous output. Conceptually, the task is the inverse of the smoothing operation used during training.
Given the continuous CASCADE estimate $p[k]$, the algorithm searches for a discrete spike sequence
\[s[k] \in {0,1,2,\ldots}\]such that
\[G_\sigma * s\]resembles the continuous CASCADE output as closely as possible.
For example, suppose the continuous estimate is approximately
frame: 497 498 499 500 501 502 503
CASCADE: 0.03 0.10 0.22 0.31 0.22 0.09 0.02
A plausible discrete candidate could be
frame: 497 498 499 500 501 502 503
spikes: 0 0 0 1 0 0 0
The algorithm does not obtain this result by rounding 0.31 to 1 or by applying a simple threshold. Instead, it asks whether placing a discrete spike at frame 500 and smoothing that spike with the same Gaussian kernel produces a continuous profile that resembles the observed CASCADE output.
Schematically,
discrete spike train
↓
Gaussian smoothing
↓
continuous reconstruction
↓
compare with CASCADE output
The algorithm then adjusts the discrete spike configuration in order to improve this reconstruction.
Internally, infer_discrete_spikes() performs several steps:
- It identifies contiguous regions of non-negligible inferred activity.
- For each region, it estimates the approximate number of spikes from the sum of the continuous CASCADE output.
- It uses a Monte-Carlo/Metropolis-like procedure to propose discrete spike locations.
- The proposed discrete spikes are Gaussian-smoothed using the smoothing parameter of the corresponding CASCADE model.
- The smoothed reconstruction is compared with the original continuous CASCADE output.
- Candidate spikes are added, shifted or removed if this decreases the reconstruction error.
- The procedure iterates until a discrete spike configuration is obtained whose smoothed representation approximates the continuous prediction.
In simplified mathematical notation, CASCADE searches approximately for
\[s^\ast = \arg\min_s \left|p - G_\sigma*s\right|,\]subject to $s[k]\in{0,1,2,\ldots}$.
The implementation uses an absolute reconstruction error together with iterative Monte-Carlo initialization, systematic movement of spike positions and pruning of unnecessary spikes.
The utility function is provided in CASCADE’s utils_discrete_spikes.pyꜛ module. Depending on the exact CASCADE repository version and package layout, it can be imported from the corresponding CASCADE module and called as
from cascade2p.utils_discrete_spikes import infer_discrete_spikes
approximations, discrete_spikes = infer_discrete_spikes(
spike_prob,
model_name=model_name,
model_folder="Pretrained_models",
verbosity=1)
The function returns two related objects:
approximationscontains the Gaussian-smoothed reconstruction generated from the inferred discrete spike train.discrete_spikescontains the inferred spike locations, typically as frame indices for each neuron.
For example,
discrete_spikes[0]
might contain
[498, 500, 500, 503, ...]
which corresponds to
frame: 497 498 499 500 501 502 503
spikes: 0 1 0 2 0 0 1
Repeated frame indices are possible because several action potentials may be assigned to the same imaging bin.
The inferred spike times can be converted to seconds using the experimental frame rate:
spike_times_sec = np.asarray(discrete_spikes[neuron_i]) / frame_rate
Importantly, this does not create sub-frame temporal resolution. The spike locations remain restricted to the original imaging-frame grid. At 10 Hz, possible reconstructed spike positions therefore remain separated by approximately 100 ms.
Why does CASCADE not return discrete spikes by default?
The CASCADE authors explicitly cautionꜛ that discrete spike predictions can suggest a temporal precision and certainty that are not justified by many calcium imaging datasets. A smooth estimate such as
0.03 0.10 0.22 0.31 0.22 0.09 0.02
contains uncertainty about the exact spike timing. Turning this into
0 0 0 1 0 0 0
requires the algorithm to commit to a particular discrete location even though neighboring locations may be nearly equally compatible with the calcium signal.
For this reason, CASCADE treats discrete spike reconstruction as optional and recommends it primarily for sufficiently high-quality recordings. The primary output remains the continuous spike-rate/spike-count estimate.
The four main representations
The representations can be summarized as follows:
| Representation | Temporal resolution | Values | Interpretation |
|---|---|---|---|
| Continuous CASCADE output | one imaging frame | continuous, $\geq 0$ | Expected/inferred spike count associated with each imaging frame |
| Estimated firing rate | one imaging frame | continuous, $\geq 0$, in Hz | Same CASCADE information expressed as inferred spikes per second |
| Inferred discrete spikes | one imaging frame | integer $0,1,2,\ldots$ | Optional reconstruction of discrete events assigned to imaging bins |
| Smoothed discrete reconstruction | one imaging frame | continuous | Gaussian-smoothed version of the reconstructed discrete spikes, mainly useful for checking how well the discrete solution reproduces the original CASCADE prediction |
None of these representations has higher temporal sampling than the original calcium recording unless an entirely different inference procedure explicitly introduces such a model.
When should I use which representation?
Continuous CASCADE output
Use the continuous CASCADE output when you want to
- preserve the primary output of the inference model,
- visualize inferred neural activity without imposing additional assumptions about exact spike timing,
- compare relative activity across neurons or time,
- calculate integrated activity or expected spike counts,
- perform downstream analyses that do not explicitly require individual spike events.
For most downstream analyses, this is the safest representation because it retains the information provided directly by CASCADE without forcing uncertain spike timing into discrete events.
Estimated firing rate
Use
firing_rate = spike_prob * frame_rate
when you want to express the same CASCADE inference in the biologically familiar unit Hz.
This is particularly useful when
- firing rate itself is the quantity of interest,
- results should be expressed in spikes per second,
- recordings with different imaging frame rates need to be compared,
- plots should use a familiar physiological quantity rather than spikes per frame.
No information is added or removed by this conversion as long as the frame rate is known.
Inferred discrete spikes
Use the optional discrete reconstruction only when the downstream analysis genuinely requires individual events, for example for
- discrete spike raster plots,
- inter-spike intervals,
- analyses requiring explicit event times,
- spike-count-based methods,
- some forms of burst detection,
- event-triggered analyses defined around individual inferred spikes.
Use this representation cautiously because the exact spike locations are partly determined by the discretization algorithm rather than directly resolved by the calcium recording.
Smoothed reconstruction of the discrete spikes
Use the returned smoothed approximation mainly for
- quality control,
- visualizing how the discretization works,
- checking whether the inferred discrete spike train can reproduce the original continuous CASCADE prediction.
It is normally not the preferred biological output for further analysis.
Rule of thumb
For most analyses:
CASCADE continuous output
|
+--> express directly as inferred spikes/frame
|
+--> multiply by frame rate --> estimated firing rate in Hz
Use the optional discrete-spike reconstruction only if the subsequent analysis specifically requires individual spike events.
In short:
- continuous CASCADE output = primary inference
- estimated firing rate = same information in Hz
- inferred discrete spikes = optional event-based reconstruction with additional assumptions
- smoothed discrete reconstruction = mainly a reconstruction/QC quantity
The distinction is important: discrete spikes are not a more precise version of the continuous CASCADE output. They are one possible discrete explanation of that continuous estimate under the temporal smoothing model used by CASCADE.
How large is the CASCADE signal of a single spike?
The numerical amplitude of the continuous CASCADE output corresponding to a single spike depends on two parameters of the selected model:
- the sampling rate of the model, in Hz, and
- the smoothing parameter, i.e., the standard deviation $\sigma$ of the Gaussian kernel used to smooth the electrophysiological ground-truth spike train during model training.
This is important when interpreting the values returned by CASCADE. A single underlying action potential does not generally appear as a value of 1 at one imaging frame in the continuous CASCADE output. Instead, the unit spike is represented by a Gaussian-smoothed target distributed across several neighboring imaging frames.
For example, consider a single discrete spike:
frame: ... 499 500 501 502 503 ...
spikes: ... 0 0 1 0 0 ...
Before being used as a training target, this discrete event is smoothed with a Gaussian kernel as described above. The standard deviation of this Gaussian is specified by the model’s smoothing parameter. For example, a model name such as Global_EXC_30Hz_smoothing50ms indicates a sampling rate of 30 Hz and a Gaussian smoothing parameter of
In units of imaging frames, this corresponds to
\[\sigma_{\mathrm{frames}} = \sigma_t f_s = 0.05\,\mathrm{s} \times 30\,\mathrm{frames/s} = 1.5\,\mathrm{frames}.\]Thus, the contribution of one spike is distributed across several neighboring frames rather than being concentrated at a single frame.
Simulating the CASCADE representation of a single spike
The CASCADE documentationꜛ provides a simple way to calculate the expected shape of such a single-spike target:
from scipy.ndimage import gaussian_filter
import numpy as np
import matplotlib.pyplot as plt
sampling_rate = 30 # Hz
smoothing = 50 # ms
single_spike = np.zeros(1001)
single_spike[501] = 1
sigma_frames = smoothing / 1e3 * sampling_rate
single_spike_smoothed = gaussian_filter(single_spike.astype(float), sigma=sigma_frames)
gaussian_amplitude = np.max(single_spike_smoothed)
gaussian_fwhm_sec = 2 * np.sqrt(2 * np.log(2)) * smoothing / 1e3
print(f"Gaussian sigma: {sigma_frames:.2f} frames")
print(f"Peak amplitude of a single spike: {gaussian_amplitude:.3f} spikes/frame")
print(f"Gaussian FWHM: {gaussian_fwhm_sec:.3f} s")
We can also visualize this representation:
frame_indices = np.arange(len(single_spike))
time_vector = (frame_indices - 501) / sampling_rate
plt.figure(figsize=(10, 4))
plt.plot(time_vector, single_spike_smoothed)
plt.axvline(0, color="black", linestyle="--", alpha=0.5, label="True spike time")
plt.xlabel("Time relative to spike (s)")
plt.ylabel("Smoothed spike count per frame")
plt.title("CASCADE Training Target for a Single Spike")
plt.xlim(-0.3, 0.3)
plt.legend()
plt.tight_layout()
plt.show()
For the example above, $\sigma=50$ ms at 30 Hz corresponds to $\sigma=1.5$ frames. The peak of the Gaussian is therefore substantially smaller than 1, even though the area under the representation corresponds to one spike.
Why is the peak smaller than one?
Gaussian smoothing redistributes the unit spike over neighboring frames. Conceptually,
discrete spike:
... 0 0 0 1 0 0 0 ...
after Gaussian smoothing:
... small ... larger ... MAX ... larger ... small ...
The exact values depend on the sampling rate and smoothing parameter.
Crucially, smoothing does not turn one spike into a fractional number of spikes. It redistributes the representation of that spike over time. For the discrete Gaussian filter used here, the sum is approximately preserved:
\[\sum_k s[k] \approx 1.\]Thus, the peak height is not the estimated number of spikes represented by the complete event. The spike information is distributed over the temporal extent of the Gaussian.
This also explains why values in the continuous CASCADE output should not be interpreted as probabilities in the usual Bernoulli sense. For example, a value of 0.25 does not simply mean “25% probability that one spike occurred in this frame.” It is part of a continuous, temporally smoothed estimate whose scale depends on the model’s sampling rate and smoothing parameter.
Dependence on the smoothing parameter
A smaller smoothing parameter produces a narrower Gaussian and therefore a larger peak amplitude. A larger smoothing parameter spreads the same spike over more time points and produces a smaller peak:
\[\text{smaller }\sigma \quad\Rightarrow\quad \text{narrower target, larger peak},\] \[\text{larger }\sigma \quad\Rightarrow\quad \text{broader target, smaller peak}.\]The smoothing parameter therefore represents an explicit trade-off in temporal precision. A model with smoothing25ms is trained against temporally sharper spike representations than a model with smoothing150ms.
Practical interpretation
When inspecting a CASCADE output trace, do not ask whether a peak reaches 1. Instead, interpret its magnitude relative to the single-spike scale expected for the selected model.
For example, several closely spaced spikes can produce overlapping Gaussian contributions. Their contributions add, so a burst of spikes can produce substantially larger CASCADE values than the peak expected from a single isolated spike.
The expected single-spike response therefore provides a useful reference scale for interpreting continuous CASCADE predictions. It connects three quantities that otherwise can easily be confused:
discrete electrophysiological spike
↓
Gaussian-smoothed training target
↓
continuous CASCADE prediction
The first is a discrete event, the second is the representation used to train the network, and the third is the network’s estimate of that representation from the measured calcium fluorescence.
Exercise: Load and analyze your own $\Delta F/F$ traces
Now it’s time, the load and plot $\Delta F/F$ traces we have derived from our CaImAn analysis.
First, open the created csv file with the your default spreadsheet software (e.g. Excel, LibreOffice Calc) and check the format of the data. You will notice that the file is structured as a table with the following columns: time, neuron_1, neuron_2, …, neuron_n. Each row corresponds to a time point, and each column corresponds to a neuron. The values in the table are the $\Delta F/F$ values for each neuron at each time point.
Next, we need to load the data into a NumPy array. We can use the np.loadtxt function to load the data from the CSV file:
# let's load the saved csv file from our CaImAn analysis:
csv_file_folder_path = os.path.join(os.getcwd() , '../../01 CaImAn tutorial')
csv_file_path = os.path.join(csv_file_folder_path, 'C_traces.csv')
# use np.loadtxt:
my_traces = np.loadtxt(csv_file_path, delimiter=',', skiprows=1)
Let’s inspect the shape of the loaded data:
print(f"Shape of loaded dF/F traces: {my_traces.shape}")
The shape of (3000, 66) indicates that we have 3000 time points and 66 neurons. Since CASCADE expects the data in the shape (neurons, time), we need to transpose the array to match this format:
my_traces = my_traces.T
print(f"Shape of loaded dF/F traces: {my_traces.shape}")
Next, we need to remove the first column, which contains the time points, and keep only the $\Delta F/F$ values. We can do this by slicing the array:
my_traces = my_traces[1:,:]
print(f"Shape of loaded dF/F traces: {my_traces.shape}")
Also, remember that CASCADE expects the $\Delta F/F$ traces range between 0 and 1 and not in percent (e.g. 0.5 instead of 50%). If your data is in percent, you need to divide the values by 100 – and this is the case for our data:
my_traces = my_traces / 100
Let’s plot the trace of the first neuron to see what it looks like:
# define the frame rate of your own data:
frame_rate = 30 # fps
# define the neuron index to plot:
neuron_i = 0
# define a time array based on the frame_rate:
time_vector = np.arange(my_traces.shape[1]) / frame_rate
# plot the calcium trace for the selected neuron:
plt.figure(figsize=(12, 5))
plt.plot(time_vector, my_traces[neuron_i, :])
plt.xlabel('Time (seconds)')
plt.ylabel(f'\Delta F/F')
plt.xlim(0, time_vector[-1])
plt.title(f'Calcium trace of neuron {neuron_i + 1}')
plt.show()
The first calcium trace from the loaded data.
After ensuring that the data is in the correct format, you can proceed with the running CASCADE on your own data. To do so:
- Scroll up to the cell indicated by the comment
# THIS CELL/STEP IS RESERVED FOR THE EXERCISEand overwrite thetracesvariable with your own data (e.g.traces = my_traces). - Proceed with all subsequent cells as they are. Adjust the frame rate setting according to your data. Also, find a suitable pre-trained model for your data, as described in the previous sections.