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 Visualize(IObservable> source, IServiceProvider provider) + { + if (provider.GetService(typeof(IDialogTypeVisualizerService)) is not Control visualizerControl) + { + return source; + } + + var mergedSource = source.SelectMany(xs => xs + .Do(value => Show(value))); + + var mashupSourceStreams = Observable.Merge( + MashupSources.Select(mashupSource => + mashupSource.Source.Output.SelectMany(xs => xs + .Do(value => mashupSource.Visualizer.Show(value))))); + + return Observable.Merge(mergedSource, mashupSourceStreams) + .ObserveOn(visualizerControl); + + } + } +} diff --git a/src/Bonsai.ML.NeuralDecoder.Design/Properties/launchSettings.json b/src/Bonsai.ML.NeuralDecoder.Design/Properties/launchSettings.json new file mode 100644 index 00000000..b48bcfa9 --- /dev/null +++ b/src/Bonsai.ML.NeuralDecoder.Design/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Bonsai": { + "commandName": "Executable", + "executablePath": "$(registry:HKEY_CURRENT_USER\\Software\\Bonsai Foundation\\Bonsai@InstallDir)Bonsai.exe", + "commandLineArgs": "--lib:\"$(TargetDir).\"", + "nativeDebugging": true + } + } +} \ No newline at end of file diff --git a/src/Bonsai.ML.NeuralDecoder.Design/TruePositionOverlay.cs b/src/Bonsai.ML.NeuralDecoder.Design/TruePositionOverlay.cs new file mode 100644 index 00000000..103c726b --- /dev/null +++ b/src/Bonsai.ML.NeuralDecoder.Design/TruePositionOverlay.cs @@ -0,0 +1,88 @@ +using Bonsai; +using Bonsai.Design; +using Bonsai.Design.Visualizers; +using System; +using System.Collections.Generic; +using OxyPlot.Series; +using OxyPlot; +using Bonsai.ML.Design; + +[assembly: TypeVisualizer(typeof(Bonsai.ML.NeuralDecoder.Design.TruePositionOverlay), + Target = typeof(MashupSource))] + +namespace Bonsai.ML.NeuralDecoder.Design +{ + /// + /// Class that overlays the true + /// + public class TruePositionOverlay : DialogTypeVisualizer + { + private PosteriorVisualizer visualizer; + private LineSeries lineSeries; + private List data = new(); + private string defaultYAxisTitle; + private HeatMapSeriesOxyPlotBase plot; + + /// + public override void Load(IServiceProvider provider) + { + var service = provider.GetService(typeof(MashupVisualizer)); + visualizer = (PosteriorVisualizer)service; + plot = visualizer.Plot; + + lineSeries = new LineSeries() + { + Title = "True Position", + Color = OxyColors.Goldenrod + }; + + plot.Model.Series.Add(lineSeries); + + plot.Model.Updated += (sender, e) => + { + defaultYAxisTitle = plot.Model.DefaultYAxis.Title; + plot.Model.DefaultYAxis.Title = "Position"; + }; + + } + + /// + public override void Show(object value) + { + var position = (double)value; + if (position == double.NaN) + { + return; + } + + data.Add(position); + + var currentCount = visualizer.CurrentCount; + var valueRange = visualizer.ValueRange; + var valueCenters = visualizer.ValueCenters; + + while (data.Count > currentCount) + { + data.RemoveAt(0); + } + lineSeries.Points.Clear(); + + var count = data.Count; + for (int i = 0; i < count; i++) + { + var closestIndex = Array.BinarySearch(valueRange, data[i]); + if (closestIndex < 0) + { + closestIndex = ~closestIndex; + } + lineSeries.Points.Add(new DataPoint(currentCount - count + i, valueCenters[closestIndex])); + } + } + + /// + public override void Unload() + { + plot.Model.DefaultYAxis.Title = defaultYAxisTitle; + } + } +} \ No newline at end of file diff --git a/src/Bonsai.ML.NeuralDecoder/Bonsai.ML.NeuralDecoder.csproj b/src/Bonsai.ML.NeuralDecoder/Bonsai.ML.NeuralDecoder.csproj new file mode 100644 index 00000000..01805ef6 --- /dev/null +++ b/src/Bonsai.ML.NeuralDecoder/Bonsai.ML.NeuralDecoder.csproj @@ -0,0 +1,18 @@ + + + Bonsai.ML.NeuralDecoder + A Bonsai package for decoding neural activity. + Bonsai Rx ML Neural Decoder + net472;netstandard2.0 + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Bonsai.ML.NeuralDecoder/ImportDecoderModule.bonsai b/src/Bonsai.ML.NeuralDecoder/ImportDecoderModule.bonsai new file mode 100644 index 00000000..5b9c1ba9 --- /dev/null +++ b/src/Bonsai.ML.NeuralDecoder/ImportDecoderModule.bonsai @@ -0,0 +1,29 @@ + + + Import the Bayesian neural decoder module. + + + + Source1 + + + + from bayesian_neural_decoder import * + + + + DecoderModule + + + + + + + + + + \ No newline at end of file diff --git a/src/Bonsai.ML.NeuralDecoder/LoadClusterlessSpikeDecoder.bonsai b/src/Bonsai.ML.NeuralDecoder/LoadClusterlessSpikeDecoder.bonsai new file mode 100644 index 00000000..6d7dec8c --- /dev/null +++ b/src/Bonsai.ML.NeuralDecoder/LoadClusterlessSpikeDecoder.bonsai @@ -0,0 +1,106 @@ + + + Load a pickle object into an instance of the clusterless spike decoder class and set it to the referenced python variable. + + + + + + + + decoder + + + + decoder + + + Name + + + + + + FileName + ../../../datasets/decoder_data/clusterless_spike_decoder.pkl + + + + + + + + + {0}=ClusterlessSpikeDecoder.load("{1}") + Item1,Item2 + + + + + + + + DecoderModule + + + + + + + + + + + + + + + + + + + DecoderModule + + + + + + + + + hmm + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Bonsai.ML.NeuralDecoder/LoadDecodedResultsFromFile.bonsai b/src/Bonsai.ML.NeuralDecoder/LoadDecodedResultsFromFile.bonsai new file mode 100644 index 00000000..878e34a5 --- /dev/null +++ b/src/Bonsai.ML.NeuralDecoder/LoadDecodedResultsFromFile.bonsai @@ -0,0 +1,134 @@ + + + Loads the decoded results from a pickle file and emits the animals position, spikes, predictions, and position bins at a specified sampling frequency. + + + + + + + FileName + ../../../datasets/decoder_data/clusterless_spike_decoding_results.pkl + + + + + + data=DataLoader.load(results_path="{0}") + + + + + + + + + DecoderModule + + + + + + + + + + + + + + Iterate + + + + + + + + PT0S + PT0.1S + + + + + + + DecoderModule + + + + + + + + + iterator = DataIterator(data) + + + + + + + + + + DecoderModule + + + + + + + + + iterator.next() + + + + it[0] + + + new(it[0] as Position, it[1] as Spikes, it[2] as Predictions, it[3] as PositionBins) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Bonsai.ML.NeuralDecoder/LoadSortedSpikeDecoder.bonsai b/src/Bonsai.ML.NeuralDecoder/LoadSortedSpikeDecoder.bonsai new file mode 100644 index 00000000..9add7c73 --- /dev/null +++ b/src/Bonsai.ML.NeuralDecoder/LoadSortedSpikeDecoder.bonsai @@ -0,0 +1,106 @@ + + + Load a pickle object into an instance of the sorted spike decoder class and set it to the referenced python variable. + + + + + + + + decoder + + + + decoder + + + Name + + + + + + FileName + ../../../datasets/decoder_data/sorted_spike_decoder.pkl + + + + + + + + + {0}=SortedSpikeDecoder.load("{1}") + Item1,Item2 + + + + + + + + DecoderModule + + + + + + + + + + + + + + + + + + + DecoderModule + + + + + + + + + hmm + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Bonsai.ML.NeuralDecoder/PerformInference.bonsai b/src/Bonsai.ML.NeuralDecoder/PerformInference.bonsai new file mode 100644 index 00000000..cf071493 --- /dev/null +++ b/src/Bonsai.ML.NeuralDecoder/PerformInference.bonsai @@ -0,0 +1,90 @@ + + + Performs inference using the Bayesian neural decoder. + + + + Source1 + + + + + + DecoderModule + + + + + + + + + spikes + + + + + + + decoder + + + Name + + + + + + {0}.decode(spikes) + Item2 + + + + + + + + DecoderModule + + + + + + + + + decoder.decode(spikes) + + + + + 0 + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Bonsai.ML.NeuralDecoder/Posterior.cs b/src/Bonsai.ML.NeuralDecoder/Posterior.cs new file mode 100644 index 00000000..bbc8051f --- /dev/null +++ b/src/Bonsai.ML.NeuralDecoder/Posterior.cs @@ -0,0 +1,80 @@ +using System.ComponentModel; +using System; +using System.Reactive.Linq; +using System.Linq; +using Python.Runtime; +using Bonsai.ML.Python; + +namespace Bonsai.ML.NeuralDecoder; + +/// +/// Transforms the input sequence of Python objects into a sequence of instances. +/// +[Combinator] +[Description("Transforms the input sequence of Python objects into a sequence of Posterior instances.")] +[WorkflowElementCategory(ElementCategory.Transform)] +public class Posterior +{ + /// + /// The data. + /// + public double[] Data { get; set; } + + /// + /// The argmax. + /// + public int ArgMax { get; set; } + + /// + /// An optional mapping of the data to a value range. + /// + public double[] ValueRange { get; set; } + + /// + /// The value centers. + /// + public double[] ValueCenters { get; set; } + + /// + /// Initializes a new instance of the class. + /// + public Posterior() + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + public Posterior(double[] data, int argMax, double[] valueRange = null) + { + Data = data; + ArgMax = argMax; + ValueRange = valueRange ?? Enumerable.Range(0, data.Length).Select(i => (double)i).ToArray(); + var step = (valueRange[valueRange.Length-1] - valueRange[0]) / data.Length; + ValueCenters = Enumerable.Range(0, data.Length).Select(i => i * step).ToArray(); + } + + /// + /// Transforms the input sequence of Python objects into a sequence of instances. + /// + /// + /// + public IObservable Process(IObservable source) + { + return source.Select(value => { + var posterior = value[0]; + var valueCenters = value[1]; + var data = (double[])PythonHelper.ConvertPythonObjectToCSharp(posterior); + var argMax = Array.IndexOf(data, data.Max()); + var valueRange = (double[])PythonHelper.ConvertPythonObjectToCSharp(valueCenters); + return new Posterior( + data, + argMax, + valueRange + ); + }); + } +} \ No newline at end of file diff --git a/src/Bonsai.ML.NeuralDecoder/Properties/launchSettings.json b/src/Bonsai.ML.NeuralDecoder/Properties/launchSettings.json new file mode 100644 index 00000000..b48bcfa9 --- /dev/null +++ b/src/Bonsai.ML.NeuralDecoder/Properties/launchSettings.json @@ -0,0 +1,10 @@ +{ + "profiles": { + "Bonsai": { + "commandName": "Executable", + "executablePath": "$(registry:HKEY_CURRENT_USER\\Software\\Bonsai Foundation\\Bonsai@InstallDir)Bonsai.exe", + "commandLineArgs": "--lib:\"$(TargetDir).\"", + "nativeDebugging": true + } + } +} \ No newline at end of file