diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..9efa0974 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,27 @@ +name: Build + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + workflow_call: + +jobs: + build: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v4.1.1 + with: + submodules: true + + - name: Setup MSBuild + uses: microsoft/setup-msbuild@v2 + + - name: Restore NuGet Packages + run: msbuild -t:restore src/Bonsai.ML.sln + + - name: Build Solution + run: msbuild src/Bonsai.ML.sln /p:Configuration=Release \ No newline at end of file diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index f3f3a637..66e49aed 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,27 +5,16 @@ on: workflow_dispatch: jobs: - build: + build_docs: runs-on: windows-latest steps: - - name: Checkout - uses: actions/checkout@v4.1.1 - with: - submodules: true + - name: Build Solution + uses: ./.github/workflows/build.yml - name: Setup .NET Core SDK uses: actions/setup-dotnet@v4.0.0 with: dotnet-version: 7.x - - - name: Setup MSBuild - uses: microsoft/setup-msbuild@v2 - - - name: Restore NuGet Packages - run: msbuild -t:restore src/Bonsai.ML.sln - - - name: Build Solution - run: msbuild src/Bonsai.ML.sln /p:Configuration=Release - name: Setup DocFX run: dotnet tool restore diff --git a/README.md b/README.md index dd326541..282052ba 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ The Bonsai.ML project is a collection of packages with reactive infrastructure f * Bonsai.ML - provides core functionality across all Bonsai.ML packages. * Bonsai.ML.LinearDynamicalSystems - package for performing inference of linear dynamical systems. Interfaces with the [lds_python](https://github.com/joacorapela/lds_python) package. - *Bonsai.ML.LinearDynamicalSystems.Kinematics* - subpackage included in the LinearDynamicalSystems package which supports using the Kalman Filter to infer kinematic data. + - *Bonsai.ML.LinearDynamicalSystems.LinearRegression* - subpackage included in the LinearDynamicalSystems package which supports using the Kalman Filter to perform Bayesian linear regression. * Bonsai.ML.Visualizers - provides a set of visualizers for dynamic graphing/plotting. > [!NOTE] diff --git a/src/Bonsai.ML.LinearDynamicalSystems/Bonsai.ML.LinearDynamicalSystems.csproj b/src/Bonsai.ML.LinearDynamicalSystems/Bonsai.ML.LinearDynamicalSystems.csproj index 4f672b63..f45791fc 100644 --- a/src/Bonsai.ML.LinearDynamicalSystems/Bonsai.ML.LinearDynamicalSystems.csproj +++ b/src/Bonsai.ML.LinearDynamicalSystems/Bonsai.ML.LinearDynamicalSystems.csproj @@ -4,7 +4,7 @@ A Bonsai package for implementing the Kalman Filter using python. Bonsai Rx ML KalmanFilter LinearDynamicalSystems net472;netstandard2.0 - 0.1.0 + 0.2.0 diff --git a/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/CreateKFModel.bonsai b/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/CreateKFModel.bonsai new file mode 100644 index 00000000..e04b18e0 --- /dev/null +++ b/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/CreateKFModel.bonsai @@ -0,0 +1,112 @@ + + + + + + Source1 + + + + + + + + + + + + + 25 + 2 + 2 + + + + + + + + model + + + + model + + + Name + + + + + + InitModel + + + + Source1 + + + + + + {0} = KalmanFilterLinearRegression({1}) + it.Item2, it.Item1 + + + + + + + + LDSModule + + + + + + + + + model = KalmanFilterLinearRegression(likelihood_precision_coef=25, prior_precision_coef=2, n_features=2, x=None, P=None) + + + + + + + + + + + + + + + + + Item1 + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/CreateMultivariatePDF.bonsai b/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/CreateMultivariatePDF.bonsai new file mode 100644 index 00000000..ad6b361f --- /dev/null +++ b/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/CreateMultivariatePDF.bonsai @@ -0,0 +1,136 @@ + + + + + + Source1 + + + + + + + + + model + + + Name + + + + + + Item2 + + + + + + + + + + + + -1 + 1 + 100 + -1 + 1 + 100 + + + + + + + PDF + + + + Source1 + + + {0}.pdf({1}) + it.Item1, it.Item2 + + + + + + + + LDSModule + + + + + + + + + model.pdf(x0=-1, x1=1, xsteps=100, y0=-1, y1=1, ysteps=100) + + + + + + + + + + + + + + + + + + + + + LDSModule + + + + + + + + + model + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/GridParameters.cs b/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/GridParameters.cs new file mode 100644 index 00000000..eeec392b --- /dev/null +++ b/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/GridParameters.cs @@ -0,0 +1,217 @@ +using System.ComponentModel; +using System; +using System.Reactive.Linq; +using Newtonsoft.Json; +using Python.Runtime; + +namespace Bonsai.ML.LinearDynamicalSystems.LinearRegression +{ + + /// + /// Represents an operator that creates the 2D grid parameters used for calculating the PDF of a multivariate distribution. + /// + [Combinator] + [Description("Creates the 2D grid parameters used for calculating the PDF of a multivariate distribution.")] + [WorkflowElementCategory(ElementCategory.Source)] + public class GridParameters + { + + private double _x0 = 0; + private double _x1 = 1; + private int _xsteps = 100; + + private double _y0 = 0; + private double _y1 = 1; + private int _ysteps = 100; + + private string _x0String; + private string _x1String; + private string _xstepsString; + + private string _y0String; + private string _y1String; + private string _ystepsString; + + /// + /// Gets or sets the lower bound of the X axis. + /// + [JsonProperty("x0")] + [Description("The lower bound of the X axis.")] + public double X0 + { + get + { + return _x0; + } + set + { + _x0 = value; + _x0String = double.IsNaN(_x0) ? "None" : _x0.ToString(); + + } + } + + /// + /// Gets or sets the upper bound of the X axis. + /// + [JsonProperty("x1")] + [Description("The upper bound of the X axis.")] + public double X1 + { + get + { + return _x1; + } + set + { + _x1 = value; + _x1String = double.IsNaN(_x1) ? "None" : _x1.ToString(); + } + } + + /// + /// Gets or sets the number of steps along the X axis. + /// + [JsonProperty("xsteps")] + [Description("The number of steps along the X axis.")] + public int XSteps + { + get + { + return _xsteps; + } + set + { + _xsteps = value >= 0 ? value : _xsteps; + _xstepsString = _xsteps.ToString(); + } + } + + /// + /// Gets or sets the lower bound of the Y axis. + /// + [JsonProperty("y0")] + [Description("The lower bound of the Y axis.")] + public double Y0 + { + get + { + return _y0; + } + set + { + _y0 = value; + _y0String = double.IsNaN(_y0) ? "None" : _y0.ToString(); + } + } + + /// + /// Gets or sets the upper bound of the Y axis. + /// + [JsonProperty("y1")] + [Description("The upper bound of the Y axis.")] + public double Y1 + { + get + { + return _y1; + } + set + { + _y1 = value; + _y1String = double.IsNaN(_y1) ? "None" : _y1.ToString(); + } + } + + /// + /// Gets or sets the number of steps along the Y axis. + /// + [JsonProperty("ysteps")] + [Description("The number of steps along the Y axis.")] + public int YSteps + { + get + { + return _ysteps; + } + set + { + _ysteps = value; + _ystepsString = _ysteps.ToString(); + } + } + + /// + /// Generates grid parameters + /// + public IObservable Process() + { + return Observable.Defer(() => Observable.Return( + new GridParameters { + X0 = _x0, + X1 = _x1, + XSteps = _xsteps, + Y0 = _y0, + Y1 = _y1, + YSteps = _ysteps, + })); + } + + /// + /// Gets the grid parameters from a PyObject of a Kalman Filter Linear Regression Model + /// + public IObservable Process(IObservable source) + { + return Observable.Select(source, pyObject => + { + return ConvertPyObject(pyObject); + }); + } + + /// + /// Converts a PyObject, represeting a Kalman Filter Linear Regression Model, into a GridParameters object + /// + public static GridParameters ConvertPyObject(PyObject pyObject) + { + var x0PyObj = pyObject.GetAttr("x0"); + var x1PyObj = pyObject.GetAttr("x1"); + var xStepsPyObj = pyObject.GetAttr("xsteps"); + + var y0PyObj = pyObject.GetAttr("y0"); + var y1PyObj = pyObject.GetAttr("y1"); + var yStepsPyObj = pyObject.GetAttr("ysteps"); + + return new GridParameters { + X0 = x0PyObj, + X1 = x1PyObj, + XSteps = xStepsPyObj, + Y0 = y0PyObj, + Y1 = y1PyObj, + YSteps = yStepsPyObj, + }; + } + + /// + /// Generates grid parameters on each input + /// + public IObservable Process(IObservable source) + { + return Observable.Select(source, x => + new GridParameters { + X0 = _x0, + X1 = _x1, + XSteps = _xsteps, + Y0 = _y0, + Y1 = _y1, + YSteps = _ysteps, + }); + } + + /// + public override string ToString() + { + + return $"x0={_x0}, x1={_x1}, xsteps={_xsteps}, y0={_y0}, y1={_y1}, ysteps={_ysteps}"; + } + } +} diff --git a/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/KFModelParameters.cs b/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/KFModelParameters.cs new file mode 100644 index 00000000..46f8ae9a --- /dev/null +++ b/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/KFModelParameters.cs @@ -0,0 +1,199 @@ +using System.ComponentModel; +using Newtonsoft.Json; +using System; +using System.Reactive.Linq; +using Python.Runtime; +using System.Xml.Serialization; + +namespace Bonsai.ML.LinearDynamicalSystems.LinearRegression +{ + + /// + /// Represents an operator that creates the model parameters for a Kalman Filter Linear Regression python class + /// + [Combinator] + [Description("Creates the model parameters used for initializing a Kalman Filter Linear Regression (KFLR) python class")] + [WorkflowElementCategory(ElementCategory.Source)] + public class KFModelParameters + { + + private double _likelihood_precision_coef; + + private double _prior_precision_coef; + + private int _n_features; + + private double[,] _x = null; + private double[,] _p = null; + + private string _likelihood_precision_coefString; + private string _prior_precision_coefString; + private string _n_featuresString; + private string _xString; + private string _pString; + + /// + /// Gets or sets the likelihood precision coefficient. + /// + [JsonProperty("likelihood_precision_coef")] + [Description("The likelihood precision coefficient.")] + [Category("Parameters")] + public double LikelihoodPrecisionCoefficient + { + get + { + return _likelihood_precision_coef; + } + set + { + _likelihood_precision_coef = value; + _likelihood_precision_coefString = double.IsNaN(_likelihood_precision_coef) ? "None" : _likelihood_precision_coef.ToString(); + } + } + + /// + /// Gets or sets the prior precision coefficient. + /// + [JsonProperty("prior_precision_coef")] + [Description("The prior precision coefficient.")] + [Category("Parameters")] + public double PriorPrecisionCoefficient + { + get + { + return _prior_precision_coef; + } + set + { + _prior_precision_coef = value; + _prior_precision_coefString = double.IsNaN(_prior_precision_coef) ? "None" : _prior_precision_coef.ToString(); + } + } + + /// + /// Gets or sets the number of features present in the model. + /// + [JsonProperty("n_features")] + [Description("The number of features.")] + [Category("Parameters")] + public int NumFeatures + { + get + { + return _n_features; + } + set + { + _n_features = value; + _n_featuresString = _n_features.ToString(); + } + } + + /// + /// Gets or sets the matrix representing the mean of the state. + /// + [XmlIgnore] + [JsonProperty("x")] + [Description("The matrix representing the mean of the state.")] + [Category("ModelState")] + public double[,] X + { + get + { + return _x; + } + set + { + _x = value; + _xString = _x == null ? "None" : NumpyHelper.NumpyParser.ParseArray(_x); + } + } + + /// + /// Gets or sets the matrix representing the covariance between state components. + /// + [XmlIgnore] + [JsonProperty("P")] + [Description("The matrix representing the covariance between state components.")] + [Category("ModelState")] + public double[,] P + { + get + { + return _p; + } + set + { + _p = value; + _pString = _p == null ? "None" : NumpyHelper.NumpyParser.ParseArray(_p); + } + } + + /// + /// Constructs a KF Model Parameters class. + /// + public KFModelParameters () + { + LikelihoodPrecisionCoefficient = 25; + PriorPrecisionCoefficient = 2; + } + + /// + /// Generates parameters for a Kalman Filter Linear Regression Model + /// + public IObservable Process() + { + return Observable.Defer(() => Observable.Return( + new KFModelParameters { + LikelihoodPrecisionCoefficient = _likelihood_precision_coef, + PriorPrecisionCoefficient = _prior_precision_coef, + NumFeatures = _n_features, + X = _x, + P = _p + })); + } + + /// + /// Gets the model parameters from a PyObject of a Kalman Filter Linear Regression Model + /// + public IObservable Process(IObservable source) + { + return Observable.Select(source, pyObject => + { + var likelihood_precision_coefPyObj = pyObject.GetAttr("likelihood_precision_coef"); + var prior_precision_coefPyObj = pyObject.GetAttr("prior_precision_coef"); + var n_featuresPyObj = pyObject.GetAttr("n_features"); + + return new KFModelParameters { + LikelihoodPrecisionCoefficient = likelihood_precision_coefPyObj, + PriorPrecisionCoefficient = _prior_precision_coef, + NumFeatures = n_featuresPyObj, + X = _x, + P = _p + }; + }); + } + + /// + /// Generates parameters for a Kalman Filter Linear Regression Model on each input + /// + public IObservable Process(IObservable source) + { + return Observable.Select(source, x => + new KFModelParameters { + LikelihoodPrecisionCoefficient = _likelihood_precision_coef, + PriorPrecisionCoefficient = _prior_precision_coef, + NumFeatures = _n_features, + X = _x, + P = _p + }); + } + + public override string ToString() + { + + return $"likelihood_precision_coef={_likelihood_precision_coefString}, prior_precision_coef={_prior_precision_coefString}, n_features={_n_featuresString}, x={_xString}, P={_pString}"; + } + } + +} \ No newline at end of file diff --git a/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/MultivariatePDF.cs b/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/MultivariatePDF.cs new file mode 100644 index 00000000..6fc92c6c --- /dev/null +++ b/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/MultivariatePDF.cs @@ -0,0 +1,46 @@ +using System.ComponentModel; +using System; +using System.Reactive.Linq; +using Python.Runtime; +using System.Xml.Serialization; + +namespace Bonsai.ML.LinearDynamicalSystems.LinearRegression +{ + /// + /// Represents an operator that converts a python object, representing a multivariate PDF, into a multivariate PDF class. + /// + [Combinator] + [Description("Converts a python object, representing a multivariate PDF, into a multivariate PDF.")] + [WorkflowElementCategory(ElementCategory.Transform)] + public class MultivariatePDF + { + + /// + /// Gets or sets the grid parameters used for generating the multivariate PDF. + /// + [XmlIgnore] + public GridParameters GridParameters; + + /// + /// Gets or sets the probability density value at each 2D position of the grid. + /// + [XmlIgnore] + public double[,] Values; + + /// + /// Converts a PyObject into a multivariate PDF. + /// + public IObservable Process(IObservable source) + { + return Observable.Select(source, pyObject => + { + var gridParameters = GridParameters.ConvertPyObject(pyObject); + var values = (double[,])pyObject.GetArrayAttribute("pdf_values"); + return new MultivariatePDF { + GridParameters = gridParameters, + Values = values + }; + }); + } + } +} \ No newline at end of file diff --git a/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/PerformInference.bonsai b/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/PerformInference.bonsai new file mode 100644 index 00000000..75aa6fea --- /dev/null +++ b/src/Bonsai.ML.LinearDynamicalSystems/LinearRegression/PerformInference.bonsai @@ -0,0 +1,149 @@ + + + + + + Source1 + + + + + + + + + model + + + Name + + + + + + Predict + + + + Source1 + + + {0}.predict() + it.Item2 + + + + + + + + LDSModule + + + + + + + + + model.predict() + + + + + + + + + + + + + + + + Update + + + + Source1 + + + {0}.update({1}) + it.Item2, it.Item1 + + + + + + + + LDSModule + + + + + + + + + model.update(x=0,y=0) + + + + + + + + + + + + + + + + + + + + + LDSModule + + + + + + + + + model + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Bonsai.ML.LinearDynamicalSystems/Reshape.cs b/src/Bonsai.ML.LinearDynamicalSystems/Reshape.cs new file mode 100644 index 00000000..8279e6bc --- /dev/null +++ b/src/Bonsai.ML.LinearDynamicalSystems/Reshape.cs @@ -0,0 +1,64 @@ +using System; +using System.Reactive.Linq; +using System.ComponentModel; + +namespace Bonsai.ML.LinearDynamicalSystems +{ + /// + /// Represents an operator that reshapes the dimensions of a 2D multi-dimensional array. + /// + [Combinator] + [Description("Reshapes the dimensions of a 2D multi-dimensional array.")] + [WorkflowElementCategory(ElementCategory.Transform)] + public class Reshape + { + /// + /// Gets or sets the number of rows in the reshaped array. + /// + [Description("The number of rows in the reshaped array.")] + public int Rows { get; set; } + + /// + /// Gets or sets the number of columns in the reshaped array. + /// + [Description("The number of columns in the reshaped array.")] + public int Cols { get; set; } + + /// + /// Reshapes a 2D multi-dimensional array into a new multi-dimensional array with the provided number of rows and columns. + /// + public IObservable Process(IObservable source) + { + + var rows = Rows; + var cols = Cols; + + return Observable.Select(source, value => + { + var inputRows = value.GetLength(0); + var inputCols = value.GetLength(1); + int totalElements = inputRows * inputCols; + + if (totalElements != rows * cols) + { + throw new InvalidOperationException($"Multi-dimensional array of shape {rows}x{cols} cannot be made from the input array with a total of {totalElements} elements."); + } + + double[,] reshapedArray = new double[rows, cols]; + + for (int i = 0; i < totalElements; i++) + { + int originalRow = i / inputCols; + int originalCol = i % inputCols; + + int newRow = i / cols; + int newCol = i % cols; + + reshapedArray[newRow, newCol] = value[originalRow, originalCol]; + } + + return reshapedArray; + }); + } + } +} \ No newline at end of file diff --git a/src/Bonsai.ML.LinearDynamicalSystems/SerializeToJson.cs b/src/Bonsai.ML.LinearDynamicalSystems/SerializeToJson.cs index f01437ca..1cbf5a64 100644 --- a/src/Bonsai.ML.LinearDynamicalSystems/SerializeToJson.cs +++ b/src/Bonsai.ML.LinearDynamicalSystems/SerializeToJson.cs @@ -114,5 +114,10 @@ public IObservable Process(IObservable source) { return Process(source); } + + public IObservable Process(IObservable source) + { + return Process(source); + } } } diff --git a/src/Bonsai.ML.LinearDynamicalSystems/Slice.cs b/src/Bonsai.ML.LinearDynamicalSystems/Slice.cs new file mode 100644 index 00000000..9ea20cae --- /dev/null +++ b/src/Bonsai.ML.LinearDynamicalSystems/Slice.cs @@ -0,0 +1,94 @@ +using System; +using System.Reactive.Linq; +using System.ComponentModel; + +namespace Bonsai.ML.LinearDynamicalSystems +{ + /// + /// Represents an operator that slices a 2D multi-dimensional array. + /// + [Combinator] + [Description("Slices a 2D multi-dimensional array.")] + [WorkflowElementCategory(ElementCategory.Transform)] + public class Slice + { + + /// + /// Gets or sets the index to begin slicing the rows of the array. A value of null indicates the first row of the array. + /// + [Description("The index to begin slicing the rows of the array. A value of null indicates the first row of the array.")] + public int? RowStart { get; set; } + + /// + /// Gets or sets the index to stop slicing the rows of the array. A value of null indicates the last row of the array. + /// + [Description("The index to stop slicing the rows of the array. A value of null indicates the last row of the array.")] + public int? RowEnd { get; set; } + + /// + /// Gets or sets the index to begin slicing the columns of the array. A value of null indicates the first column of the array. + /// + [Description("The index to begin slicing the columns of the array. A value of null indicates the first column of the array.")] + public int? ColStart { get; set; } + + /// + /// Gets or sets the index to stop slicing the columns of the array. A value of null indicates the last column of the array. + /// + [Description("The index to stop slicing the columns of the array. A value of null indicates the last column of the array.")] + public int? ColEnd { get; set; } + + /// + /// Slices a 2D multi-dimensional array into a new multi-dimensional array by extracting elements between the provided start and end indices of the rows and columns. + /// + public IObservable Process(IObservable source) + { + + int rowStart = RowStart.HasValue ? RowStart.Value : 0; + int rowEnd = RowEnd.HasValue ? RowEnd.Value : int.MaxValue; + + int colStart = ColStart.HasValue ? ColStart.Value : 0; + int colEnd = ColEnd.HasValue ? ColEnd.Value : int.MaxValue; + + if (rowEnd < rowStart) + { + throw new InvalidOperationException("Starting row must be less than or equal to ending row."); + } + + if (colEnd < colStart) + { + throw new InvalidOperationException("Starting column must be less than or equal to ending column."); + } + + return Observable.Select(source, value => + { + var inputRows = value.GetLength(0); + var inputCols = value.GetLength(1); + + if (rowEnd == int.MaxValue) + { + rowEnd = inputRows; + } + + if (colEnd == int.MaxValue) + { + colEnd = inputCols; + } + + int rowCount = rowEnd - rowStart; + int colCount = colEnd - colStart; + + double[,] slicedArray = new double[rowCount, colCount]; + + for (int i = 0; i < rowCount; i++) + { + for (int j = 0; j < colCount; j++) + { + slicedArray[i, j] = value[rowStart + i, colStart + j]; + } + } + + return slicedArray; + }); + } + } +} \ No newline at end of file diff --git a/src/Bonsai.ML.LinearDynamicalSystems/StateComponent.cs b/src/Bonsai.ML.LinearDynamicalSystems/StateComponent.cs index 77f70da8..57d3eabc 100644 --- a/src/Bonsai.ML.LinearDynamicalSystems/StateComponent.cs +++ b/src/Bonsai.ML.LinearDynamicalSystems/StateComponent.cs @@ -1,6 +1,7 @@ using System.ComponentModel; using System; using System.Xml.Serialization; +using System.Reactive.Linq; using Newtonsoft.Json; namespace Bonsai.ML.LinearDynamicalSystems @@ -53,7 +54,15 @@ public double Variance } /// - /// Extracts a single state compenent from the full state + /// Initializes a new instance of the class. + /// + public StateComponent() + { + } + + /// + /// Initializes a new instance of the class from + /// the full state and covariance matrices given an index /// public StateComponent(double[,] X, double[,] P, int i) { @@ -65,5 +74,31 @@ private double Sigma(double variance) { return 2 * Math.Sqrt(variance); } + + /// + /// Generates a state component + /// + public IObservable Process() + { + return Observable.Defer(() => Observable.Return( + new StateComponent { + Mean = _mean, + Variance = _variance + })); + } + + /// + /// Generates a state component + /// + public IObservable Process(IObservable source) + { + return Observable.Select(source, pyObject => + { + return new StateComponent { + Mean = _mean, + Variance = _variance + }; + }); + } } } diff --git a/src/Bonsai.ML.LinearDynamicalSystems/main.py b/src/Bonsai.ML.LinearDynamicalSystems/main.py index b56c401b..5ff25c64 100644 --- a/src/Bonsai.ML.LinearDynamicalSystems/main.py +++ b/src/Bonsai.ML.LinearDynamicalSystems/main.py @@ -1,19 +1,6 @@ -from lds.inference import OnlineKalmanFilter +from lds.inference import OnlineKalmanFilter, TimeVaryingOnlineKalmanFilter import numpy as np - -DEFAULT_PARAMS = { - "pos_x0" : 0, - "pos_y0" : 0, - "vel_x0" : 0, - "vel_y0" : 0, - "acc_x0" : 0, - "acc_y0" : 0, - "sigma_a" : 10000, - "sigma_x" : 100, - "sigma_y" : 100, - "sqrt_diag_V0_value" : 0.001, - "fps" : 60 -} +from scipy.stats import multivariate_normal class KalmanFilterKinematics(OnlineKalmanFilter): @@ -31,8 +18,6 @@ def __init__(self, fps: int ) -> None: - super(OnlineKalmanFilter, self).__init__() - self.pos_x0=pos_x0 self.pos_y0=pos_y0 self.vel_x0=vel_x0 @@ -51,12 +36,8 @@ def __init__(self, if np.isnan(self.pos_y0): self.pos_y0 = 0 - # build KF matrices for tracking dt = 1.0 / self.fps - # Taken from the book - # barShalomEtAl01-estimationWithApplicationToTrackingAndNavigation.pdf - # section 6.3.3 - # Eq. 6.3.3-2 + B = np.array([ [1, dt, .5*dt**2, 0, 0, 0], [0, 1, dt, 0, 0, 0], [0, 0, 1, 0, 0, 0], @@ -64,8 +45,10 @@ def __init__(self, [0, 0, 0, 0, 1, dt], [0, 0, 0, 0, 0, 1]], dtype=np.double) + Z = np.array([ [1, 0, 0, 0, 0, 0], [0, 0, 0, 1, 0, 0]], + dtype=np.double) Qt = np.array([ [dt**4/4, dt**3/2, dt**2/2, 0, 0, 0], [dt**3/2, dt**2, dt, 0, 0, 0], @@ -91,12 +74,61 @@ def update(self, x, y): return super().update(y=np.array([x, y])) -if __name__ == "__main__": - - model = KalmanFilterKinematics(**DEFAULT_PARAMS) - x, y = 100, 100 - model.predict() - model.update(x, y) - print(model.x) - print(model.x.shape) - print(model.P.shape) \ No newline at end of file +class KalmanFilterLinearRegression(TimeVaryingOnlineKalmanFilter): + + def __init__(self, + likelihood_precision_coef: float, + prior_precision_coef: float, + n_features: int, + x: list[list[float]] = None, + P: list[list[float]] = None, + ) -> None: + + self.likelihood_precision_coef = likelihood_precision_coef + self.prior_precision_coef = prior_precision_coef + self.n_features = n_features + + if x is None: + x = np.zeros((self.n_features,1), dtype=np.double) + else: + x = np.array(x) + + if P is None: + P = 1.0 / self.prior_precision_coef * np.eye(len(x)) + else: + P = np.array(P) + + self.x = x + self.P = P + + self.B = np.eye(N=len(self.x)) + self.Q = np.zeros(shape=((len(self.x), len(self.x)))) + self.R = np.array([[1.0/self.likelihood_precision_coef]]) + + super().__init__() + + def predict(self): + self.x, self.P = super().predict(x = self.x, P = self.P, B = self.B, Q = self.Q) + + def update(self, x, y): + if not isinstance(x, list): + x = [x] + self.x, self.P = super().update(y = np.array(y, dtype=np.float64), x = self.x, P = self.P, Z = np.array(x).reshape((1, -1)), R = self.R) + if self.x.ndim == 1: + self.x = self.x[:, np.newaxis] + + def pdf(self, x0 = 0, x1 = 1, xsteps = 100, y0 = 0, y1 = 1, ysteps = 100): + + self.x0 = x0 + self.x1 = x1 + self.xsteps = xsteps + self.y0 = y0 + self.y1 = y1 + self.ysteps = ysteps + + rv = multivariate_normal(self.x.flatten(), self.P) + xpos = np.linspace(x0, x1, xsteps) + ypos = np.linspace(y0, y1, ysteps) + xx, yy = np.meshgrid(xpos, ypos) + pos = np.dstack((xx, yy)) + self.pdf_values = rv.pdf(pos) \ No newline at end of file diff --git a/src/Bonsai.ML.Visualizers/Bonsai.ML.Visualizers.csproj b/src/Bonsai.ML.Visualizers/Bonsai.ML.Visualizers.csproj index 58bc3adf..b834503e 100644 --- a/src/Bonsai.ML.Visualizers/Bonsai.ML.Visualizers.csproj +++ b/src/Bonsai.ML.Visualizers/Bonsai.ML.Visualizers.csproj @@ -5,7 +5,7 @@ Bonsai Rx ML Machine Learning Visualizers net472 true - 0.1.0 + 0.2.0 diff --git a/src/Bonsai.ML.Visualizers/ColorPalette.cs b/src/Bonsai.ML.Visualizers/ColorPalette.cs new file mode 100644 index 00000000..5f887fe7 --- /dev/null +++ b/src/Bonsai.ML.Visualizers/ColorPalette.cs @@ -0,0 +1,20 @@ +namespace Bonsai.ML.Visualizers +{ + internal enum ColorPalette + { + Cividis, + Inferno, + Viridis, + Magma, + Plasma, + BlackWhiteRed, + BlueWhiteRed, + Cool, + Gray, + Hot, + Hue, + HueDistinct, + Jet, + Rainbow + } +} diff --git a/src/Bonsai.ML.Visualizers/HeatMapSeriesOxyPlotBase.cs b/src/Bonsai.ML.Visualizers/HeatMapSeriesOxyPlotBase.cs new file mode 100644 index 00000000..ee80be9b --- /dev/null +++ b/src/Bonsai.ML.Visualizers/HeatMapSeriesOxyPlotBase.cs @@ -0,0 +1,233 @@ +using System.Windows.Forms; +using OxyPlot; +using OxyPlot.Series; +using OxyPlot.WindowsForms; +using System.Drawing; +using System; +using OxyPlot.Axes; +using System.Collections.Generic; + +namespace Bonsai.ML.Visualizers +{ + internal class HeatMapSeriesOxyPlotBase : UserControl + { + private PlotView view; + private PlotModel model; + private HeatMapSeries heatMapSeries; + private LinearColorAxis colorAxis; + + private ToolStripComboBox paletteComboBox; + private ToolStripLabel paletteLabel; + private int _paletteSelectedIndex; + private OxyPalette palette; + + private ToolStripComboBox renderMethodComboBox; + private ToolStripLabel renderMethodLabel; + private int _renderMethodSelectedIndex; + private HeatMapRenderMethod renderMethod = HeatMapRenderMethod.Bitmap; + + private StatusStrip statusStrip; + + private int _numColors = 100; + + /// + /// Event handler which can be used to hook into events generated when the combobox values have changed. + /// + public event EventHandler PaletteComboBoxValueChanged; + + public ToolStripComboBox PaletteComboBox => paletteComboBox; + + public event EventHandler RenderMethodComboBoxValueChanged; + + public ToolStripComboBox RenderMethodComboBox => renderMethodComboBox; + + public StatusStrip StatusStrip => statusStrip; + + /// + /// Constructor of the TimeSeriesOxyPlotBase class. + /// Requires a line series name and an area series name. + /// Data source is optional, since pasing it to the constructor will populate the combobox and leave it empty otherwise. + /// The selected index is only needed when the data source is provided. + /// + public HeatMapSeriesOxyPlotBase(int paletteSelectedIndex, int renderMethodSelectedIndex, int numColors = 100) + { + _paletteSelectedIndex = paletteSelectedIndex; + _renderMethodSelectedIndex = renderMethodSelectedIndex; + _numColors = numColors; + Initialize(); + } + + private void Initialize() + { + view = new PlotView + { + Dock = DockStyle.Fill, + }; + + model = new PlotModel(); + + heatMapSeries = new HeatMapSeries { + X0 = 0, + X1 = 100, + Y0 = 0, + Y1 = 100, + Interpolate = true, + RenderMethod = renderMethod, + CoordinateDefinition = HeatMapCoordinateDefinition.Edge + }; + + colorAxis = new LinearColorAxis { + Position = AxisPosition.Right, + }; + + model.Axes.Add(colorAxis); + model.Series.Add(heatMapSeries); + + view.Model = model; + Controls.Add(view); + + InitializeColorPalette(); + InitializeRenderMethod(); + + statusStrip = new StatusStrip + { + Visible = false + }; + + statusStrip.Items.AddRange(new ToolStripItem[] { + paletteLabel, + paletteComboBox, + renderMethodLabel, + renderMethodComboBox + }); + + Controls.Add(statusStrip); + view.MouseClick += new MouseEventHandler(onMouseClick); + AutoScaleDimensions = new SizeF(6F, 13F); + } + + private void InitializeColorPalette() + { + paletteLabel = new ToolStripLabel + { + Text = "Color palette:", + AutoSize = true + }; + + paletteComboBox = new ToolStripComboBox() + { + Name = "palette", + AutoSize = true, + }; + + foreach (var value in Enum.GetValues(typeof(ColorPalette))) + { + paletteComboBox.Items.Add(value); + } + + paletteComboBox.SelectedIndexChanged += PaletteComboBoxSelectedIndexChanged; + paletteComboBox.SelectedIndex = _paletteSelectedIndex; + UpdateColorPalette(); + } + + private void PaletteComboBoxSelectedIndexChanged(object sender, EventArgs e) + { + if (paletteComboBox.SelectedIndex != _paletteSelectedIndex) + { + _paletteSelectedIndex = paletteComboBox.SelectedIndex; + UpdateColorPalette(); + PaletteComboBoxValueChanged?.Invoke(this, e); + UpdatePlot(); + } + } + + private void UpdateColorPalette() + { + var selectedPalette = (ColorPalette)paletteComboBox.Items[_paletteSelectedIndex]; + paletteLookup.TryGetValue(selectedPalette, out Func paletteMethod); + palette = paletteMethod(_numColors); + colorAxis.Palette = palette; + } + + private void InitializeRenderMethod() + { + renderMethodLabel = new ToolStripLabel + { + Text = "Render method:", + AutoSize = true + }; + + renderMethodComboBox = new ToolStripComboBox() + { + Name = "renderMethod", + AutoSize = true, + }; + + foreach (var value in Enum.GetValues(typeof(HeatMapRenderMethod))) + { + renderMethodComboBox.Items.Add(value); + } + + renderMethodComboBox.SelectedIndexChanged += renderMethodComboBoxSelectedIndexChanged; + renderMethodComboBox.SelectedIndex = _renderMethodSelectedIndex; + UpdateRenderMethod(); + } + + private void renderMethodComboBoxSelectedIndexChanged(object sender, EventArgs e) + { + if (renderMethodComboBox.SelectedIndex != _renderMethodSelectedIndex) + { + _renderMethodSelectedIndex = renderMethodComboBox.SelectedIndex; + UpdateRenderMethod(); + RenderMethodComboBoxValueChanged?.Invoke(this, e); + UpdatePlot(); + } + } + + private void UpdateRenderMethod() + { + renderMethod = (HeatMapRenderMethod)renderMethodComboBox.Items[_renderMethodSelectedIndex]; + heatMapSeries.RenderMethod = renderMethod; + } + + private void onMouseClick(object sender, MouseEventArgs e) + { + if (e.Button == MouseButtons.Right) + { + statusStrip.Visible = !statusStrip.Visible; + } + } + + 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; + heatMapSeries.Data = data; + } + + public void UpdatePlot() + { + model.InvalidatePlot(true); + } + + private static readonly Dictionary> paletteLookup = new Dictionary> + { + { ColorPalette.Cividis, (numColors) => OxyPalettes.Cividis(numColors) }, + { ColorPalette.Inferno, (numColors) => OxyPalettes.Inferno(numColors) }, + { ColorPalette.Viridis, (numColors) => OxyPalettes.Viridis(numColors) }, + { ColorPalette.Magma, (numColors) => OxyPalettes.Magma(numColors) }, + { ColorPalette.Plasma, (numColors) => OxyPalettes.Plasma(numColors) }, + { ColorPalette.BlackWhiteRed, (numColors) => OxyPalettes.BlackWhiteRed(numColors) }, + { ColorPalette.BlueWhiteRed, (numColors) => OxyPalettes.BlueWhiteRed(numColors) }, + { ColorPalette.Cool, (numColors) => OxyPalettes.Cool(numColors) }, + { ColorPalette.Gray, (numColors) => OxyPalettes.Gray(numColors) }, + { ColorPalette.Hot, (numColors) => OxyPalettes.Hot(numColors) }, + { ColorPalette.Hue, (numColors) => OxyPalettes.Hue(numColors) }, + { ColorPalette.HueDistinct, (numColors) => OxyPalettes.HueDistinct(numColors) }, + { ColorPalette.Jet, (numColors) => OxyPalettes.Jet(numColors) }, + { ColorPalette.Rainbow, (numColors) => OxyPalettes.Rainbow(numColors) }, + }; + } +} diff --git a/src/Bonsai.ML.Visualizers/MultidimensionalArrayVisualizer.cs b/src/Bonsai.ML.Visualizers/MultidimensionalArrayVisualizer.cs new file mode 100644 index 00000000..6977f8bd --- /dev/null +++ b/src/Bonsai.ML.Visualizers/MultidimensionalArrayVisualizer.cs @@ -0,0 +1,92 @@ +using System; +using System.Windows.Forms; +using System.Collections.Generic; +using System.Reflection; +using Bonsai; +using Bonsai.Design; +using Bonsai.ML.Visualizers; +using Bonsai.ML.LinearDynamicalSystems; +using Bonsai.ML.LinearDynamicalSystems.LinearRegression; +using System.Drawing; +using System.Reactive; +using Bonsai.Reactive; +using Bonsai.Expressions; +using OxyPlot; + +[assembly: TypeVisualizer(typeof(MultidimensionalArrayVisualizer), Target = typeof(double[,]))] + +namespace Bonsai.ML.Visualizers +{ + + /// + /// Provides a type visualizer to display the state components of a Kalman Filter kinematics model. + /// + public class MultidimensionalArrayVisualizer : 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; + + /// + public override void Load(IServiceProvider provider) + { + Plot = new HeatMapSeriesOxyPlotBase(PaletteSelectedIndex, RenderMethodSelectedIndex) + { + Dock = DockStyle.Fill, + }; + + Plot.PaletteComboBoxValueChanged += PaletteIndexChanged; + Plot.RenderMethodComboBoxValueChanged += RenderMethodIndexChanged; + + var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService)); + if (visualizerService != null) + { + visualizerService.AddControl(Plot); + } + } + + /// + public override void Show(object value) + { + var mdarray = (double[,])value; + var shape = new int[] {mdarray.GetLength(0), mdarray.GetLength(1)}; + + Plot.UpdateHeatMapSeries( + -0.5, + shape[0] - 0.5, + -0.5, + shape[1] - 0.5, + mdarray + ); + + 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.Visualizers/MultivariatePDFVisualizer.cs b/src/Bonsai.ML.Visualizers/MultivariatePDFVisualizer.cs new file mode 100644 index 00000000..4b9012c1 --- /dev/null +++ b/src/Bonsai.ML.Visualizers/MultivariatePDFVisualizer.cs @@ -0,0 +1,87 @@ +using System; +using System.Windows.Forms; +using System.Collections.Generic; +using System.Reflection; +using Bonsai; +using Bonsai.Design; +using Bonsai.ML.Visualizers; +using Bonsai.ML.LinearDynamicalSystems; +using Bonsai.ML.LinearDynamicalSystems.LinearRegression; +using System.Drawing; +using System.Reactive; +using Bonsai.Reactive; + +[assembly: TypeVisualizer(typeof(MultivariatePDFVisualizer), Target = typeof(MultivariatePDF))] + +namespace Bonsai.ML.Visualizers +{ + + /// + /// Provides a type visualizer to display the state components of a Kalman Filter kinematics model. + /// + public class MultivariatePDFVisualizer : 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; + + /// + public override void Load(IServiceProvider provider) + { + Plot = new HeatMapSeriesOxyPlotBase(PaletteSelectedIndex, RenderMethodSelectedIndex) + { + Dock = DockStyle.Fill, + }; + + Plot.PaletteComboBoxValueChanged += PaletteIndexChanged; + Plot.RenderMethodComboBoxValueChanged += RenderMethodIndexChanged; + + var visualizerService = (IDialogTypeVisualizerService)provider.GetService(typeof(IDialogTypeVisualizerService)); + if (visualizerService != null) + { + visualizerService.AddControl(Plot); + } + } + + /// + public override void Show(object value) + { + var pdf = (MultivariatePDF)value; + Plot.UpdateHeatMapSeries( + pdf.GridParameters.X0 - (1 / 2 * pdf.GridParameters.XSteps), + pdf.GridParameters.X1 - (1 / 2 * pdf.GridParameters.XSteps), + pdf.GridParameters.Y0 - (1 / 2 * pdf.GridParameters.YSteps), + pdf.GridParameters.Y1 - (1 / 2 * pdf.GridParameters.YSteps), + pdf.Values + ); + 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.Visualizers/StateComponentVisualizer.cs b/src/Bonsai.ML.Visualizers/StateComponentVisualizer.cs index 6a71301c..716aa75e 100644 --- a/src/Bonsai.ML.Visualizers/StateComponentVisualizer.cs +++ b/src/Bonsai.ML.Visualizers/StateComponentVisualizer.cs @@ -80,7 +80,6 @@ protected override void Show(DateTime time, object value) { _startTime = time; Plot.StartTime = _startTime.Value; - // Plot.ResetSeries(); Plot.ResetAxes(); } diff --git a/src/Bonsai.ML.Visualizers/TimeSeriesOxyPlotBase.cs b/src/Bonsai.ML.Visualizers/TimeSeriesOxyPlotBase.cs index b4e2e440..9256364e 100644 --- a/src/Bonsai.ML.Visualizers/TimeSeriesOxyPlotBase.cs +++ b/src/Bonsai.ML.Visualizers/TimeSeriesOxyPlotBase.cs @@ -31,6 +31,8 @@ internal class TimeSeriesOxyPlotBase : UserControl /// public int Capacity { get; set; } + public StatusStrip StatusStrip => statusStrip; + /// /// Buffer the data beyond the capacity. /// @@ -84,6 +86,8 @@ private void Initialize() view.MouseClick += new MouseEventHandler(onMouseClick); Controls.Add(statusStrip); + Controls.Add(statusStrip); + AutoScaleDimensions = new SizeF(6F, 13F); }