diff --git a/Bonsai.ML.sln b/Bonsai.ML.sln
index c5a91b13..bf116ccc 100644
--- a/Bonsai.ML.sln
+++ b/Bonsai.ML.sln
@@ -1,4 +1,4 @@
-
+
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
@@ -30,6 +30,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bonsai.ML.LinearDynamicalSy
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bonsai.ML.HiddenMarkovModels.Design", "src\Bonsai.ML.HiddenMarkovModels.Design\Bonsai.ML.HiddenMarkovModels.Design.csproj", "{FC395DDC-62A4-4E14-A198-272AB05B33C7}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bonsai.ML.NeuralDecoder", "src\Bonsai.ML.NeuralDecoder\Bonsai.ML.NeuralDecoder.csproj", "{CE083548-26CB-4CF6-AE51-D7E32AE7377A}"
+EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Bonsai.ML.NeuralDecoder.Design", "src\Bonsai.ML.NeuralDecoder.Design\Bonsai.ML.NeuralDecoder.Design.csproj", "{D2CECE2F-CE7C-41BC-9888-EA53493D64D6}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -60,18 +64,26 @@ Global
{39A4414F-52B1-42D7-82FA-E65DAD885264}.Debug|Any CPU.Build.0 = Debug|Any CPU
{39A4414F-52B1-42D7-82FA-E65DAD885264}.Release|Any CPU.ActiveCfg = Release|Any CPU
{39A4414F-52B1-42D7-82FA-E65DAD885264}.Release|Any CPU.Build.0 = Release|Any CPU
- {A135C7DB-EA50-4FC6-A6CB-6A5A5CC5FA13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {A135C7DB-EA50-4FC6-A6CB-6A5A5CC5FA13}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {A135C7DB-EA50-4FC6-A6CB-6A5A5CC5FA13}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {A135C7DB-EA50-4FC6-A6CB-6A5A5CC5FA13}.Release|Any CPU.Build.0 = Release|Any CPU
- {17DF50BE-F481-4904-A4C8-5DF9725B2CA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {17DF50BE-F481-4904-A4C8-5DF9725B2CA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {17DF50BE-F481-4904-A4C8-5DF9725B2CA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {17DF50BE-F481-4904-A4C8-5DF9725B2CA1}.Release|Any CPU.Build.0 = Release|Any CPU
- {FC395DDC-62A4-4E14-A198-272AB05B33C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {FC395DDC-62A4-4E14-A198-272AB05B33C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {FC395DDC-62A4-4E14-A198-272AB05B33C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {FC395DDC-62A4-4E14-A198-272AB05B33C7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {A135C7DB-EA50-4FC6-A6CB-6A5A5CC5FA13}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {A135C7DB-EA50-4FC6-A6CB-6A5A5CC5FA13}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {A135C7DB-EA50-4FC6-A6CB-6A5A5CC5FA13}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {A135C7DB-EA50-4FC6-A6CB-6A5A5CC5FA13}.Release|Any CPU.Build.0 = Release|Any CPU
+ {17DF50BE-F481-4904-A4C8-5DF9725B2CA1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {17DF50BE-F481-4904-A4C8-5DF9725B2CA1}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {17DF50BE-F481-4904-A4C8-5DF9725B2CA1}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {17DF50BE-F481-4904-A4C8-5DF9725B2CA1}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FC395DDC-62A4-4E14-A198-272AB05B33C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FC395DDC-62A4-4E14-A198-272AB05B33C7}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FC395DDC-62A4-4E14-A198-272AB05B33C7}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FC395DDC-62A4-4E14-A198-272AB05B33C7}.Release|Any CPU.Build.0 = Release|Any CPU
+ {CE083548-26CB-4CF6-AE51-D7E32AE7377A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {CE083548-26CB-4CF6-AE51-D7E32AE7377A}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {CE083548-26CB-4CF6-AE51-D7E32AE7377A}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {CE083548-26CB-4CF6-AE51-D7E32AE7377A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D2CECE2F-CE7C-41BC-9888-EA53493D64D6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D2CECE2F-CE7C-41BC-9888-EA53493D64D6}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D2CECE2F-CE7C-41BC-9888-EA53493D64D6}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D2CECE2F-CE7C-41BC-9888-EA53493D64D6}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -84,8 +96,10 @@ Global
{81DB65B3-EA65-4947-8CF1-0E777324C082} = {461FE3E2-21C4-47F9-8405-DF72326AAB2B}
{BAD0A733-8EFB-4EAF-9648-9851656AF7FF} = {12312384-8828-4786-AE19-EFCEDF968290}
{39A4414F-52B1-42D7-82FA-E65DAD885264} = {12312384-8828-4786-AE19-EFCEDF968290}
- {A135C7DB-EA50-4FC6-A6CB-6A5A5CC5FA13} = {12312384-8828-4786-AE19-EFCEDF968290}
- {17DF50BE-F481-4904-A4C8-5DF9725B2CA1} = {12312384-8828-4786-AE19-EFCEDF968290}
+ {A135C7DB-EA50-4FC6-A6CB-6A5A5CC5FA13} = {12312384-8828-4786-AE19-EFCEDF968290}
+ {17DF50BE-F481-4904-A4C8-5DF9725B2CA1} = {12312384-8828-4786-AE19-EFCEDF968290}
+ {CE083548-26CB-4CF6-AE51-D7E32AE7377A} = {12312384-8828-4786-AE19-EFCEDF968290}
+ {D2CECE2F-CE7C-41BC-9888-EA53493D64D6} = {12312384-8828-4786-AE19-EFCEDF968290}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B6468F13-97CD-45E0-9E1E-C122D7F1E09F}
diff --git a/Directory.Build.props b/Directory.Build.props
index 11390a0b..e9805deb 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -14,7 +14,7 @@
icon.png
true
git
- 0.3.1
+ 0.4.0
12.0
diff --git a/README.md b/README.md
index cd875504..f3998f82 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,12 @@ Facilitates inference using Hidden Markov Models (HMMs). It interfaces with the
### Bonsai.ML.HiddenMarkovModels.Design
Visualizers and editor features for the HiddenMarkovModels package.
+### Bonsai.ML.NeuralDecoder
+Enables online neural decoding of spike sorted or clusterless neural activity. It interfaces with the [bayesian-neural-decoder](https://github.com/ncguilbeault/bayesian-neural-decoder) package using the [Bonsai - Python Scripting](https://github.com/bonsai-rx/python-scripting) library. The neural decoder consists of a bayesian state-space point process model to decode sorted spikes or clusterless neural activity. The technical details describing the models implementation are described in: Denovellis, E.L., Gillespie, A.K., Coulter, M.E., et al. Hippocampal replay of experience at real-world speeds. eLife 10, e64505 (2021). https://doi.org/10.7554/eLife.64505.
+
+### Bonsai.ML.NeuralDecoder.Design
+Visualizers for the Neural Decoder package.
+
> [!NOTE]
> Bonsai.ML packages can be installed through Bonsai's integrated package manager and are generally ready for immediate use. However, some packages may require additional installation steps. Refer to the specific package section for detailed installation guides and documentation.
diff --git a/docs/articles/NeuralDecoder/nd-getting-started.md b/docs/articles/NeuralDecoder/nd-getting-started.md
new file mode 100644
index 00000000..1e098c12
--- /dev/null
+++ b/docs/articles/NeuralDecoder/nd-getting-started.md
@@ -0,0 +1,26 @@
+# Getting Started
+
+To get started using the Bonsai.ML.NeuralDecoder package, please read below or get started on the demo in the [Neural Decoding example guide](~/examples/README.md).
+
+## Algorithm
+
+The neural decoder consists of a bayesian state space point process model. With this model, latent variables such as an animals position can be decoded from neural activity. To read more about the theory behind the model and how the algorithm works, we refer the reader to: Denovellis, E.L., Gillespie, A.K., Coulter, M.E., et al. Hippocampal replay of experience at real-world speeds. eLife 10, e64505 (2021). https://doi.org/10.7554/eLife.64505.
+
+## Installation
+
+### Python
+
+To install the python package needed to use the package, run the following:
+
+```
+cd \path\to\examples\NeuralDecoding\PositionDecodingFromHippocampus
+python -m venv .venv
+.\.venv\Scripts\activate
+pip install git+https://github.com/ncguilbeault/bayesian-neural-decoder.git
+```
+
+You can test whether the installation was successful by launching python and running
+
+```python
+import bayesian_neural_decoder
+```
diff --git a/docs/articles/toc.yml b/docs/articles/toc.yml
index e22b0b80..f2bdfb6b 100644
--- a/docs/articles/toc.yml
+++ b/docs/articles/toc.yml
@@ -13,4 +13,7 @@
- name: Overview
href: HiddenMarkovModels/hmm-overview.md
- name: Getting Started
- href: HiddenMarkovModels/hmm-getting-started.md
\ No newline at end of file
+ href: HiddenMarkovModels/hmm-getting-started.md
+- name: NeuralDecoder
+- name: Getting Started
+ href: NeuralDecoder/nd-getting-started.md
\ No newline at end of file
diff --git a/src/Bonsai.ML.Design/HeatMapSeriesOxyPlotBase.cs b/src/Bonsai.ML.Design/HeatMapSeriesOxyPlotBase.cs
index e9ddf640..ff410e13 100644
--- a/src/Bonsai.ML.Design/HeatMapSeriesOxyPlotBase.cs
+++ b/src/Bonsai.ML.Design/HeatMapSeriesOxyPlotBase.cs
@@ -63,6 +63,11 @@ public class HeatMapSeriesOxyPlotBase : UserControl
///
public StatusStrip StatusStrip => statusStrip;
+ ///
+ /// Gets the plot model.
+ ///
+ public PlotModel Model => model;
+
///
/// Constructor of the TimeSeriesOxyPlotBase class.
/// Requires a line series name and an area series name.
@@ -295,7 +300,42 @@ public void UpdateHeatMapSeries(double[,] data)
}
///
- /// Method to update the heatmap series with new data.
+ /// Method to update the heatmap x axis.
+ ///
+ ///
+ ///
+ public void UpdateHeatMapXAxis(double x0, double x1)
+ {
+ heatMapSeries.X0 = x0;
+ heatMapSeries.X1 = x1;
+ }
+
+ ///
+ /// Method to update the heatmap y axis.
+ ///
+ ///
+ ///
+ public void UpdateHeatMapYAxis(double y0, double y1)
+ {
+ heatMapSeries.Y0 = y0;
+ heatMapSeries.Y1 = y1;
+ }
+
+ ///
+ /// Method to update the heatmap axes.
+ ///
+ ///
+ ///
+ ///
+ ///
+ public void UpdateHeatMapAxes(double x0, double x1, double y0, double y1)
+ {
+ UpdateHeatMapXAxis(x0, x1);
+ UpdateHeatMapYAxis(y0, y1);
+ }
+
+ ///
+ /// Method to update the heatmap series data and axes.
///
/// The minimum x value.
/// The maximum x value.
@@ -304,10 +344,7 @@ public void UpdateHeatMapSeries(double[,] data)
/// The data to be displayed.
public void UpdateHeatMapSeries(double x0, double x1, double y0, double y1, double[,] data)
{
- heatMapSeries.X0 = x0;
- heatMapSeries.X1 = x1;
- heatMapSeries.Y0 = y0;
- heatMapSeries.Y1 = y1;
+ UpdateHeatMapAxes(x0, x1, y0, y1);
heatMapSeries.Data = data;
}
diff --git a/src/Bonsai.ML.Design/UnidimensionalArrayTimeSeriesVisualizer.cs b/src/Bonsai.ML.Design/UnidimensionalArrayTimeSeriesVisualizer.cs
new file mode 100644
index 00000000..25d11460
--- /dev/null
+++ b/src/Bonsai.ML.Design/UnidimensionalArrayTimeSeriesVisualizer.cs
@@ -0,0 +1,152 @@
+using System;
+using System.Collections.Generic;
+using System.Windows.Forms;
+using Bonsai;
+using Bonsai.Design;
+
+[assembly: TypeVisualizer(typeof(Bonsai.ML.Design.UnidimensionalArrayTimeSeriesVisualizer),
+ Target = typeof(double[]))]
+
+namespace Bonsai.ML.Design
+{
+ ///
+ /// Provides a type visualizer to display unidimensional array data as a heatmap time series.
+ ///
+ public class UnidimensionalArrayTimeSeriesVisualizer : DialogTypeVisualizer
+ {
+ ///
+ /// Gets or sets the selected index of the color palette to use.
+ ///
+ public int PaletteSelectedIndex { get; set; }
+
+ ///
+ /// Gets or sets the selected index of the render method to use.
+ ///
+ public int RenderMethodSelectedIndex { get; set; }
+
+ private HeatMapSeriesOxyPlotBase plot;
+
+ ///
+ /// Gets the plot control.
+ ///
+ public HeatMapSeriesOxyPlotBase Plot => plot;
+
+ ///
+ /// Gets or sets the current count of data points.
+ ///
+ public int CurrentCount { get; set; }
+
+ ///
+ /// Gets or sets the current length of the data array.
+ ///
+ public int CurrentArrayLength { get; set; }
+
+ private int _capacity = 100;
+
+ ///
+ /// Gets or sets the integer value that determines how many data points should be shown along the x axis.
+ ///
+ public int Capacity
+ {
+ get => _capacity;
+ set
+ {
+ _capacity = value;
+ }
+ }
+
+ private List dataList = new();
+
+ ///
+ public override void Load(IServiceProvider provider)
+ {
+ plot = new HeatMapSeriesOxyPlotBase(PaletteSelectedIndex, RenderMethodSelectedIndex)
+ {
+ Dock = DockStyle.Fill,
+ };
+
+ plot.PaletteComboBoxValueChanged += PaletteIndexChanged;
+ plot.RenderMethodComboBoxValueChanged += RenderMethodIndexChanged;
+
+ var capacityLabel = new ToolStripLabel
+ {
+ Text = "Capacity:",
+ AutoSize = true
+ };
+ var capacityValue = new ToolStripLabel
+ {
+ Text = Capacity.ToString(),
+ AutoSize = true
+ };
+
+ plot.StatusStrip.Items.AddRange([
+ capacityLabel,
+ capacityValue
+ ]);
+
+ var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService));
+ if (visualizerService != null)
+ {
+ visualizerService.AddControl(plot);
+ }
+ }
+
+ ///
+ public override void Show(object value)
+ {
+ var array = (double[])value;
+
+ if (dataList.Count < Capacity)
+ {
+ dataList.Add(array);
+ CurrentCount = dataList.Count;
+ }
+ else
+ {
+ while (dataList.Count >= Capacity)
+ {
+ dataList.RemoveAt(0);
+ }
+ dataList.Add(array);
+ }
+
+ if (array.Length != CurrentArrayLength)
+ {
+ CurrentArrayLength = array.Length;
+ plot.UpdateHeatMapYAxis(-0.5, CurrentArrayLength - 0.5);
+ }
+
+ var mdarray = new double[CurrentCount, CurrentArrayLength];
+ for (int i = 0; i < CurrentCount; i++)
+ {
+ for (int j = 0; j < CurrentArrayLength; j++)
+ {
+ mdarray[i, j] = dataList[i][j];
+ }
+ }
+
+ plot.UpdateHeatMapSeries(mdarray);
+ plot.UpdateHeatMapXAxis(-0.5, CurrentCount - 0.5);
+ plot.UpdatePlot();
+ }
+
+ ///
+ public override void Unload()
+ {
+ if (!plot.IsDisposed)
+ {
+ plot.Dispose();
+ }
+ }
+
+ private void PaletteIndexChanged(object sender, EventArgs e)
+ {
+ PaletteSelectedIndex = plot.PaletteComboBox.SelectedIndex;
+ }
+
+ private void RenderMethodIndexChanged(object sender, EventArgs e)
+ {
+ RenderMethodSelectedIndex = plot.RenderMethodComboBox.SelectedIndex;
+ }
+ }
+}
diff --git a/src/Bonsai.ML.NeuralDecoder.Design/Bonsai.ML.NeuralDecoder.Design.csproj b/src/Bonsai.ML.NeuralDecoder.Design/Bonsai.ML.NeuralDecoder.Design.csproj
new file mode 100644
index 00000000..edc72c09
--- /dev/null
+++ b/src/Bonsai.ML.NeuralDecoder.Design/Bonsai.ML.NeuralDecoder.Design.csproj
@@ -0,0 +1,18 @@
+
+
+ Bonsai.ML.NeuralDecoder.Design
+ A Bonsai package for visualizing decoded neural activity.
+ Bonsai Rx ML Neural Decoder Design
+ net472
+ true
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Bonsai.ML.NeuralDecoder.Design/PosteriorVisualizer.cs b/src/Bonsai.ML.NeuralDecoder.Design/PosteriorVisualizer.cs
new file mode 100644
index 00000000..175b60ad
--- /dev/null
+++ b/src/Bonsai.ML.NeuralDecoder.Design/PosteriorVisualizer.cs
@@ -0,0 +1,145 @@
+using Bonsai;
+using Bonsai.Design;
+using System;
+using System.Collections.Generic;
+using OxyPlot.Series;
+using OxyPlot;
+using Bonsai.ML.Design;
+using System.Windows.Forms;
+using System.Reactive.Linq;
+using System.Linq;
+using System.Xml.Serialization;
+
+[assembly: TypeVisualizer(typeof(Bonsai.ML.NeuralDecoder.Design.PosteriorVisualizer),
+ Target = typeof(Bonsai.ML.NeuralDecoder.Posterior))]
+
+namespace Bonsai.ML.NeuralDecoder.Design
+{
+ ///
+ /// Provides a mashup visualizer to display the posterior of the neural decoder.
+ ///
+ public class PosteriorVisualizer : MashupVisualizer
+ {
+ private UnidimensionalArrayTimeSeriesVisualizer visualizer;
+ private LineSeries lineSeries;
+ private List argMaxVals = new();
+ private double[] valueCenters = null;
+ private double[] valueRange = null;
+
+ ///
+ /// Gets the underlying heatmap plot.
+ ///
+ public HeatMapSeriesOxyPlotBase Plot => visualizer.Plot;
+
+ ///
+ /// Gets the capacity of the visualizer.
+ ///
+ public int Capacity => visualizer.Capacity;
+
+ ///
+ /// Gets the current count of data points.
+ ///
+ public int CurrentCount => visualizer.CurrentCount;
+
+ ///
+ /// Gets the values of the Y axis.
+ ///
+ [XmlIgnore]
+ public double[] ValueCenters => valueCenters;
+
+ ///
+ /// Gets the range of values mapped to the values of the Y axis.
+ ///
+ [XmlIgnore]
+ public double[] ValueRange => valueRange;
+
+ ///
+ public override void Load(IServiceProvider provider)
+ {
+ visualizer = new UnidimensionalArrayTimeSeriesVisualizer()
+ {
+ PaletteSelectedIndex = 1,
+ RenderMethodSelectedIndex = 1
+ };
+
+ visualizer.Load(provider);
+
+ lineSeries = new LineSeries()
+ {
+ Title = "Maximum Posterior",
+ Color = OxyColors.SkyBlue
+ };
+ visualizer.Plot.Model.Series.Add(lineSeries);
+
+ base.Load(provider);
+ }
+
+ ///
+ public override void Show(object value)
+ {
+ Posterior posterior = (Posterior)value;
+ if (posterior == null)
+ {
+ return;
+ }
+
+ if (valueCenters == null)
+ {
+ valueCenters = posterior.ValueCenters;
+ }
+
+ if (valueRange == null)
+ {
+ valueRange = posterior.ValueRange;
+ }
+
+ var data = posterior.Data;
+ var argMax = posterior.ArgMax;
+
+ while (argMaxVals.Count >= Capacity)
+ {
+ argMaxVals.RemoveAt(0);
+ }
+
+ argMaxVals.Add(valueCenters[argMax]);
+ lineSeries.Points.Clear();
+ var count = argMaxVals.Count;
+
+ for (int i = 0; i < count; i++)
+ {
+ lineSeries.Points.Add(new DataPoint(i, argMaxVals[i]));
+ }
+
+ visualizer.Show(data);
+ Plot.UpdateHeatMapYAxis(valueRange[0], valueRange[valueRange.Length - 1]);
+ }
+
+ ///
+ public override void Unload()
+ {
+ visualizer.Unload();
+ base.Unload();
+ }
+
+ ///
+ public override IObservable