This repo contains a C# implementation of the Bayesian state space point process neural decoder. The code uses the TorchSharp library, a pytorch implementation in .NET/C#, and is inspired by the replay_trajectory_classification repository from the Eden-Kramer Lab. It provides a flexible framework for performing neural decoding of observations from spike-train or clusterless mark data based on point processes and Bayesian state space models.
The goal of this software is to perform neural decoding. Bayesian state-space models, in particular, provide a framework to model latent states based on neural activity while point processes capture the probabilistic relationship between neural activity and latent observations.
There are 3 main components of the model: encoder, likelihood, and decoder.
Currently, the software supports the StateSpaceDecoder
, which implements a Bayesian state space decoding model. The Bayesian state space decoder postulates that, given the initial conditions
We need to specify the initial conditions,
For the initial conditions, DiscreteUniformStateSpace
, in which all possible states are equally likely to occur. In this configuration, the state space is bounded by minimum and maximum values for each dimension of our data and gets partitioned into discrete bins based on the number of steps along each dimension. Users must supply values for the minStateSpace
, maxStateSpace
, and stepsStateSpace
, which can be uniquely specified for each dimension. The length of each array corresponds to the number of state space dimension, and all of them must match the user specified stateSpaceDimensions
parameter.
For the transitions, UniformTransitions
, where the latent variable has equal probability of transitioning to any other point in the state space, or RandomWalkTransitions
, where the transitions are constrained by a multivariate normal distribution such that adjacent positions in the state space are more likely to occur. The variance of RandomWalkTransitions
can be specified with the sigmaRandomWalk
parameter that determines the variance of the movement in all dimensions.
For the likelihood measure, PoissonLikelihood
and ClusterlessLikelihood
.
The PoissonLikelihood
is used in conjunction with the SortedSpikeEncoder
and performs the following calculation:
Here,
The ClusterlessLikelihood
method is used in conjunction with the clusterless mark encoder and performs the following computation:
Here, ClusterlessLikelihood
is comprised of two seperate conditional intensity functions. The conditional intensity
The encoder is used to calculate the conditional intensity functions, the rate of events occurring with respect to the latent variable. There are two types of encoders currently supported: SortedSpikeEncoder
and ClusterlessMarkEncoder
In the case of the SortedSpikeEncoder
, the conditional intensity function for each sorted unit takes the form:
Where SortedSpikeEncoder
, the user must specify the nUnits
parameter to allocates the appropriate number of unit estimators at runtime.
For the ClusterlessMarkEncoder
, we use a marked point process procedure where each spike/event has an associated feature vector or set of marks. In general, marks can be anything associated with a spike event (i.e. spike width, maximum amplitude, etc). The mark conditional intensity function is:
Where
Next, we define the channel conditional intensity function as:
Where ClusterlessMarkEncoder
, users must specify the markDimensions
and markChannels
parameters which define the number of mark features associated with each spike event and the number of recording channels, respectively.
We approximate the probability distributions KernelDensity
and KernelCompression
.
The KernelDensity
estimation method can be formalized as follows:
The probability distribution KernelDensity
method is more accurate compared to the KernelCompression
method, but requires more memory and computation time as the number of observations increases.
At the cost of a small amount of accuracy, the KernelCompression
estimation method is faster than the KernelDensity
method with greater observations and requires less memory. It works by computing a gaussian mixture model to represent
Where each kernel component
The KernelCompression
algorithm uses a kernel merging procedure to determine whether the observed data point should lead to the creation of a new kernel component or whether the data point should be used to update the paremeters of the closest existing kernel. First, the mahalanobis distance is calculated between existing kernels and the new data point. The distance is evaluated against the user specified distanceThreshold
parameter, such that if the distance to the closest kernel is greater than the distanceThreshold
, then the data point is used to create a new kernel. If the distance is less than this, the closest kernel is updated using a moment matching method. The new weight of the component is updated as follows:
And the new
Since only the diagonal of the covariance matrix is used in the kernel density estimate, we only update the diagonal elements of the matrix using:
Users of the package must specify the bandwidth parameters used for density estimation. For the distribution, observationBandwidth
parameter, where a unique bandwidth can be set for each dimension. Again, the length of this array must be equal to the number of stateSpaceDimensions
defined above. For both the SortedSpikeEncoder
and ClusterlessMarkEncoder
, the observationBandwidth
parameter is used to compute the distributions ClusterlessMarkEncoder
also takes the markBandwidth
parameter which is used for calculating the distribution, markDimensions
parameter.
-
Install the .NET SDK: Download the .NET SDK if you haven't already.
-
Clone the repository:
git clone https://github.com/ncguilbeault/PointProcessDecoder.cs
cd PointProcessDecoder
- Restore dependencies:
dotnet restore
- Build the solution:
dotnet build
Here is a minimal example of how to use the decoder in a console app:
using PointProcessDecoder.Core;
using PointProcessDecoder.Plot;
using PointProcessDecoder.Simulation;
namespace DecoderDemo
{
class Program
{
static void Main(string[] args)
{
// 1. Load data.
// Example: Generate simulated data
(position, spikeCounts) = Simulation.Utilities.InitializeSimulation1D(
numNeurons: 40,
placeFieldRadius: 0.8,
firingThreshold: 0.2
);
// 2. Create the model and select parameters.
var model = new PointProcessModel(
estimationMethod: Core.Estimation.EstimationMethod.KernelDensity,
transitionsType: Core.Transitions.TransitionsType.Uniform,
encoderType: Core.Encoder.EncoderType.SortedSpikeEncoder,
decoderType: Core.Decoder.DecoderType.StateSpaceDecoder,
stateSpaceType: Core.StateSpace.StateSpaceType.DiscreteUniformStateSpace,
likelihoodType: Core.Likelihood.LikelihoodType.Poisson,
minStateSpace: [0],
maxStateSpace: [120],
stepsStateSpace: [50],
observationBandwidth: [5],
stateSpaceDimensions: 1,
nUnits: 40
);
// 4. Encode neural data and observations
model.Encode(spikeCounts, position);
// 5. Predict or decode observations from spikes
var prediction = model.Decode(spikeCounts);
// 6. Display results
Heatmap plotPrediction = new(
xMin: 0,
xMax: steps * cycles,
yMin: 0,
yMax: 120,
title: "Prediction"
);
plotPrediction.Show<float>(
prediction
);
plotPrediction.Save(png: true);
}
}
}
This work is based on several previously published works. If you use this software, consider citing the following:
-
Denovellis, E. L., Gillespie, A. K., Coulter, M. E., Sosa, M., Chung, J. E., Eden, U. T., & Frank, L. M. (2021). Hippocampal replay of experience at real-world speeds. Elife, 10, e64505.
-
Sodkomkham, D., Ciliberti, D., Wilson, M. A., Fukui, K. I., Moriyama, K., Numao, M., & Kloosterman, F. (2016). Kernel density compression for real-time Bayesian encoding/decoding of unsorted hippocampal spikes. Knowledge-Based Systems, 94, 1-12.
Contributions and feedback are welcome!